Skip to content

Commit fa784e9

Browse files
authored
Merge pull request #2 from dscho/sync-gitster-git
Add a scheduled workflow to synchronize branches from `gitster/git`
2 parents 6abbef2 + 525752b commit fa784e9

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed
+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
name: sync-gitster-git-branches
2+
3+
on:
4+
schedule:
5+
- cron: '17 6 * * *'
6+
workflow_dispatch:
7+
8+
env:
9+
SOURCE_REPOSITORY: gitster/git
10+
TARGET_REPOSITORY: gitgitgadget/git
11+
12+
# We want to limit queuing to a single workflow run i.e. if there is already
13+
# an active workflow run and a queued one, queue another one canceling the
14+
# already queued one.
15+
concurrency:
16+
group: ${{ github.workflow }}
17+
18+
jobs:
19+
sync-gitster-git-branches:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: check which refs need to be synchronized
23+
uses: actions/github-script@v6
24+
id: check
25+
with:
26+
script: |
27+
const sleep = async (milliseconds) => {
28+
return new Promise(resolve => setTimeout(resolve, milliseconds))
29+
}
30+
31+
const getRefs = async (repository) => {
32+
let attemptCounter = 1
33+
for (;;) {
34+
try {
35+
const [owner, repo] = repository.split('/')
36+
const { data } = await github.rest.git.listMatchingRefs({
37+
owner,
38+
repo,
39+
// We want to match `maint-*` as well as `[a-z][a-z]/*`
40+
// sadly, this is not possible via GitHub's REST API,
41+
// hence we do it below via the `filter()` call.
42+
ref: 'heads/'
43+
})
44+
return data
45+
.filter(e => e.ref.match(/^refs\/heads\/(maint-\d|[a-z][a-z]\/)/))
46+
.sort((a, b) => a.ref.localeCompare(b.ref))
47+
} catch (e) {
48+
if (e?.status !== 502) throw e
49+
}
50+
51+
if (++attemptCounter > 10) throw new Error('Giving up listing refs after 10 attempts')
52+
53+
const seconds = attemptCounter * attemptCounter + 15 * Math.random()
54+
core.info(`Encountered a Server Error; retrying in ${seconds} seconds`)
55+
await sleep(1000 * seconds)
56+
}
57+
}
58+
59+
const sourceRefs = await getRefs(process.env.SOURCE_REPOSITORY)
60+
const targetRefs = await getRefs(process.env.TARGET_REPOSITORY)
61+
62+
const refspecs = []
63+
const toFetch = new Set()
64+
for (let i = 0, j = 0; i < sourceRefs.length || j < targetRefs.length; ) {
65+
const compare = i >= sourceRefs.length
66+
? +1
67+
: j >= targetRefs.length
68+
? -1
69+
: sourceRefs[i].ref.localeCompare(targetRefs[j].ref)
70+
if (compare > 0) {
71+
// no source ref => delete target ref
72+
refspecs.push(`:${targetRefs[j].ref}`)
73+
j++
74+
} else if (compare < 0) {
75+
// no corresponding target ref yet => push source ref (new)
76+
const sha = sourceRefs[i].object.sha
77+
toFetch.add(sha)
78+
refspecs.push(`${sha}:${sourceRefs[i].ref}`)
79+
i++
80+
} else {
81+
// the sourceRef's name matches the targetRef's
82+
if (sourceRefs[i].object.sha !== targetRefs[j].object.sha) {
83+
// target ref needs updating
84+
const sha = sourceRefs[i].object.sha
85+
toFetch.add(sha)
86+
refspecs.push(`+${sha}:${sourceRefs[i].ref}`)
87+
}
88+
i++
89+
j++
90+
}
91+
}
92+
93+
core.setOutput('refspec', refspecs.join(' '))
94+
targetRefs.forEach((e) => toFetch.delete(e.object.sha))
95+
core.setOutput('to-fetch', [...toFetch].join(' '))
96+
- name: obtain installation token
97+
if: steps.check.outputs.refspec != ''
98+
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92
99+
id: token
100+
with:
101+
app_id: ${{ secrets.GITGITGADGET_GITHUB_APP_ID }}
102+
private_key: ${{ secrets.GITGITGADGET_GITHUB_APP_PRIVATE_KEY }}
103+
repository: ${{ env.TARGET_REPOSITORY }}
104+
- name: set authorization header
105+
if: steps.check.outputs.refspec != ''
106+
uses: actions/github-script@v6
107+
id: auth
108+
with:
109+
script: |
110+
// Sadly, `git push` does not work with 'Authorization: Bearer <PAT>', therefore
111+
// we have to use the `Basic` variant
112+
const auth = Buffer.from('PAT:${{ steps.token.outputs.token }}').toString('base64')
113+
core.setSecret(auth)
114+
core.setOutput('header', `Authorization: Basic ${auth}`)
115+
- name: sync
116+
if: steps.check.outputs.refspec != ''
117+
shell: bash
118+
run: |
119+
set -ex
120+
git init --bare
121+
122+
git remote add source "${{ github.server_url }}/$SOURCE_REPOSITORY"
123+
# pretend to be a partial clone
124+
git config remote.source.promisor true
125+
git config remote.source.partialCloneFilter blob:none
126+
127+
# fetch some commits
128+
printf '%s' '${{ steps.check.outputs.to-fetch }}' |
129+
xargs -d ' ' -r git fetch --depth 10000 source
130+
rm -f .git/shallow
131+
132+
# push the commits
133+
printf '%s' '${{ steps.check.outputs.refspec }}' |
134+
xargs -d ' ' -r git -c http.extraHeader='${{ steps.auth.outputs.header }}' \
135+
push "${{ github.server_url }}/$TARGET_REPOSITORY"

0 commit comments

Comments
 (0)