Skip to content

Commit 115a75f

Browse files
committed
Add a workflow definition to upload Git for Windows' snapshots
This is another step in the migration from the Azure Pipeline that produced Git for Windows' snapshot builds and the Azure Release Pipeline that then uploaded them to Azure Blobs. With this new GitHub workflow, all we need are the successful `git-artifacts` runs (one per CPU architecture), and the workflow will upload them to the new snapshot location reachable via https://gitforwindows.org/git-snapshots. Signed-off-by: Johannes Schindelin <[email protected]>
1 parent 600d642 commit 115a75f

File tree

2 files changed

+287
-0
lines changed

2 files changed

+287
-0
lines changed

.github/workflows/upload-snapshot.yml

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
name: upload-snapshot
2+
run-name: "Upload Git for Windows snapshot"
3+
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
git_artifacts_i686_workflow_run_id:
8+
description: 'ID of the git-artifacts (i686) workflow run'
9+
required: true
10+
git_artifacts_x86_64_workflow_run_id:
11+
description: 'ID of the git-artifacts (x86_64) workflow run'
12+
required: true
13+
git_artifacts_aarch64_workflow_run_id:
14+
description: 'ID of the git-artifacts (aarch64) workflow run'
15+
required: true
16+
17+
env:
18+
OWNER: "${{ github.repository_owner }}"
19+
REPO: git
20+
SNAPSHOTS_REPO: git-snapshots
21+
ARTIFACTS_REPO: git-for-windows-automation
22+
I686_WORKFLOW_RUN_ID: "${{ github.event.inputs.git_artifacts_i686_workflow_run_id }}"
23+
X86_64_WORKFLOW_RUN_ID: "${{ github.event.inputs.git_artifacts_x86_64_workflow_run_id }}"
24+
AARCH64_WORKFLOW_RUN_ID: "${{ github.event.inputs.git_artifacts_aarch64_workflow_run_id }}"
25+
CREATE_CHECK_RUN: "true"
26+
NODEJS_VERSION: 16
27+
28+
jobs:
29+
upload-snapshot:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
- name: download `bundle-artifacts`
34+
id: bundle-artifacts
35+
uses: actions/github-script@v7
36+
with:
37+
script: |
38+
const {
39+
getWorkflowRunArtifactsURLs,
40+
downloadAndUnZip,
41+
} = require('./github-release')
42+
43+
const token = ${{ toJSON(secrets.GITHUB_TOKEN) }}
44+
const workflowRunId = process.env.X86_64_WORKFLOW_RUN_ID
45+
const urls = await getWorkflowRunArtifactsURLs(
46+
console,
47+
token,
48+
process.env.OWNER,
49+
process.env.ARTIFACTS_REPO,
50+
workflowRunId
51+
)
52+
core.setOutput('x86_64-urls', urls)
53+
54+
const dir = 'bundle-artifacts-x86_64'
55+
await downloadAndUnZip(token, urls['bundle-artifacts'], dir)
56+
57+
const fs = require('fs')
58+
const sha = fs.readFileSync(`${dir}/git-commit-oid`, 'utf-8').trim()
59+
core.notice(`git-commit-oid: ${sha}`)
60+
61+
const githubApiRequest = require('./github-api-request')
62+
const { commit: { committer: { date } } } = await githubApiRequest(
63+
console,
64+
token,
65+
'GET',
66+
`/repos/${process.env.OWNER}/${process.env.REPO}/commits/${sha}`
67+
)
68+
69+
// emulate Git's date/time format
70+
core.setOutput('date', new Date(date).toLocaleString('en-US', {
71+
weekday: 'short',
72+
month: 'short',
73+
day: 'numeric',
74+
year: 'numeric',
75+
hour: '2-digit',
76+
minute: '2-digit',
77+
second: '2-digit',
78+
timeZoneName: 'longOffset',
79+
}).replace(/^(.*,.*),(.*),(.* )((PM|AM) GMT)([-+]\d\d):(\d\d)$/, '$1$2$3$6$7'))
80+
core.setOutput('git-commit-oid', sha)
81+
core.setOutput('ver', fs.readFileSync(`${dir}/ver`, 'utf-8').trim())
82+
- name: Mirror Check Run to ${{ env.OWNER }}/${{ env.REPO }}
83+
if: env.CREATE_CHECK_RUN != 'false'
84+
uses: ./.github/actions/check-run-action
85+
with:
86+
app-id: ${{ secrets.GH_APP_ID }}
87+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
88+
owner: ${{ env.OWNER }}
89+
repo: ${{ env.REPO }}
90+
rev: ${{ steps.bundle-artifacts.outputs.git-commit-oid }}
91+
check-run-name: "upload-snapshot"
92+
title: "Upload snapshot ${{ steps.bundle-artifacts.outputs.ver }}"
93+
summary: "Upload snapshot ${{ steps.bundle-artifacts.outputs.ver }}"
94+
text: "For details, see [this run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id}})."
95+
details-url: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id}}"
96+
- name: download remaining artifacts
97+
id: download-artifacts
98+
uses: actions/github-script@v7
99+
with:
100+
script: |
101+
const {
102+
getWorkflowRunArtifactsURLs,
103+
downloadAndUnZip,
104+
architectures
105+
} = require('./github-release')
106+
107+
const token = ${{ toJSON(secrets.GITHUB_TOKEN) }}
108+
const directories = ['bundle-artifacts-x86_64']
109+
for (const arch of architectures) {
110+
const architecture = arch.name
111+
112+
const urls = architecture === 'x86_64'
113+
? ${{ steps.bundle-artifacts.outputs.x86_64-urls }}
114+
: await getWorkflowRunArtifactsURLs(
115+
console,
116+
token,
117+
process.env.OWNER,
118+
process.env.ARTIFACTS_REPO,
119+
process.env[`${architecture.toUpperCase()}_WORKFLOW_RUN_ID`]
120+
)
121+
for (const name of Object.keys(urls)) {
122+
if (name === 'bundle-artifacts' && architecture === 'x86_64') continue // already got it
123+
if (!name.match(/^(installer|portable|mingit|bundle)/)) continue
124+
const outputDirectory = name.endsWith(`-${architecture}`) ? name : `${name}-${architecture}`
125+
console.log(`Downloading ${name} and extracting to ${outputDirectory}/`)
126+
await downloadAndUnZip(token, urls[name], outputDirectory)
127+
directories.push(outputDirectory)
128+
}
129+
}
130+
131+
const fs = require('fs')
132+
const assetsToUpload = directories
133+
.map(directory => fs
134+
.readdirSync(directory)
135+
.filter(file => file.match(/^(Min|Portable)Git-.*\.(exe|zip)$/))
136+
.map(file => `${directory}/${file}`))
137+
.flat()
138+
if (assetsToUpload.length === 0) throw new Error(`No assets to upload!`)
139+
console.log(JSON.stringify(assetsToUpload, null, 2))
140+
core.setOutput('paths', assetsToUpload.join(' '))
141+
return assetsToUpload
142+
- name: update check-run
143+
if: env.CREATE_CHECK_RUN != 'false'
144+
uses: ./.github/actions/check-run-action
145+
with:
146+
app-id: ${{ secrets.GH_APP_ID }}
147+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
148+
append-text: 'Downloaded all artifacts'
149+
- name: validate
150+
id: validate
151+
uses: actions/github-script@v7
152+
with:
153+
script: |
154+
const fs = require('fs')
155+
156+
const { architectures } = require('./github-release')
157+
for (const arch of architectures) {
158+
const ver = fs.readFileSync(`bundle-artifacts-${arch.name}/ver`, 'utf-8').trim()
159+
if (${{ toJSON(steps.bundle-artifacts.outputs.ver) }} !== ver) {
160+
core.error(`Mismatched version between x86_64 and ${arch.name}: ${{ toJSON(steps.bundle-artifacts.outputs.ver) }} != "${ver}"`)
161+
process.exit(1)
162+
}
163+
}
164+
165+
const githubApiRequest = require('./github-api-request')
166+
const { ahead_by } = await githubApiRequest(
167+
console,
168+
${{ toJSON(secrets.GITHUB_TOKEN) }},
169+
'GET',
170+
`/repos/${process.env.OWNER}/${process.env.REPO}/compare/HEAD...${{ steps.bundle-artifacts.outputs.git-commit-oid }}`
171+
)
172+
if (ahead_by !== 0) {
173+
core.error(`The snapshots are built from a commit that is not reachable from git-for-windows/git's default branch!`)
174+
process.exit(1)
175+
}
176+
- name: configure token
177+
id: snapshots-token
178+
uses: actions/github-script@v7
179+
with:
180+
result-encoding: string
181+
script: |
182+
const { callGit, getPushAuthorizationHeader } = require('./repository-updates')
183+
const header = await getPushAuthorizationHeader(
184+
console,
185+
core.setSecret,
186+
${{ secrets.GH_APP_ID }},
187+
${{ toJSON(secrets.GH_APP_PRIVATE_KEY) }},
188+
process.env.OWNER,
189+
process.env.SNAPSHOTS_REPO
190+
)
191+
console.log(callGit([
192+
'config',
193+
'--global',
194+
`http.${{ github.server_url }}/${process.env.OWNER}/${process.env.SNAPSHOTS_REPO}.extraHeader`,
195+
header
196+
]))
197+
return Buffer.from(header.replace(/^Authorization: Basic /, ''), 'base64').toString('utf-8').replace(/^PAT:/, '')
198+
- name: figure out if we need to push commits
199+
uses: actions/github-script@v7
200+
with:
201+
script: |
202+
// Since `git-snapshots` is a fork, and forks share the same object store, we can
203+
// assume that `git-commit-oid` is accessible in the `git-snapshots` repository even
204+
// if it might not yet be reachable.
205+
const githubApiRequest = require('./github-api-request')
206+
const token = ${{ toJSON(steps.snapshots-token.outputs.result) }}
207+
const sha = ${{ toJSON(steps.bundle-artifacts.outputs.git-commit-oid) }}
208+
const { ahead_by, behind_by } = await githubApiRequest(
209+
console,
210+
token,
211+
'GET',
212+
`/repos/${process.env.OWNER}/${process.env.SNAPSHOTS_REPO}/compare/${sha}...HEAD`
213+
)
214+
if (ahead_by > 0) throw new Error(`The snapshots repository is ahead of ${sha}!`)
215+
if (behind_by > 0) {
216+
await githubApiRequest(
217+
console,
218+
token,
219+
'PATCH',
220+
`/repos/${process.env.OWNER}/${process.env.SNAPSHOTS_REPO}/git/refs/heads/main`, {
221+
sha,
222+
force: false // require fast-forward
223+
}
224+
)
225+
}
226+
- name: upload snapshots to ${{ env.SNAPSHOTS_REPO }}
227+
env:
228+
GH_TOKEN: ${{ steps.snapshots-token.outputs.result }}
229+
run: |
230+
gh release create \
231+
-R "$OWNER/$SNAPSHOTS_REPO" \
232+
--target "${{ steps.bundle-artifacts.outputs.git-commit-oid }}" \
233+
--title "${{ steps.bundle-artifacts.outputs.date }}" \
234+
${{ steps.bundle-artifacts.outputs.ver }} \
235+
${{ steps.download-artifacts.outputs.paths }} &&
236+
echo "::notice::Uploaded snapshot artifacts to ${{ github.server_url }}/${{ env.OWNER }}/${{ env.SNAPSHOTS_REPO }}/releases/tag/${{ steps.bundle-artifacts.outputs.ver }}"
237+
- name: update check-run
238+
if: env.CREATE_CHECK_RUN != 'false'
239+
uses: ./.github/actions/check-run-action
240+
with:
241+
app-id: ${{ secrets.GH_APP_ID }}
242+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
243+
append-text: 'Created release at ${{ github.server_url }}/${{ env.OWNER }}/${{ env.SNAPSHOTS_REPO }}/releases/tag/${{ steps.bundle-artifacts.outputs.ver }}'
244+
- name: clone gh-pages
245+
uses: actions/checkout@v4
246+
with:
247+
repository: ${{ env.OWNER }}/${{ env.SNAPSHOTS_REPO }}
248+
ref: gh-pages
249+
path: gh-pages
250+
token: ${{ steps.snapshots-token.outputs.result }}
251+
- name: update index.html
252+
uses: actions/github-script@v7
253+
with:
254+
script: |
255+
const urlPrefix = `${{ github.server_url }}/${{ env.OWNER }}/${{ env.SNAPSHOTS_REPO }}/releases/download/${{ steps.bundle-artifacts.outputs.ver }}/`
256+
process.chdir('gh-pages')
257+
const main = require('./add-entry')
258+
await main(
259+
'--date=${{ steps.bundle-artifacts.outputs.date }}',
260+
'--commit=${{ steps.bundle-artifacts.outputs.git-commit-oid }}',
261+
...${{ steps.download-artifacts.outputs.result }}
262+
.map(path => `${urlPrefix}${path.replace(/.*\//, '')}`)
263+
)
264+
- name: push gh-pages
265+
run: |
266+
git -C gh-pages \
267+
-c user.name="${{ github.actor }}" \
268+
-c user.email="${{ github.actor }}@noreply.github.com" \
269+
commit -sm "Add snapshot: ${{ steps.bundle-artifacts.outputs.ver }}" index.html &&
270+
git -C gh-pages push &&
271+
echo "::notice::Updated https://gitforwindows.org/git-snapshots (pending GitHub Pages deployment)"
272+
- name: update check-run
273+
if: env.CREATE_CHECK_RUN != 'false'
274+
uses: ./.github/actions/check-run-action
275+
with:
276+
app-id: ${{ secrets.GH_APP_ID }}
277+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
278+
append-text: 'Updated https://gitforwindows.org/git-snapshots (pending GitHub Pages deployment)'
279+
- name: mark check run as completed
280+
if: env.CREATE_CHECK_RUN != 'false' && always()
281+
uses: ./.github/actions/check-run-action
282+
with:
283+
app-id: ${{ secrets.GH_APP_ID }}
284+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
285+
append-text: "${{ job.status == 'success' && 'Done!' || format('Completed: {0}', job.status) }}."
286+
conclusion: ${{ job.status }}

github-release.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,4 +349,5 @@ module.exports = {
349349
pushGitTag,
350350
downloadReleaseAssets,
351351
downloadReleaseAssetsFromURL,
352+
architectures,
352353
}

0 commit comments

Comments
 (0)