sync-git-gui-branches #167
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: sync-git-gui-branches | |
on: | |
schedule: | |
- cron: '31 22 * * *' | |
workflow_dispatch: | |
env: | |
SOURCE_REPOSITORY: j6t/git-gui | |
TARGET_REPOSITORY: gitgitgadget/git | |
TARGET_REF_NAMESPACE: git-gui/ | |
# We want to limit queuing to a single workflow run i.e. if there is already | |
# an active workflow run and a queued one, queue another one canceling the | |
# already queued one. | |
concurrency: | |
group: ${{ github.workflow }} | |
jobs: | |
sync-git-gui-branches: | |
runs-on: ubuntu-latest | |
steps: | |
- name: check which refs need to be synchronized | |
uses: actions/github-script@v7 | |
id: check | |
with: | |
script: | | |
const sleep = async (milliseconds) => { | |
return new Promise(resolve => setTimeout(resolve, milliseconds)) | |
} | |
const getRefs = async (repository, stripRefsPrefix) => { | |
let attemptCounter = 1 | |
for (;;) { | |
try { | |
const [owner, repo] = repository.split('/') | |
let { data } = await github.rest.git.listMatchingRefs({ | |
owner, | |
repo, | |
ref: 'heads/' | |
}) | |
data = data.filter(e => { | |
if (!e.ref.startsWith('refs/heads/')) return false | |
e.name = e.ref.slice(11) | |
return true | |
}) | |
if (stripRefsPrefix) { | |
data = data.filter(e => { | |
if (!e.name.startsWith(stripRefsPrefix)) return false | |
e.name = e.name.slice(stripRefsPrefix.length) | |
return true | |
}) | |
} | |
return data | |
.sort((a, b) => a.ref.localeCompare(b.ref)) | |
} catch (e) { | |
if (e?.status !== 502) throw e | |
} | |
if (++attemptCounter > 10) throw new Error('Giving up listing refs after 10 attempts') | |
const seconds = attemptCounter * attemptCounter + 15 * Math.random() | |
core.info(`Encountered a Server Error; retrying in ${seconds} seconds`) | |
await sleep(1000 * seconds) | |
} | |
} | |
const sourceRefs = await getRefs(process.env.SOURCE_REPOSITORY) | |
const targetRefs = await getRefs(process.env.TARGET_REPOSITORY, process.env.TARGET_REF_NAMESPACE) | |
const targetPrefix = `refs/heads/${process.env.TARGET_REF_NAMESPACE}` | |
const refspecs = [] | |
const toFetch = new Set() | |
for (let i = 0, j = 0; i < sourceRefs.length || j < targetRefs.length; ) { | |
const compare = i >= sourceRefs.length | |
? +1 | |
: j >= targetRefs.length | |
? -1 | |
: sourceRefs[i].name.localeCompare(targetRefs[j].name) | |
if (compare > 0) { | |
// no source ref => delete target ref | |
refspecs.push(`:${targetPrefix}${targetRefs[j].name}`) | |
j++ | |
} else if (compare < 0) { | |
// no corresponding target ref yet => push source ref (new) | |
const sha = sourceRefs[i].object.sha | |
toFetch.add(sha) | |
refspecs.push(`${sha}:${targetPrefix}${sourceRefs[i].name}`) | |
i++ | |
} else { | |
// the sourceRef's name matches the targetRef's | |
if (sourceRefs[i].object.sha !== targetRefs[j].object.sha) { | |
// target ref needs updating | |
const sha = sourceRefs[i].object.sha | |
toFetch.add(sha) | |
refspecs.push(`+${sha}:${targetPrefix}${sourceRefs[i].name}`) | |
} | |
i++ | |
j++ | |
} | |
} | |
core.setOutput('refspec', refspecs.join(' ')) | |
targetRefs.forEach((e) => toFetch.delete(e.object.sha)) | |
core.setOutput('to-fetch', [...toFetch].join(' ')) | |
- name: obtain installation token | |
if: steps.check.outputs.refspec != '' | |
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 | |
id: token | |
with: | |
app_id: ${{ secrets.GITGITGADGET_GITHUB_APP_ID }} | |
private_key: ${{ secrets.GITGITGADGET_GITHUB_APP_PRIVATE_KEY }} | |
repository: ${{ env.TARGET_REPOSITORY }} | |
- name: set authorization header | |
if: steps.check.outputs.refspec != '' | |
uses: actions/github-script@v7 | |
id: auth | |
with: | |
script: | | |
// Sadly, `git push` does not work with 'Authorization: Bearer <PAT>', therefore | |
// we have to use the `Basic` variant | |
const auth = Buffer.from('PAT:${{ steps.token.outputs.token }}').toString('base64') | |
core.setSecret(auth) | |
core.setOutput('header', `Authorization: Basic ${auth}`) | |
- name: sync | |
if: steps.check.outputs.refspec != '' | |
shell: bash | |
run: | | |
set -ex | |
git init --bare | |
git remote add source "${{ github.server_url }}/$SOURCE_REPOSITORY" | |
# pretend to be a partial clone | |
git config remote.source.promisor true | |
git config remote.source.partialCloneFilter blob:none | |
# fetch some commits | |
printf '%s' '${{ steps.check.outputs.to-fetch }}' | | |
xargs -d ' ' -r git fetch --depth 10000 source | |
rm -f .git/shallow | |
# push the commits | |
printf '%s' '${{ steps.check.outputs.refspec }}' | | |
xargs -d ' ' -r git -c http.extraHeader='${{ steps.auth.outputs.header }}' \ | |
push "${{ github.server_url }}/$TARGET_REPOSITORY" |