From 37288aada4e2b1b6b3a32550190b65ffd712ae11 Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Thu, 8 Oct 2026 11:08:41 -0400 Subject: [PATCH] Auto-merge rebuild PRs once their builds pass (#862) sync-rebuilds now runs on the unattended lane like track-branches: it opens the pkgrel bump PR with PKGS_BOT_TOKEN and enables auto-merge, so a Qt (or any rebuild_on) update reaches users without a maintainer merge. Branch protection still requires result, self-tests and build-isolation to pass; a failed rebuild stays an open red PR. The PAT is required because a GITHUB_TOKEN merge does not start publish.yml. PAT pushes are not held for approval, so the approve job, the build-approved label and the review request go away. Co-authored-by: David Heinemeier Hansson Co-authored-by: Claude Opus 5.5 --- .github/workflows/sync-rebuilds.yml | 91 ++++++++++++----------------- README.md | 14 ++--- tests/pr-workflow-approval.cjs | 16 ++++- 3 files changed, 60 insertions(+), 61 deletions(-) diff --git a/.github/workflows/sync-rebuilds.yml b/.github/workflows/sync-rebuilds.yml index 4852140..7a9d9c0 100644 --- a/.github/workflows/sync-rebuilds.yml +++ b/.github/workflows/sync-rebuilds.yml @@ -1,5 +1,14 @@ name: Sync Rebuild Triggers +# Rebuilds ride the unattended lane, like track-branches.yml: the pkgrel bump +# PR builds on the droplets, auto-merge lands it once `result`, `self-tests` +# and `build-isolation` are green, and the merge publishes. A rebuild that +# fails stays an unmerged red PR for a maintainer. +# +# The PR and auto-merge use the PAT in PKGS_BOT_TOKEN, because a merge made +# with the built-in GITHUB_TOKEN does not start publish.yml, and its pushes +# are held for approval instead of building. + on: schedule: # Every 6 hours, off the hour to dodge the scheduling backlog at :00 @@ -17,14 +26,17 @@ jobs: permissions: contents: write pull-requests: write - outputs: - branch: ${{ steps.branch.outputs.branch }} - pushed_at: ${{ steps.pushed.outputs.at }} - number: ${{ steps.cpr.outputs.pull-request-number }} - operation: ${{ steps.cpr.outputs.pull-request-operation }} - head_sha: ${{ steps.cpr.outputs.pull-request-head-sha }} steps: + - name: Require the bot token + env: + PKGS_BOT_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }} + run: | + if [[ -z "$PKGS_BOT_TOKEN" ]]; then + echo "::error::Set PKGS_BOT_TOKEN to a PAT with Contents: write and Pull requests: write on this repository, owned by an account trusted to trigger builds." + exit 1 + fi + - name: Checkout repository uses: actions/checkout@v4 with: @@ -85,19 +97,12 @@ jobs: echo "has_changes=true" >> "$GITHUB_OUTPUT" fi - # Runs created by this push are newer than this; the approve job - # waits for them. A minute's slack absorbs runner clock skew. - - name: Record push time - if: steps.changes.outputs.has_changes == 'true' - id: pushed - run: echo "at=$(date -u -d '-1 minute' +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" - - name: Create Pull Request if: steps.changes.outputs.has_changes == 'true' id: cpr uses: peter-evans/create-pull-request@v7 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.PKGS_BOT_TOKEN }} commit-message: 'chore: rebuild against updated dependencies' title: "chore: rebuild against updated dependencies${{ steps.branch.outputs.scope && format(' ({0})', steps.branch.outputs.scope) || '' }}" body: | @@ -109,14 +114,27 @@ jobs: bump is what makes the rebuilt package an upgrade pacman will offer; without it the build produces the version already published and no one receives it. + + This PR auto-merges once the build checks pass. A failing rebuild + leaves it open for a maintainer. branch: ${{ steps.branch.outputs.branch }} delete-branch: true - # The bot is trusted; build-approved lets the approve job below - # release GitHub's hold on its pushes without a maintainer. - labels: | - automated - build-approved - reviewers: ryanrhughes + labels: automated + + # Auto-merge, not a direct merge: branch protection still has to see + # the build checks green before the rebuild lands. + - name: Enable auto-merge + if: steps.cpr.outputs.pull-request-number != '' + env: + GH_TOKEN: ${{ secrets.PKGS_BOT_TOKEN }} + PR: ${{ steps.cpr.outputs.pull-request-number }} + run: | + # Idempotent across re-runs of an updated PR: enabling twice errors. + if [[ "$(gh pr view "$PR" -R "${{ github.repository }}" --json autoMergeRequest --jq '.autoMergeRequest != null')" == true ]]; then + echo "auto-merge already enabled on #$PR" + exit 0 + fi + gh pr merge --auto --merge "$PR" -R "${{ github.repository }}" - name: Notify Basecamp on failure if: failure() && env.BASECAMP_CHATBOT_URL != '' @@ -129,36 +147,3 @@ jobs: "🔴 Rebuild trigger sync failed
View run" \ '{content: $content}')" \ "$BASECAMP_CHATBOT_URL" - - # GitHub holds pull_request runs from a GITHUB_TOKEN push for approval and - # creates no pull_request_target run for it, so approve-pr.yml never sees - # the sync's own pushes. The sync labels its PR build-approved, so release - # the held runs for the commit just pushed, whether it opened the PR or - # updated it. A separate job, so the sync container's token never holds - # actions: write. - approve: - needs: sync - if: ${{ !cancelled() && (needs.sync.outputs.operation == 'created' || needs.sync.outputs.operation == 'updated') }} - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: read - pull-requests: read - actions: write - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Release held build and test runs - uses: actions/github-script@v7 - env: - NUMBER: ${{ needs.sync.outputs.number }} - BRANCH: ${{ needs.sync.outputs.branch }} - HEAD_SHA: ${{ needs.sync.outputs.head_sha }} - SINCE: ${{ needs.sync.outputs.pushed_at }} - with: - script: | - const approve = require('./.github/scripts/approve-sync-push.cjs'); - const { NUMBER, BRANCH, HEAD_SHA, SINCE } = process.env; - await approve({ github, context, core, number: Number(NUMBER), - branch: BRANCH, headSha: HEAD_SHA, since: SINCE }); diff --git a/README.md b/README.md index 02cd002..0a10536 100644 --- a/README.md +++ b/README.md @@ -893,15 +893,15 @@ The repository includes GitHub workflows and systemd services for automated rele #### GitHub Workflows 1. **sync-upstream.yml** (Every 6 hours): Watches direct upstream feeds and updates owned recipes on the reviewed lane. Successful package updates reach a PR even if another feed fails; failed recipes stay untouched and the workflow remains red. -2. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories and opens a PR. +2. **sync-rebuilds.yml** (Every 6 hours): Bumps pkgrel for packages whose `rebuild_on` dependencies have moved in the official repositories, opens a PR, and enables auto-merge. The PR lands once its build checks pass; a rebuild that fails stays an open red PR for a maintainer. 3. **track-branches.yml** (Every 2 hours): The unattended lane. Pins every `"auto_merge": true` package to the tip of its watched branch once its commit timestamp clears `min_release_age`, opens one PR for all of them, and enables auto-merge. Packages pinned from the same branch move together or not at all, including targeted syncs. The PR builds like any other; a tip that fails to build stays an open red PR until the next tick supersedes it. -The tracking PR and auto-merge use the PAT stored in `PKGS_BOT_TOKEN`, with +The tracking and rebuild PRs and their auto-merge use the PAT stored in `PKGS_BOT_TOKEN`, with Contents and Pull requests write access to this repository and an owner trusted to trigger builds. The existing controller PAT can be reused. No GitHub App is required. The built-in Actions `GITHUB_TOKEN` cannot drive the unattended -build-and-publish chain, so the tracker requires this secret before it runs. -The reviewed sync workflows continue to use `GITHUB_TOKEN` and require +build-and-publish chain, so both workflows require this secret before they run. +The reviewed upstream sync continues to use `GITHUB_TOKEN` and requires maintainer approval as before. See [setup instructions](docs/upstream-sources.md#enable-unattended-branch-updates). Scheduled runs regenerate one shared PR (`auto/sync-upstream`, `auto/sync-rebuilds`) @@ -912,10 +912,10 @@ pending updates. The next scheduled run still picks the same update up in the shared PR if it has not merged by then; identical package trees reuse the same build artifacts. -Sync PRs are pushed with `GITHUB_TOKEN`, so GitHub holds their build and test +Upstream sync PRs are pushed with `GITHUB_TOKEN`, so GitHub holds their build and test runs for approval on every push and starts no `pull_request_target` workflow -for them. The sync workflows label their own PRs **`build-approved`**, and -their `approve` job releases the held runs for each commit they push, including +for them. The upstream sync labels its own PRs **`build-approved`**, and +its `approve` job releases the held runs for each commit they push, including the push that opens the PR. A push to an `auto/sync-*` branch does not cancel the PR's in-flight build: the new build waits for it and then reuses its artifacts, so a long aarch64 build is not restarted by every sync. diff --git a/tests/pr-workflow-approval.cjs b/tests/pr-workflow-approval.cjs index d4dcd9d..6202c64 100644 --- a/tests/pr-workflow-approval.cjs +++ b/tests/pr-workflow-approval.cjs @@ -435,7 +435,7 @@ test('scoped branch names reject anything that is not a package name', () => { }); test('sync workflows push scoped runs aside and keep actions: write out of the sync job', () => { - for (const file of ['sync-upstream.yml', 'sync-rebuilds.yml']) { + for (const file of ['sync-upstream.yml']) { const text = readFileSync(join(__dirname, '../.github/workflows', file), 'utf8'); const sync = text.slice(text.indexOf('\n sync:\n'), text.indexOf('\n approve:\n')); const approveJob = text.slice(text.indexOf('\n approve:\n')); @@ -450,3 +450,17 @@ test('sync workflows push scoped runs aside and keep actions: write out of the s assert.match(approveJob, /needs\.sync\.outputs\.operation == 'updated'/, file); } }); + +test('rebuild sync opens its PR with the bot token and auto-merges it', () => { + const file = 'sync-rebuilds.yml'; + const text = readFileSync(join(__dirname, '../.github/workflows', file), 'utf8'); + assert.match(text, /sync-pr-branch\.sh auto\/sync-rebuilds "\$\{package_args\[@\]\}"/, file); + assert.match(text, /branch: \$\{\{ steps\.branch\.outputs\.branch \}\}/, file); + // A GITHUB_TOKEN merge would not start publish.yml, so the PR and the + // merge both go through the PAT. + assert.match(text, /token: \$\{\{ secrets\.PKGS_BOT_TOKEN \}\}/, file); + assert.doesNotMatch(text, /secrets\.GITHUB_TOKEN/, file); + assert.match(text, /gh pr merge --auto --merge "\$PR"/, file); + assert.doesNotMatch(text, /^ +actions: write$/m, file); + assert.doesNotMatch(text, /\n approve:\n/, file); +});