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);
+});