From 87a4de25f4a9dc0ec572821486d5d1ee1707cebe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 01:32:29 +0000 Subject: [PATCH 01/12] Update myoung34/github-runner Docker tag to v2.336.0 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8d5a073..c448808 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG VERSION=2.334.0-ubuntu-noble +ARG VERSION=2.336.0-ubuntu-noble ARG JAVA_VERSION=21 ARG COMPILE_SDK=36.1 ARG BUILD_TOOLS=37.0.0 From d6703748b74fd4d7d6488a96331a8c3b8968e8cb Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 4 Aug 2026 19:48:29 -0700 Subject: [PATCH 02/12] Add design spec for PR build testing + auto-merge Co-Authored-By: Claude Fable 5 --- .../2026-08-04-pr-build-automerge-design.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md diff --git a/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md b/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md new file mode 100644 index 0000000..e6cf9f1 --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md @@ -0,0 +1,90 @@ +# PR Build Testing + Auto-Merge for Dependency PRs + +**Date:** 2026-08-04 +**Status:** Approved + +## Problem + +Dependency PRs (Renovate base-image bumps and the weekly SDK/JDK update PRs) +receive no CI. The container is only built after merge, on push to `main` +(deploy.yml), so a broken update is discovered post-merge. Renovate is already +configured with `automerge: true` / `platformAutomerge: true`, but with no +required status checks on `main`, GitHub auto-merge is unavailable and every PR +is merged manually. + +## Goal + +Every PR gets a full test build of the container (all JDK variants, no push). +When the build passes, dependency PRs from both bots (renovate[bot] and the +SDK/JDK update workflow) merge automatically. A failed build leaves the PR open +and unmerged, and nothing deploys. + +## Design + +### 1. New workflow: `.github/workflows/build.yml` + +- **Trigger:** `pull_request` (all PRs, no path filter — a required check must + report on every PR or merges block forever). +- **Structure:** mirrors deploy.yml: + - `matrix` job: generates the JDK matrix from `matrix.json` (same jq logic). + - `build` job: per-JDK; extracts `VERSION` / `COMPILE_SDK` from the + Dockerfile, sets up Buildx, runs `docker/build-push-action` with + `push: false`, same `build-args` as deploy. No registry logins. + - `fail-fast: false` so every broken JDK variant is visible. +- **Concurrency:** group keyed on the PR ref with `cancel-in-progress: true` + so superseded runs are cancelled. +- **Gate job:** a final job named **"All builds passed"** with `if: always()` + and `needs: [matrix, build]` that fails unless every needed job succeeded. + Matrix check names ("Build JDK 17", …) change as matrix.json evolves; the + gate gives the ruleset one stable check name. + +### 2. Branch ruleset on `main` + +- Created via `gh api` (POST `/repos/{owner}/{repo}/rulesets`). +- Targets `main`; single rule: required status check **"All builds passed"**. +- Bypass: repository admin (so the owner can push to main in an emergency). +- Effect: GitHub auto-merge becomes available, so Renovate's existing + `platformAutomerge: true` works with no config change. Renovate merges still + trigger deploy.yml because renovate[bot] is a GitHub App, not the repo's + `GITHUB_TOKEN`. + +### 3. check-sdk-updates.yml changes (requires a PAT) + +PRs created and pushes made with the default `GITHUB_TOKEN` never trigger other +workflows, so the build would never run on the SDK/JDK PRs and auto-merge would +hang; a GITHUB_TOKEN-enabled merge also wouldn't trigger deploy.yml. + +- The owner creates a fine-grained PAT scoped to this repo with + **contents: write** and **pull-requests: write**, saved as repo secret + `AUTOMERGE_PAT`. +- The workflow uses `AUTOMERGE_PAT` for checkout/push and `gh` PR operations. +- After `gh pr create`, add `gh pr merge --auto --merge` so the PR merges once + the required check passes. +- If the secret is missing, the workflow should fail loudly rather than + silently creating a PR that can never auto-merge. + +### 4. Unchanged + +- deploy.yml (still builds and pushes on merge to main). +- renovate.json (automerge settings already correct). + +## Error handling + +- Any JDK build failure → gate check fails → PR stays open, unmerged; no + deploy. Owner investigates manually. +- Gate job treats cancelled/skipped needed jobs as failure (`if: always()` + + explicit result check) so a cancelled run can never satisfy the ruleset. + +## Testing / verification + +- The implementation PR itself triggers build.yml, self-validating the + workflow before merge (merged manually one last time). +- After merge: run check-sdk-updates via `workflow_dispatch` (or wait for the + next Renovate PR) and confirm the build runs, the PR auto-merges, and + deploy.yml fires on the merge. + +## Out of scope + +- Docker layer caching for PR builds (repo GHA cache is far smaller than the + Android SDK layers; revisit only if build times become a problem). +- Refactoring deploy.yml/build.yml to share the matrix job. From 946a0e2595754da77f3a4791342c44867d39183f Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 5 Aug 2026 08:55:45 -0700 Subject: [PATCH 03/12] Add implementation plan for PR build testing + auto-merge Co-Authored-By: Claude Fable 5 --- .../plans/2026-08-05-pr-build-automerge.md | 365 ++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-05-pr-build-automerge.md diff --git a/docs/superpowers/plans/2026-08-05-pr-build-automerge.md b/docs/superpowers/plans/2026-08-05-pr-build-automerge.md new file mode 100644 index 0000000..9274f54 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-pr-build-automerge.md @@ -0,0 +1,365 @@ +# PR Build Testing + Auto-Merge Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Every PR gets a full test build of the container (all JDK variants, no push); dependency PRs from renovate[bot] and the weekly SDK/JDK workflow auto-merge when the build passes. + +**Architecture:** A new `build.yml` workflow mirrors deploy.yml's matrix but builds with `push: false`, ending in a single stable gate check named "All builds passed". A branch ruleset on `main` requires that check, which activates Renovate's existing `platformAutomerge`. The SDK/JDK update workflow switches to a fine-grained PAT (so its PRs trigger workflows) and enables GitHub auto-merge on its PRs. + +**Tech Stack:** GitHub Actions, docker/build-push-action, gh CLI, GitHub branch rulesets API. + +**Spec:** `docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md` + +## Global Constraints + +- Action versions must match deploy.yml exactly: `actions/checkout@v6`, `docker/setup-buildx-action@v4`, `docker/build-push-action@v7`. +- The gate job's `name:` must be exactly `All builds passed` — the ruleset matches on this string. +- The PAT secret name is exactly `AUTOMERGE_PAT`. +- deploy.yml and renovate.json must not be modified. +- Work happens on branch `feat/pr-build-automerge`. NEVER push to main; the user merges the PR themselves. +- Repo: `compscidr/docker-github-actions-runner-android` (verify with `gh repo view`). + +--- + +### Task 1: Create the PR build workflow + +**Files:** +- Create: `.github/workflows/build.yml` + +**Interfaces:** +- Consumes: `matrix.json` (keys `java_versions`, `default_jdk`), Dockerfile ARGs `VERSION`, `JAVA_VERSION`. +- Produces: a check run named `All builds passed` on every PR head SHA (Task 4's ruleset and Task 2's auto-merge depend on this exact name). + +- [ ] **Step 1: Write the workflow file** + +Create `.github/workflows/build.yml` with exactly this content: + +```yaml +name: Build container (PR) +on: + pull_request: + +concurrency: + group: build-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + matrix: + name: Generate build matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate.outputs.matrix }} + steps: + - uses: actions/checkout@v6 + + - name: Generate matrix from matrix.json + id: generate + run: | + DEFAULT_JDK=$(jq -r '.default_jdk' matrix.json) + MATRIX=$(jq -c --arg default "$DEFAULT_JDK" '{ + "include": [.java_versions[] | { + "java_version": ., + "is_default_jdk": (. == $default) + }] + }' matrix.json) + echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT + + build: + name: Build JDK ${{ matrix.java_version }} + needs: matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + + - name: Extract versions from Dockerfile + id: versions + run: | + set -euo pipefail + RUNNER_TAG=$(grep -m1 '^ARG VERSION=' Dockerfile | cut -d= -f2) + + if [ -z "$RUNNER_TAG" ]; then + echo "::error::Failed to extract versions from Dockerfile" + exit 1 + fi + + echo "runner_tag=${RUNNER_TAG}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build (no push) + uses: docker/build-push-action@v7 + with: + context: . + push: false + build-args: | + VERSION=${{ steps.versions.outputs.runner_tag }} + JAVA_VERSION=${{ matrix.java_version }} + + gate: + name: All builds passed + needs: [matrix, build] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check that all required jobs succeeded + run: | + if [ "${{ needs.matrix.result }}" != "success" ] || [ "${{ needs.build.result }}" != "success" ]; then + echo "::error::A required job failed or was cancelled (matrix: ${{ needs.matrix.result }}, build: ${{ needs.build.result }})" + exit 1 + fi + echo "All builds succeeded" +``` + +Notes on intent (do not deviate): +- No registry logins, no metadata action, no tags — this only proves the image builds. +- `if: always()` on the gate plus explicit result checks means cancelled/skipped builds fail the gate; a cancelled run can never satisfy the ruleset. +- Concurrency key uses the PR number so a force-push cancels the superseded run. + +- [ ] **Step 2: Validate the workflow syntax** + +Run: +```bash +docker run --rm -v /home/jason/dev/docker-github-actions-runner-android:/repo -w /repo rhysd/actionlint:latest -color +``` +Expected: no output for build.yml (exit 0). If docker/actionlint is unavailable, fall back to: +```bash +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/build.yml'))" && echo OK +``` +Expected: `OK`. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/build.yml +git commit -m "Add PR build workflow with 'All builds passed' gate check + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 2: Switch check-sdk-updates.yml to the PAT and enable auto-merge + +**Files:** +- Modify: `.github/workflows/check-sdk-updates.yml` (checkout step ~line 17, "Create pull request" step ~lines 177-238) + +**Interfaces:** +- Consumes: repo secret `AUTOMERGE_PAT` (fine-grained PAT, contents: write + pull-requests: write; created by the user in Task 5). +- Produces: SDK/JDK PRs whose creation triggers build.yml and which have GitHub auto-merge enabled. + +- [ ] **Step 1: Add a fail-loudly guard as the first step of the job** + +In `.github/workflows/check-sdk-updates.yml`, insert this as the FIRST step of the `check-updates` job, BEFORE `- uses: actions/checkout@v6` (before checkout so a missing secret fails with this clear message instead of a confusing checkout auth error): + +```yaml + - name: Verify AUTOMERGE_PAT is configured + env: + AUTOMERGE_PAT: ${{ secrets.AUTOMERGE_PAT }} + run: | + if [ -z "$AUTOMERGE_PAT" ]; then + echo "::error::AUTOMERGE_PAT secret is not configured. Create a fine-grained PAT scoped to this repo with contents:write and pull-requests:write, and save it as a repository secret named AUTOMERGE_PAT. PRs created with the default GITHUB_TOKEN never trigger the build workflow, so they could never auto-merge." + exit 1 + fi +``` + +- [ ] **Step 2: Make checkout (and therefore git push) use the PAT** + +Change: +```yaml + - uses: actions/checkout@v6 +``` +to: +```yaml + - uses: actions/checkout@v6 + with: + token: ${{ secrets.AUTOMERGE_PAT }} +``` +(Only the checkout step inside check-sdk-updates.yml — no other workflow.) + +- [ ] **Step 3: Make gh use the PAT and enable auto-merge** + +In the "Create pull request" step, change the env from: +```yaml + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` +to: +```yaml + env: + GH_TOKEN: ${{ secrets.AUTOMERGE_PAT }} +``` + +Then, at the end of that step's `run:` block (after the `if [ -z "$EXISTING" ] ... fi` create-or-update logic), append: + +```bash + # Enable auto-merge so the PR merges once "All builds passed" succeeds. + # Runs for both new and force-updated existing PRs; idempotent. + gh pr merge --auto --merge "$BRANCH" +``` + +- [ ] **Step 4: Validate the workflow syntax** + +Run: +```bash +docker run --rm -v /home/jason/dev/docker-github-actions-runner-android:/repo -w /repo rhysd/actionlint:latest -color +``` +Expected: exit 0, no findings for check-sdk-updates.yml. Fallback if unavailable: +```bash +python3 -c "import yaml; yaml.safe_load(open('.github/workflows/check-sdk-updates.yml'))" && echo OK +``` +Expected: `OK`. + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/check-sdk-updates.yml +git commit -m "Use AUTOMERGE_PAT in SDK update workflow and enable auto-merge + +Co-Authored-By: Claude Fable 5 " +``` + +--- + +### Task 3: Push branch, open PR, verify the build check reports + +**Files:** none (git/GitHub operations only) + +**Interfaces:** +- Consumes: branch `feat/pr-build-automerge` with Tasks 1-2 committed (plus the already-committed spec and this plan). +- Produces: an open PR whose head SHA has a green `All builds passed` check (Task 4 must not run until this is verified). + +- [ ] **Step 1: Verify branch and push** + +```bash +git branch --show-current +``` +Expected: `feat/pr-build-automerge`. Then: +```bash +git push -u origin feat/pr-build-automerge +``` + +- [ ] **Step 2: Open the PR** + +```bash +gh pr create --title "Add PR build testing and auto-merge for dependency PRs" --body "## Summary +- New \`build.yml\` workflow: builds all JDK variants from matrix.json on every PR (\`push: false\`), gated by a single stable check named **All builds passed** +- \`check-sdk-updates.yml\` now uses the \`AUTOMERGE_PAT\` secret (default \`GITHUB_TOKEN\` PRs never trigger workflows) and enables auto-merge on its PRs +- Design: \`docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md\` + +## Follow-up (manual, after this merges) +- A branch ruleset on \`main\` requires **All builds passed** (created via API as part of this work) +- @compscidr must create a fine-grained PAT (this repo only; contents: write, pull-requests: write) and save it as repo secret \`AUTOMERGE_PAT\` + +🤖 Generated with [Claude Code](https://claude.com/claude-code)" +``` + +- [ ] **Step 3: Confirm build.yml triggered on the PR** + +```bash +gh pr checks feat/pr-build-automerge +``` +Expected: pending/running checks including `Build JDK 17` ... `Build JDK 26` and `All builds passed`. If no checks appear within ~2 minutes, investigate (`gh run list --workflow=build.yml`) before proceeding. + +- [ ] **Step 4: Wait for the full matrix to finish and the gate to pass** + +Poll until done (builds are heavyweight — expect 30-60+ minutes; poll every ~5 minutes, do not busy-wait): +```bash +gh pr checks feat/pr-build-automerge +``` +Expected final state: `All builds passed` = pass. If any JDK build fails, STOP: debug the workflow (this is exactly what the gate exists to catch), fix, commit, push, and re-verify. Do not proceed to Task 4 with a red gate. + +--- + +### Task 4: Create the branch ruleset on main + +**Files:** none (GitHub API operation) + +**Interfaces:** +- Consumes: a green `All builds passed` check on the open PR (Task 3). +- Produces: active branch ruleset on `main` requiring status check `All builds passed`, with repository-admin bypass. + +- [ ] **Step 1: Create the ruleset** + +`actor_id: 5` is the built-in Repository Admin role; `integration_id: 15368` is the GitHub Actions app, ensuring only Actions can satisfy the check. `strict_required_status_checks_policy: false` deliberately does NOT require branches to be up to date with main — strict mode would stall auto-merge behind constant rebases. + +```bash +gh api repos/compscidr/docker-github-actions-runner-android/rulesets -X POST --input - <<'EOF' +{ + "name": "main-requires-build", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } + }, + "bypass_actors": [ + { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "always" } + ], + "rules": [ + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { "context": "All builds passed", "integration_id": 15368 } + ] + } + } + ] +} +EOF +``` +Expected: JSON response containing `"enforcement": "active"` and an `id`. + +- [ ] **Step 2: Verify the ruleset applies to main** + +```bash +gh api repos/compscidr/docker-github-actions-runner-android/rules/branches/main +``` +Expected: array containing a `required_status_checks` rule whose contexts include `All builds passed`. + +- [ ] **Step 3: Verify the open PR is still mergeable via auto-merge path** + +```bash +gh pr view feat/pr-build-automerge --json mergeStateStatus,statusCheckRollup --jq '{state: .mergeStateStatus, gate: [.statusCheckRollup[] | select(.name == "All builds passed") | .conclusion]}' +``` +Expected: gate contains `SUCCESS`; state is `CLEAN` (or `UNSTABLE`/`BLOCKED` only if unrelated checks are still running — the green gate is what matters). + +--- + +### Task 5: User actions and end-to-end verification + +**Files:** none + +**Interfaces:** +- Consumes: merged PR from Task 3, ruleset from Task 4. +- Produces: verified hands-off dependency-update pipeline. + +- [ ] **Step 1: Ask the user to create the PAT and secret (blocked on user)** + +Tell the user exactly this — the executor cannot do it: +1. Create a fine-grained PAT at https://github.com/settings/personal-access-tokens/new — Resource owner: compscidr; Repository access: only `docker-github-actions-runner-android`; Permissions: Contents = Read and write, Pull requests = Read and write. Set a long expiry and note the renewal date. +2. Add it as a repository secret: `gh secret set AUTOMERGE_PAT` (paste the token when prompted), or via repo Settings → Secrets and variables → Actions. + +- [ ] **Step 2: Ask the user to merge the PR** + +The user merges `feat/pr-build-automerge` themselves (per their git rules). This is the last manual dependency-PR-era merge. + +- [ ] **Step 3: End-to-end test of the SDK/JDK path (after Steps 1-2)** + +```bash +gh workflow run check-sdk-updates.yml +``` +Then watch: +```bash +gh run list --workflow=check-sdk-updates.yml --limit 1 +``` +- If no updates exist upstream, the run succeeds without creating a PR — the PAT guard passing is still a valid partial test. +- If a PR is created: confirm build.yml triggers on it (`gh pr checks automated/sdk-jdk-updates`), auto-merge is enabled (PR page shows "Auto-merge enabled"), and after the gate passes the PR merges and deploy.yml fires on main (`gh run list --workflow=deploy.yml --limit 1`). + +- [ ] **Step 4: Confirm the Renovate path** + +Nothing to configure (renovate.json already has `automerge: true` + `platformAutomerge: true`). On the next Renovate PR, confirm: build.yml runs, Renovate enables auto-merge, the PR merges on green, and deploy.yml fires. If Renovate does not enable auto-merge, check the Renovate dashboard issue for errors before changing any config. From 84f9d0627bd46b56d9f71250a1f1fafc03d36caf Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 5 Aug 2026 09:55:35 -0700 Subject: [PATCH 04/12] Add PR build workflow with 'All builds passed' gate check Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yml | 77 +++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bc6be99 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,77 @@ +name: Build container (PR) +on: + pull_request: + +concurrency: + group: build-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + matrix: + name: Generate build matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.generate.outputs.matrix }} + steps: + - uses: actions/checkout@v6 + + - name: Generate matrix from matrix.json + id: generate + run: | + DEFAULT_JDK=$(jq -r '.default_jdk' matrix.json) + MATRIX=$(jq -c --arg default "$DEFAULT_JDK" '{ + "include": [.java_versions[] | { + "java_version": ., + "is_default_jdk": (. == $default) + }] + }' matrix.json) + echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT + + build: + name: Build JDK ${{ matrix.java_version }} + needs: matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + + - name: Extract versions from Dockerfile + id: versions + run: | + set -euo pipefail + RUNNER_TAG=$(grep -m1 '^ARG VERSION=' Dockerfile | cut -d= -f2) + + if [ -z "$RUNNER_TAG" ]; then + echo "::error::Failed to extract versions from Dockerfile" + exit 1 + fi + + echo "runner_tag=${RUNNER_TAG}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build (no push) + uses: docker/build-push-action@v7 + with: + context: . + push: false + build-args: | + VERSION=${{ steps.versions.outputs.runner_tag }} + JAVA_VERSION=${{ matrix.java_version }} + + gate: + name: All builds passed + needs: [matrix, build] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check that all required jobs succeeded + run: | + if [ "${{ needs.matrix.result }}" != "success" ] || [ "${{ needs.build.result }}" != "success" ]; then + echo "::error::A required job failed or was cancelled (matrix: ${{ needs.matrix.result }}, build: ${{ needs.build.result }})" + exit 1 + fi + echo "All builds succeeded" From 7397bfe380ee0249f8775edb123851a7368f79cb Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 5 Aug 2026 10:48:48 -0700 Subject: [PATCH 05/12] Use AUTOMERGE_PAT in SDK update workflow and enable auto-merge Co-Authored-By: Claude Fable 5 --- .github/workflows/check-sdk-updates.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-sdk-updates.yml b/.github/workflows/check-sdk-updates.yml index 78f5694..60bc09b 100644 --- a/.github/workflows/check-sdk-updates.yml +++ b/.github/workflows/check-sdk-updates.yml @@ -14,7 +14,18 @@ jobs: name: Check for updates runs-on: ubuntu-latest steps: + - name: Verify AUTOMERGE_PAT is configured + env: + AUTOMERGE_PAT: ${{ secrets.AUTOMERGE_PAT }} + run: | + if [ -z "$AUTOMERGE_PAT" ]; then + echo "::error::AUTOMERGE_PAT secret is not configured. Create a fine-grained PAT scoped to this repo with contents:write and pull-requests:write, and save it as a repository secret named AUTOMERGE_PAT. PRs created with the default GITHUB_TOKEN never trigger the build workflow, so they could never auto-merge." + exit 1 + fi + - uses: actions/checkout@v6 + with: + token: ${{ secrets.AUTOMERGE_PAT }} - name: Check for new JDK versions id: jdk @@ -177,7 +188,7 @@ jobs: - name: Create pull request if: steps.jdk.outputs.has_new == 'true' || steps.sdk.outputs.updated == 'true' env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.AUTOMERGE_PAT }} run: | BRANCH="automated/sdk-jdk-updates" @@ -236,3 +247,7 @@ jobs: else echo "PR #${EXISTING} already exists, updated with force push" fi + + # Enable auto-merge so the PR merges once "All builds passed" succeeds. + # Runs for both new and force-updated existing PRs; idempotent. + gh pr merge --auto --merge "$BRANCH" From 38d4b1d70f50613fc56d958ac3a6f10c68c71f2e Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 5 Aug 2026 11:27:09 -0700 Subject: [PATCH 06/12] Clarify in spec why build.yml only extracts VERSION Co-Authored-By: Claude Fable 5 --- .../specs/2026-08-04-pr-build-automerge-design.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md b/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md index e6cf9f1..c21a0dc 100644 --- a/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md +++ b/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md @@ -27,9 +27,13 @@ and unmerged, and nothing deploys. report on every PR or merges block forever). - **Structure:** mirrors deploy.yml: - `matrix` job: generates the JDK matrix from `matrix.json` (same jq logic). - - `build` job: per-JDK; extracts `VERSION` / `COMPILE_SDK` from the - Dockerfile, sets up Buildx, runs `docker/build-push-action` with - `push: false`, same `build-args` as deploy. No registry logins. + - `build` job: per-JDK; extracts `VERSION` from the Dockerfile, sets up + Buildx, runs `docker/build-push-action` with `push: false`, same + `build-args` as deploy. No registry logins. (`COMPILE_SDK` is not + extracted: deploy.yml only needs it to compose image tags, and the PR + build produces no tags — the Docker build itself consumes `COMPILE_SDK` + from the Dockerfile's ARG default, so a broken value still fails the + build.) - `fail-fast: false` so every broken JDK variant is visible. - **Concurrency:** group keyed on the PR ref with `cancel-in-progress: true` so superseded runs are cancelled. From 73c7042b55f1975c15e3e63f5ff4af80ace6d667 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 5 Aug 2026 11:44:41 -0700 Subject: [PATCH 07/12] Harden build.yml with read-only permissions; fix spec concurrency wording Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yml | 3 +++ docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bc6be99..01122c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,9 @@ name: Build container (PR) on: pull_request: +permissions: + contents: read + concurrency: group: build-pr-${{ github.event.pull_request.number }} cancel-in-progress: true diff --git a/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md b/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md index c21a0dc..8990f49 100644 --- a/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md +++ b/docs/superpowers/specs/2026-08-04-pr-build-automerge-design.md @@ -35,7 +35,7 @@ and unmerged, and nothing deploys. from the Dockerfile's ARG default, so a broken value still fails the build.) - `fail-fast: false` so every broken JDK variant is visible. -- **Concurrency:** group keyed on the PR ref with `cancel-in-progress: true` +- **Concurrency:** group keyed on the PR number with `cancel-in-progress: true` so superseded runs are cancelled. - **Gate job:** a final job named **"All builds passed"** with `if: always()` and `needs: [matrix, build]` that fails unless every needed job succeeded. From 1a364c04f4b8da880f8911a31c87537ad4ceb878 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Wed, 5 Aug 2026 11:47:19 -0700 Subject: [PATCH 08/12] Shrink SDK workflow token permissions; guard against expired PAT Co-Authored-By: Claude Fable 5 --- .github/workflows/check-sdk-updates.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-sdk-updates.yml b/.github/workflows/check-sdk-updates.yml index 60bc09b..b886c64 100644 --- a/.github/workflows/check-sdk-updates.yml +++ b/.github/workflows/check-sdk-updates.yml @@ -6,8 +6,7 @@ on: workflow_dispatch: permissions: - contents: write - pull-requests: write + contents: read jobs: check-updates: @@ -23,6 +22,11 @@ jobs: exit 1 fi + if ! GH_TOKEN="$AUTOMERGE_PAT" gh api user --silent; then + echo "::error::AUTOMERGE_PAT is invalid or expired. Generate a new fine-grained PAT (contents: write, pull-requests: write, this repo only) and update the AUTOMERGE_PAT repository secret." + exit 1 + fi + - uses: actions/checkout@v6 with: token: ${{ secrets.AUTOMERGE_PAT }} From 8c6a75f331fbe03628e634a3e122625dfd876d30 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 18:57:36 +0000 Subject: [PATCH 09/12] Update actions/checkout action to v7 (#75) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 4 ++-- .github/workflows/check-sdk-updates.yml | 2 +- .github/workflows/deploy.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01122c4..a1ebbbe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: outputs: matrix: ${{ steps.generate.outputs.matrix }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Generate matrix from matrix.json id: generate @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Extract versions from Dockerfile id: versions diff --git a/.github/workflows/check-sdk-updates.yml b/.github/workflows/check-sdk-updates.yml index b886c64..439ddf8 100644 --- a/.github/workflows/check-sdk-updates.yml +++ b/.github/workflows/check-sdk-updates.yml @@ -27,7 +27,7 @@ jobs: exit 1 fi - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: token: ${{ secrets.AUTOMERGE_PAT }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ed56d3f..1c15512 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -11,7 +11,7 @@ jobs: outputs: matrix: ${{ steps.generate.outputs.matrix }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Generate matrix from matrix.json id: generate @@ -33,7 +33,7 @@ jobs: fail-fast: false matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Extract versions from Dockerfile id: versions From a47de56e6d03de19c9f637543b9fe2cbe6b6e1a1 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Mon, 10 Aug 2026 11:58:05 -0700 Subject: [PATCH 10/12] fix: validate persisted runner registration before reusing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The upstream myoung34 entrypoint reuses CONFIGURED_ACTIONS_RUNNER_FILES_DIR credentials unconditionally: if .runner exists it skips registration with no validation and no fallback. When the runner has been removed server-side (GitHub prunes runners offline >14 days, manual deletion, etc), the stored credentials are dead and the container crash-loops forever — even though a valid ACCESS_TOKEN is sitting in the environment the whole time. Add a preflight entrypoint that asks GitHub whether the persisted agentId still exists. Only a definitive 404 wipes the persisted registration, letting the upstream entrypoint fall through to fresh ACCESS_TOKEN registration and store the new credentials back. Transient errors, missing env, or unparseable state leave everything untouched, so behavior is never worse than upstream. Declaring ENTRYPOINT resets the CMD inherited from the base image, so the base CMD is restated verbatim. Co-Authored-By: Claude Fable 5 --- Dockerfile | 13 +++++- preflight-entrypoint.sh | 95 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 preflight-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index c448808..da406ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,17 @@ COPY cleanup.sh /usr/local/bin/cleanup.sh RUN chmod +x /usr/local/bin/cleanup.sh ENV ACTIONS_RUNNER_HOOK_JOB_COMPLETED=/usr/local/bin/cleanup.sh -# NB: there is no CMD so it will work the same as the base image. See the +# Preflight entrypoint: validates the persisted runner registration against +# GitHub before handing off to the upstream entrypoint. Upstream reuses +# CONFIGURED_ACTIONS_RUNNER_FILES_DIR credentials unconditionally, which +# crash-loops the container when the runner was removed server-side; the +# preflight wipes provably-dead registrations so a fresh ACCESS_TOKEN +# registration happens instead. See preflight-entrypoint.sh for details. +COPY preflight-entrypoint.sh /usr/local/bin/preflight-entrypoint.sh +RUN chmod +x /usr/local/bin/preflight-entrypoint.sh +ENTRYPOINT ["/usr/local/bin/preflight-entrypoint.sh"] +# Declaring ENTRYPOINT resets the CMD inherited from the base image, so the +# base image's CMD is restated here. See # https://github.com/myoung34/docker-github-actions-runner#environment-variables # for how to use the image +CMD ["./bin/Runner.Listener", "run", "--startuptype", "service"] diff --git a/preflight-entrypoint.sh b/preflight-entrypoint.sh new file mode 100644 index 0000000..f569286 --- /dev/null +++ b/preflight-entrypoint.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# Preflight wrapper around the upstream myoung34 entrypoint. +# +# When CONFIGURED_ACTIONS_RUNNER_FILES_DIR is set, the upstream entrypoint +# copies the persisted config into /actions-runner and skips registration +# whenever a .runner file exists — with no validation of the stored +# credentials and no fallback to ACCESS_TOKEN. If the runner was meanwhile +# removed server-side (pruned by GitHub after being offline too long, deleted +# manually, etc), the stored credentials are dead and the container +# crash-loops forever without ever re-registering, even though a perfectly +# good ACCESS_TOKEN is sitting in the environment. +# +# This preflight asks GitHub whether the persisted runner still exists. Only +# a definitive 404 wipes the persisted registration, which makes the upstream +# entrypoint fall through to a fresh ACCESS_TOKEN registration (and store the +# new credentials back into the persist dir). Transient errors, missing +# variables, or unparseable state leave everything untouched, so behavior is +# never worse than upstream's. +# +# Intentionally not using set -e: preflight must never block runner startup. + +log() { echo "[preflight] $*"; } + +preflight() { + local dir="${CONFIGURED_ACTIONS_RUNNER_FILES_DIR:-}" + [ -n "$dir" ] || return 0 + [ -f "$dir/.runner" ] || return 0 + if [ -z "${ACCESS_TOKEN:-}" ]; then + log "ACCESS_TOKEN not set; cannot validate persisted registration, leaving as-is" + return 0 + fi + + # .runner is JSON written by the runner itself; it may carry a UTF-8 BOM. + local agent_id + agent_id=$(sed '1s/^\xEF\xBB\xBF//' "$dir/.runner" | jq -r '.agentId // empty' 2>/dev/null) + if [ -z "$agent_id" ]; then + log "could not read agentId from $dir/.runner; leaving as-is" + return 0 + fi + + local github_host="${GITHUB_HOST:-github.com}" + local api_base + if [ "$github_host" = "github.com" ]; then + api_base="https://api.github.com" + else + api_base="https://${github_host}/api/v3" + fi + + local api_path + case "${RUNNER_SCOPE:-repo}" in + org) + if [ -z "${ORG_NAME:-}" ]; then + log "RUNNER_SCOPE=org but ORG_NAME unset; leaving as-is" + return 0 + fi + api_path="orgs/${ORG_NAME}/actions/runners/${agent_id}" + ;; + ent*) + log "enterprise scope validation not implemented; leaving as-is" + return 0 + ;; + *) + # repo scope: REPO_URL looks like https://// + local repo_path="${REPO_URL#*://*/}" + if [ -z "$repo_path" ] || [ "$repo_path" = "${REPO_URL:-}" ]; then + log "cannot parse REPO_URL; leaving as-is" + return 0 + fi + api_path="repos/${repo_path}/actions/runners/${agent_id}" + ;; + esac + + local status + status=$(curl -sS -o /dev/null -w '%{http_code}' \ + -H "Authorization: token ${ACCESS_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + "${api_base}/${api_path}" 2>/dev/null) + + case "$status" in + 200) + log "persisted runner agentId=${agent_id} still registered; reusing stored credentials" + ;; + 404) + log "persisted runner agentId=${agent_id} no longer exists on ${github_host};" \ + "wiping persisted registration to force fresh ACCESS_TOKEN registration" + rm -f "$dir/.runner" "$dir/.credentials" "$dir/.credentials_rsaparams" + ;; + *) + log "could not validate persisted runner (HTTP ${status:-none}); leaving as-is" + ;; + esac +} + +preflight +exec /entrypoint.sh "$@" From 577c054bd37fb7ea673b03c9b7e95416769122b6 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Mon, 10 Aug 2026 12:11:05 -0700 Subject: [PATCH 11/12] fix: bound the validation curl so network stalls can't block startup Co-Authored-By: Claude Fable 5 --- preflight-entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/preflight-entrypoint.sh b/preflight-entrypoint.sh index f569286..40fecb7 100644 --- a/preflight-entrypoint.sh +++ b/preflight-entrypoint.sh @@ -70,8 +70,11 @@ preflight() { ;; esac + # Bounded timeouts so a DNS/TLS/network stall can't block runner startup; + # a timeout falls into the default case below and leaves the config as-is. local status status=$(curl -sS -o /dev/null -w '%{http_code}' \ + --connect-timeout 5 --max-time 15 \ -H "Authorization: token ${ACCESS_TOKEN}" \ -H "Accept: application/vnd.github+json" \ "${api_base}/${api_path}" 2>/dev/null) From 2ea3834f4286336ddbc5faf17119c81522e0b48b Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Tue, 11 Aug 2026 12:08:24 -0700 Subject: [PATCH 12/12] fix: wipe migrated config files too when clearing dead registrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 404 wipe removed .runner/.credentials/.credentials_rsaparams but left .runner_migrated behind. The runner's IsConfigured() treats .runner_migrated as a valid config (ConfigurationStore.cs checks either file), so config.sh refused to re-register while run.sh crashed loading the missing .runner ("Value cannot be null. (Parameter 'configuredSettings')") — an unrecoverable crash loop, observed on bump-android-nas.local after GitHub pruned its registration during a long offline stretch. Wipes now cover the migrated files, clean /actions-runner as well (the writable layer survives restarts and the upstream persist-back step re-seeds the persist dir from it), and a preflight pass heals the already-wedged orphaned-.runner_migrated state left by older images. Co-Authored-By: Claude Fable 5 --- preflight-entrypoint.sh | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/preflight-entrypoint.sh b/preflight-entrypoint.sh index 40fecb7..5f5b013 100644 --- a/preflight-entrypoint.sh +++ b/preflight-entrypoint.sh @@ -21,9 +21,33 @@ log() { echo "[preflight] $*"; } +# The runner's IsConfigured()/HasCredentials() checks treat .runner_migrated / +# .credentials_migrated as equivalent to the primary files, so a wipe must +# remove all five or config.sh refuses to re-register while run.sh can't load +# settings ("Value cannot be null. (Parameter 'configuredSettings')") — a +# crash loop that never self-heals. The files are removed from /actions-runner +# too: the writable layer survives restarts, and the upstream entrypoint's +# persist-back step otherwise re-seeds the persist dir from it. +wipe_registration() { + local dir="$1" f + for f in .runner .credentials .credentials_rsaparams .runner_migrated .credentials_migrated; do + rm -f "$dir/$f" "/actions-runner/$f" + done +} + preflight() { local dir="${CONFIGURED_ACTIONS_RUNNER_FILES_DIR:-}" [ -n "$dir" ] || return 0 + + # Heal the wedged state left behind by a partial wipe (an older preflight or + # manual cleanup that removed .runner but not .runner_migrated): without + # .runner there is nothing to validate, and the orphaned migrated files + # alone block registration. + if [ ! -f "$dir/.runner" ] && [ -f "$dir/.runner_migrated" ]; then + log "orphaned .runner_migrated without .runner; removing it so registration can proceed" + wipe_registration "$dir" + fi + [ -f "$dir/.runner" ] || return 0 if [ -z "${ACCESS_TOKEN:-}" ]; then log "ACCESS_TOKEN not set; cannot validate persisted registration, leaving as-is" @@ -86,7 +110,7 @@ preflight() { 404) log "persisted runner agentId=${agent_id} no longer exists on ${github_host};" \ "wiping persisted registration to force fresh ACCESS_TOKEN registration" - rm -f "$dir/.runner" "$dir/.credentials" "$dir/.credentials_rsaparams" + wipe_registration "$dir" ;; *) log "could not validate persisted runner (HTTP ${status:-none}); leaving as-is"