Label sync #3
This file contains hidden or 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
| # Keeps the org's labels matching .github/labels.js. | |
| # | |
| # Structure follows apache/maven-gh-actions-shared/.github/workflows/labels-sync.yml | |
| # so the two orgs stay recognisably the same, with two differences: | |
| # | |
| # * the repository list comes from the GitHub API rather than gitbox, which | |
| # the ASF has and codehaus-plexus does not. Archived repositories are | |
| # filtered out, so retired components are never touched. | |
| # * the scheduled run is REPORT-ONLY and needs no credentials. Reading labels | |
| # from a public repository requires no permissions, so drift is detected on | |
| # the default GITHUB_TOKEN. Only reconciling needs a token. | |
| # | |
| # Reconciling uses an optional LABEL_SYNC_TOKEN secret - a fine-grained PAT | |
| # owned by the org with `Issues: Read and write`. Only an org owner can add it. | |
| # Until one exists the drift report still works, and anyone with push rights can | |
| # reconcile from a checkout by dispatching this workflow from their own fork or | |
| # by running the equivalent locally. | |
| name: Label sync | |
| on: | |
| schedule: | |
| - cron: '0 6 * * 1' | |
| workflow_dispatch: | |
| inputs: | |
| apply: | |
| description: 'Reconcile labels (needs LABEL_SYNC_TOKEN). Leave off to only report drift.' | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| repos: | |
| name: Prepare repositories list | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.repo.outputs.matrix }} | |
| steps: | |
| - id: repo | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| { | |
| echo 'matrix<<EOF' | |
| gh api 'orgs/codehaus-plexus/repos?per_page=100' --paginate \ | |
| --jq '.[] | select(.archived == false) | .name' \ | |
| | sort | jq -Rsc 'split("\n") | map(select(length > 0))' | |
| echo 'EOF' | |
| } >> "$GITHUB_OUTPUT" | |
| cat "$GITHUB_OUTPUT" | |
| label-sync: | |
| name: ${{ matrix.repo }} | |
| runs-on: ubuntu-latest | |
| needs: repos | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 10 | |
| matrix: | |
| repo: ${{ fromJSON(needs.repos.outputs.matrix) }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - name: Check the token when reconciling | |
| if: ${{ inputs.apply == true }} | |
| env: | |
| TOKEN: ${{ secrets.LABEL_SYNC_TOKEN }} | |
| run: | | |
| if [ -z "$TOKEN" ]; then | |
| echo "::error::Reconciling needs the LABEL_SYNC_TOKEN secret, which only an org owner can add." | |
| echo "::error::Re-run without 'apply' to get a drift report instead." | |
| exit 1 | |
| fi | |
| - name: Sync labels in ${{ matrix.repo }} | |
| uses: actions/github-script@v7 | |
| env: | |
| OWNER: codehaus-plexus | |
| REPO: ${{ matrix.repo }} | |
| APPLY: ${{ inputs.apply }} | |
| with: | |
| # Reading labels from a public repo needs no permissions, so the | |
| # default token is enough for the report-only path. | |
| github-token: ${{ secrets.LABEL_SYNC_TOKEN || github.token }} | |
| script: | | |
| const labels = require('./.github/labels.js'); | |
| const apply = process.env.APPLY === 'true'; | |
| const { OWNER: owner, REPO: repo } = process.env; | |
| const current = await github.paginate(github.rest.issues.listLabelsForRepo, { | |
| owner, repo, per_page: 100 | |
| }); | |
| const drift = []; | |
| for (const label of labels) { | |
| const existing = current.find(({ name }) => name === label.name); | |
| if (!existing) { | |
| drift.push(`missing: ${label.name}`); | |
| if (apply) { | |
| await github.rest.issues.createLabel({ owner, repo, ...label }); | |
| } | |
| } else { | |
| const what = []; | |
| if (existing.color.toLowerCase() !== label.color.toLowerCase()) { | |
| what.push(`colour #${existing.color} -> #${label.color}`); | |
| } | |
| if ((existing.description || '') !== label.description) { | |
| what.push('description'); | |
| } | |
| if (what.length) { | |
| drift.push(`${label.name}: ${what.join(', ')}`); | |
| if (apply) { | |
| await github.rest.issues.updateLabel({ owner, repo, ...label }); | |
| } | |
| } | |
| } | |
| } | |
| if (drift.length === 0) { | |
| core.info(`${repo}: up to date`); | |
| return; | |
| } | |
| drift.forEach(d => core.info(`${repo}: ${d}`)); | |
| core.summary.addHeading(repo, 3).addList(drift).write(); | |
| if (!apply) { | |
| core.setFailed(`${repo}: ${drift.length} label(s) drifted from .github/labels.js`); | |
| } |