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 <david@hey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+60
-61
No files matched your search
@@ -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:
|
||||
"🔴 <strong>Rebuild trigger sync failed</strong><br><a href=\"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\">View run</a>" \
|
||||
'{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 });
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user