fix: bypass release PR checks with admin token (#2758)

This commit is contained in:
Carlos Florêncio
2026-08-17 19:39:13 +00:00
committed by GitHub
parent 8fa816f404
commit 96dbcb9938
13 changed files with 529 additions and 35 deletions
+16
View File
@@ -38,6 +38,22 @@ Stable runs entirely in CI via `.github/workflows/release.yml`, triggered by a m
6. `update-homebrew-tap` pushes updated `Formula/kandev.rb` to `kdlbs/homebrew-kandev` via SSH deploy key.
7. `update-scoop-bucket` pushes updated `bucket/kandev.json` to `kdlbs/scoop-kandev` via its SSH deploy key.
## Release PR ruleset bypass
A normal Stable release creates its branch and pull request with `GITHUB_TOKEN`.
It uses `RELEASE_PR_BYPASS_TOKEN` only for an exact-head `gh pr merge --admin`.
Store this fine-grained personal access token in the protected `release`
environment. Its owner must remain an organization administrator. Select only
`kdlbs/kandev` and grant `contents: write` repository permission.
Record the token owner and expiration date. Rotate the environment secret before
the token expires or the owner loses administrator access. The workflow must
stop before tag creation when the token is missing or cannot bypass the ruleset.
After the merge, use `GITHUB_TOKEN` to read the PR state. Tag only the merge
commit that GitHub reports after it appears on `origin/main`.
**Workflow-control invariant:** When a channel intentionally skips a job, every
downstream job reachable through that dependency chain must use a status function
such as `!cancelled()` plus explicit `needs.<job>.result == 'success'` checks.
@@ -328,12 +328,54 @@ class ReleaseWorkflowContractTest(unittest.TestCase):
self.assertIn("CURRENT_REF: ${{ github.ref }}", guard)
self.assertIn('if [ "$CURRENT_REF" != "refs/heads/main" ]', guard)
def test_normal_release_uses_admin_token_only_for_exact_head_merge(self) -> None:
create = step_block("Create release PR")
merge = step_block("Merge release PR with administrator token")
select = step_block("Select merged release commit")
for step in (create, merge, select):
self.assertIn(NORMAL_RELEASE_IF, step)
self.assertIn("id: release_pr", create)
self.assertIn("GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}", create)
self.assertIn('MSG="release: publish $NEXT"', create)
self.assertIn('HEAD_SHA="$(git rev-parse HEAD)"', create)
self.assertIn('echo "url=$PR_URL" >> "$GITHUB_OUTPUT"', create)
self.assertIn('echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"', create)
self.assertNotIn("RELEASE_PR_BYPASS_TOKEN", create)
self.assertIn(
"GH_TOKEN: ${{ secrets.RELEASE_PR_BYPASS_TOKEN }}",
merge,
)
self.assertIn("PR_URL: ${{ steps.release_pr.outputs.url }}", merge)
self.assertIn("EXPECTED_HEAD: ${{ steps.release_pr.outputs.head }}", merge)
self.assertIn('if [ -z "$GH_TOKEN" ]', merge)
self.assertIn("RELEASE_PR_BYPASS_TOKEN is required", merge)
self.assertIn('gh pr merge "$PR_URL"', merge)
self.assertIn("--admin", merge)
self.assertIn("--squash", merge)
self.assertIn('--match-head-commit "$EXPECTED_HEAD"', merge)
self.assertNotIn("--delete-branch", merge)
self.assertNotIn("--auto", merge)
self.assertIn("GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}", select)
self.assertNotIn("RELEASE_PR_BYPASS_TOKEN", select)
self.assertIn("--json state,mergedAt,mergeCommit,headRefOid", select)
self.assertIn('if [ "$PR_STATE" != "MERGED" ]', select)
self.assertIn('if [ "$PR_HEAD" != "$EXPECTED_HEAD" ]', select)
self.assertIn('[[ "$MERGE_COMMIT" =~ ^[0-9a-f]{40}$ ]]', select)
self.assertIn('git merge-base --is-ancestor "$MERGE_COMMIT" origin/main', select)
self.assertIn('git checkout --detach "$MERGE_COMMIT"', select)
def test_normal_release_preflights_and_revalidates_signing_key_around_merge(
self,
) -> None:
prepare = job_block("prepare")
bump = step_block("Bump version + generate CHANGELOG (in working tree)")
merge = step_block("Create release PR + squash-merge")
create_pr = step_block("Create release PR")
merge = step_block("Merge release PR with administrator token")
select = step_block("Select merged release commit")
preflight_public_key = step_block("Validate committed release signing public key")
preflight = step_block("Preflight release tag signing fingerprint")
merged_public_key = step_block("Validate merged release signing public key")
@@ -419,8 +461,10 @@ class ReleaseWorkflowContractTest(unittest.TestCase):
self.assertLess(prepare.index(preflight_public_key), prepare.index(preflight))
self.assertLess(prepare.index(preflight), prepare.index(bump))
self.assertLess(prepare.index(bump), prepare.index(merge))
self.assertLess(prepare.index(merge), prepare.index(merged_public_key))
self.assertLess(prepare.index(bump), prepare.index(create_pr))
self.assertLess(prepare.index(create_pr), prepare.index(merge))
self.assertLess(prepare.index(merge), prepare.index(select))
self.assertLess(prepare.index(select), prepare.index(merged_public_key))
self.assertLess(prepare.index(merged_public_key), prepare.index(signing))
self.assertLess(prepare.index(merge), prepare.index(signing))
self.assertLess(prepare.index(signing), prepare.index(validate))
@@ -469,6 +513,17 @@ class ReleaseWorkflowContractTest(unittest.TestCase):
self.assertIsNotNone(documented_fingerprint)
self.assertEqual(primary_fingerprints[0], documented_fingerprint.group(1))
def test_release_documentation_declares_the_pr_bypass_token_contract(self) -> None:
for requirement in (
"RELEASE_PR_BYPASS_TOKEN",
"fine-grained personal access token",
"Contents: Read and write",
"organization administrator",
"Only select repositories",
"expiration date",
):
self.assertIn(requirement, RELEASE_PROCESS)
def test_release_contract_ci_runs_when_key_or_release_documentation_changes(self) -> None:
"""This contract must run on every change, not a listed subset.
@@ -574,7 +629,9 @@ class ReleaseWorkflowContractTest(unittest.TestCase):
for name in (
"Bump version + generate CHANGELOG (in working tree)",
"Create release PR + squash-merge",
"Create release PR",
"Merge release PR with administrator token",
"Select merged release commit",
"Import release tag signing key",
"Validate release tag signing identity",
"Create and push signed release tag",
+61 -12
View File
@@ -406,7 +406,8 @@ jobs:
pnpm -C apps install --no-frozen-lockfile
git-cliff --tag "$TAG" -o CHANGELOG.md
- name: Create release PR + squash-merge
- name: Create release PR
id: release_pr
if: ${{ !inputs.dry_run && !inputs.desktop_validation_only && inputs.backfill_tag == '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -415,9 +416,9 @@ jobs:
run: |
set -euo pipefail
# main is protected by a ruleset that requires PRs, so we go through
# a release branch + auto-merged PR rather than pushing directly.
# a release branch and administrator-merged PR instead of pushing directly.
BRANCH="release/$TAG"
MSG="release: $NEXT"
MSG="release: publish $NEXT"
git checkout -b "$BRANCH"
git add apps/cli/package.json apps/desktop/package.json apps/desktop/src-tauri/Cargo.lock apps/desktop/src-tauri/Cargo.toml apps/desktop/src-tauri/tauri.conf.json CHANGELOG.md apps/pnpm-lock.yaml
@@ -430,22 +431,70 @@ jobs:
git push origin --delete "$BRANCH" 2>/dev/null || true
git push -u origin "$BRANCH"
gh pr create \
HEAD_SHA="$(git rev-parse HEAD)"
PR_URL="$(gh pr create \
--title "$MSG" \
--body "Automated release: \`$NEXT\`" \
--head "$BRANCH" \
--base main
# Immediate squash-merge. This relies on main's ruleset requiring
# only "PR required" with no status checks. If status checks are
# added to the ruleset later, this needs to switch to `--auto` +
# polling for merge completion before the tag steps below.
gh pr merge "$BRANCH" --squash --delete-branch --subject "$MSG" --body ""
--base main)"
echo "url=$PR_URL" >> "$GITHUB_OUTPUT"
echo "head=$HEAD_SHA" >> "$GITHUB_OUTPUT"
- name: Merge release PR with administrator token
if: ${{ !inputs.dry_run && !inputs.desktop_validation_only && inputs.backfill_tag == '' }}
env:
GH_TOKEN: ${{ secrets.RELEASE_PR_BYPASS_TOKEN }}
PR_URL: ${{ steps.release_pr.outputs.url }}
EXPECTED_HEAD: ${{ steps.release_pr.outputs.head }}
MSG: "release: publish ${{ steps.compute.outputs.version }}"
run: |
set -euo pipefail
if [ -z "$GH_TOKEN" ]; then
echo "RELEASE_PR_BYPASS_TOKEN is required in the release environment." >&2
exit 1
fi
gh pr merge "$PR_URL" \
--admin \
--squash \
--match-head-commit "$EXPECTED_HEAD" \
--subject "$MSG" \
--body ""
- name: Select merged release commit
if: ${{ !inputs.dry_run && !inputs.desktop_validation_only && inputs.backfill_tag == '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_URL: ${{ steps.release_pr.outputs.url }}
EXPECTED_HEAD: ${{ steps.release_pr.outputs.head }}
run: |
set -euo pipefail
PR_JSON="$(gh pr view "$PR_URL" --json state,mergedAt,mergeCommit,headRefOid)"
PR_STATE="$(jq -r '.state' <<<"$PR_JSON")"
MERGED_AT="$(jq -r '.mergedAt // empty' <<<"$PR_JSON")"
PR_HEAD="$(jq -r '.headRefOid' <<<"$PR_JSON")"
MERGE_COMMIT="$(jq -r '.mergeCommit.oid // empty' <<<"$PR_JSON")"
if [ "$PR_STATE" != "MERGED" ] || [ -z "$MERGED_AT" ]; then
echo "The release pull request did not merge." >&2
exit 1
fi
if [ "$PR_HEAD" != "$EXPECTED_HEAD" ]; then
echo "The merged release pull request head does not match the expected commit." >&2
exit 1
fi
if ! [[ "$MERGE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "The release pull request did not report a valid merge commit." >&2
exit 1
fi
# Pull the squashed commit into local main. The signing key is not
# imported until all repository-controlled release work is complete.
git fetch origin main
git checkout main
git reset --hard origin/main
if ! git merge-base --is-ancestor "$MERGE_COMMIT" origin/main; then
echo "The reported release merge commit is not on origin/main." >&2
exit 1
fi
git checkout --detach "$MERGE_COMMIT"
- name: Validate merged release signing public key
id: merged_release_gpg
+1 -1
View File
@@ -65,7 +65,7 @@ The commitlint hook caps the header at **100 characters** (`type(scope): descrip
### Release & Versioning
Stable Kandev releases use one **SemVer** `X.Y.Z` across npm, Homebrew, Scoop, GitHub Releases, Desktop, and containers. Scheduled npm-only Nightlies use `X.Y.(Z+1)-nightly.sha<12-hex>` without moving any Stable channel. Both flows run in `.github/workflows/release.yml`. A Stable release is complete only after the GitHub Release, npm, Homebrew, and Scoop publications succeed and are verified. Full details are in the `/release` skill — load it when cutting a release, changing version channels, or debugging release artifacts.
Stable Kandev releases use one **SemVer** `X.Y.Z` across npm, Homebrew, Scoop, GitHub Releases, Desktop, and containers. Scheduled npm-only Nightlies use `X.Y.(Z+1)-nightly.sha<12-hex>` without moving any Stable channel. Both flows run in `.github/workflows/release.yml`. A normal Stable release uses the protected `RELEASE_PR_BYPASS_TOKEN` environment secret only for its administrator PR merge. A Stable release is complete only after the GitHub Release, npm, Homebrew, and Scoop publications succeed and are verified. Full details are in the `/release` skill — load it when cutting a release, changing version channels, or debugging release artifacts.
### Code Quality
+29 -16
View File
@@ -1,14 +1,14 @@
# Enabling the merge queue
# Merge queue
Everything in this document except the final section is already in the
repository. Nothing here is switched on: the workflows are prepared, and the
ruleset change that turns a merge queue on is a separate, deliberate decision.
The `main` ruleset enabled the merge queue and six required checks on
2026-08-16. This document records the workflow preparation, active rules, and
the Stable release exception.
## Why
`main`'s ruleset (id `13341245`) has `deletion`, `non_fast_forward`,
`required_linear_history`, and `pull_request` rules, and no
`required_status_checks` rule at all. Merges are not gated on CI in any way.
Before activation, `main`'s ruleset (id `13341245`) had `deletion`,
`non_fast_forward`, `required_linear_history`, and `pull_request` rules. It had
no `required_status_checks` rule.
That is how #2681 (`38c94e7d0`) landed red. Its E2E checks were fully green, but
they had run 23 hours earlier against a merge ref that predated the retry loop
@@ -141,16 +141,29 @@ Concurrency groups were checked too. None of the six keyed on a PR number
`cancel-in-progress: ${{ github.event_name != 'merge_group' }}`, because a queue
reads a cancelled check as a failure and ejects the pull request.
## Enabling it
## Active ruleset configuration
1. Add a `merge_queue` rule to the `main` ruleset.
2. Add a `required_status_checks` rule and select exactly the six check names in
the table above. Do **not** select any per-shard name (`E2E Shard 3/14`,
`Backend Tests (1/2)`, ...) -- they are covered by the gates, and a queue
configured against them breaks the next time a shard count changes.
3. Do **not** select `lint` (from `pr-title.yml`), `Validate public docs`, or
any `claude-review-*` / `opencode-review-*` / `deploy-*` check. Those never
run in a merge group and the queue will wait on them indefinitely.
The `main` ruleset contains a `merge_queue` rule and requires the six gate names
in the table above. It does not require per-shard names.
Do not require `lint` from `pr-title.yml`, `Validate public docs`, review checks,
or deployment checks. These checks do not run in every merge group.
### Stable release exception
The Stable release workflow creates a mechanical release PR with `GITHUB_TOKEN`.
GitHub holds the PR workflows for approval, but maintainers do not require CI
for this generated commit.
The protected `release` environment contains `RELEASE_PR_BYPASS_TOKEN`. This
fine-grained personal access token belongs to an organization administrator and
selects only `kdlbs/kandev` with `contents: write` permission.
The workflow exposes this token only to `gh pr merge --admin`. It also binds
the merge to the expected PR head. `GITHUB_TOKEN` performs all other PR work.
The administrator merge bypasses the queue and required checks for the release
PR only. Ordinary PRs still use the queue and all six required checks.
### Caveat: the ruleset can still be bypassed
@@ -0,0 +1,45 @@
# ADR-2026-08-17-release-pr-ruleset-bypass: Give Stable Release PRs an Administrator Token Bypass
**Status:** accepted
**Date:** 2026-08-17
**Area:** infra, workflow, security
## Context
The Stable release workflow creates a generated version pull request and merges it immediately. The `main` ruleset added required checks and a merge queue on 2026-08-16.
GitHub CLI now rejects the `--delete-branch` merge option in the workflow when the queue is active. Queue enrollment also waits for checks that GitHub holds for approval on `GITHUB_TOKEN`-created pull requests.
Maintainers do not require CI for generated release commits. The workflow still needs a pull request for traceability and must bind the signed tag to the release merge.
## Decision
Normal Stable releases keep the release pull request. A fine-grained personal access token from an organization administrator merges that pull request with the GitHub CLI `--admin` option.
The token selects only `kdlbs/kandev`. It has `contents: write` repository permission. The existing organization-administrator bypass in the `main` ruleset authorizes the merge.
The protected `release` environment owns the token as the `RELEASE_PR_BYPASS_TOKEN` secret. Maintainers must rotate the token before it expires and replace it when its owner loses administrator access.
The normal `GITHUB_TOKEN` pushes the release branch, creates the pull request, and reads the merged state. The personal access token is available only to the step that merges the expected head.
The workflow removes `--delete-branch` and relies on the repository branch-cleanup setting. It verifies the reported merge commit on `origin/main` before it signs the release tag.
The repair contract is recorded in `docs/specs/release-pr-queue-bypass/spec.md`.
## Consequences
Generated release pull requests merge without CI or queue latency. Ordinary pull requests remain subject to every required check and queue rule.
The person-owned token becomes a protected release dependency. A missing, expired, revoked, or insufficient token stops the release before tag creation.
The token owner can bypass repository rules as an organization administrator. Restricting the token to one repository and one workflow step limits its exposure, but not the owner's bypass capability.
Maintainers must configure the `release` environment before the next normal Stable release. Ruleset `13341245` already grants organization administrators bypass access.
## Alternatives Considered
- Wait for pull-request and merge-group CI. Rejected because maintainers do not require CI for generated release commits.
- Use a dedicated Release GitHub App. Rejected for the initial repair because its setup and key management add operational work. It remains the preferred replacement for a person-owned token.
- Add the built-in GitHub Actions app to the bypass list. Rejected because this gives the same bypass identity to other workflows.
- Push the release commit directly to `main`. Rejected because this removes the pull-request trail and requires a wider bypass.
- Remove required checks or the merge queue. Rejected because ordinary pull requests need those protections.
+1
View File
@@ -172,3 +172,4 @@ Read individual ADRs for full context. Create new ones via `/record decision` or
| 2026-08-12-task-bound-fork-destinations | [Bind Fork Push Destinations to Tasks](2026-08-12-task-bound-fork-destinations.md) | accepted (amended 2026-08-13) | backend, frontend, workflow, security, GitHub | 2026-08-12 |
| 2026-08-13-hard-delete-task-contribution-links | [Hard delete owns task contribution links](2026-08-13-hard-delete-task-contribution-links.md) | accepted | backend | 2026-08-13 |
| 2026-08-15-office-mode-follows-active-workspace | [Office Mode Follows the Active Workspace](2026-08-15-office-mode-follows-active-workspace.md) | accepted | frontend, backend | 2026-08-15 |
| 2026-08-17-release-pr-ruleset-bypass | [Give Stable Release PRs an Administrator Token Bypass](2026-08-17-release-pr-ruleset-bypass.md) | accepted | infra, workflow, security | 2026-08-17 |
+105
View File
@@ -0,0 +1,105 @@
---
spec: docs/specs/release-pr-queue-bypass/spec.md
created: 2026-08-17
status: complete
---
# Implementation Plan: Stable Release PR Queue Bypass
## Overview
The release workflow still requests an immediate branch deletion during a queued merge. GitHub rejects that option before the generated release pull request can merge.
The repair uses an organization-administrator personal access token for one privileged pull-request merge. It then binds tag creation to the merge commit reported by GitHub.
## Release workflow
### Regression contract
- Update `.github/scripts/release-workflow-contract_test.py` before the workflow change.
- Require the protected `RELEASE_PR_BYPASS_TOKEN` secret only in the privileged merge step.
- Require an explicit missing-token failure before the merge command.
- Require `GITHUB_TOKEN` for branch push and pull-request creation.
- Require the administrator token for `gh pr merge --admin` only.
- Require `GITHUB_TOKEN` for merged-state inspection.
- Reject `--delete-branch`, queue enrollment, polling, and a merge without `--match-head-commit`.
- Require the reported merge commit to exist on `origin/main` before tag creation.
- Require a valid lowercase pull-request title subject.
### Immediate privileged merge
- Update `.github/workflows/release.yml`.
- Read `RELEASE_PR_BYPASS_TOKEN` only in the normal-release merge step.
- Fail before the merge command when the secret is empty.
- Capture the release pull-request URL and head commit.
- Merge with `--admin`, `--squash`, and `--match-head-commit`.
- Remove `--delete-branch` and rely on repository branch cleanup.
- Read the merged state and merge commit through `GITHUB_TOKEN`.
- Fetch `main`, make sure that it contains the merge commit, and select that commit for signing.
- Keep the existing public-key revalidation after the merge.
## Release contract records
- Update `docs/public/release-process.md` as a how-to guide.
- Update `docs/ci-merge-queue.md` with the administrator-token exception.
- Update `.agents/skills/release/SKILL.md` with the bypass and configuration contract.
- Update `AGENTS.md` because its release guidance must name the bypass requirement.
- Set the repair spec and its index entry to `shipped` after the implementation checks pass.
- Keep ADR `2026-08-17-release-pr-ruleset-bypass` aligned with the final implementation.
## Tests
- **What:** Normal Stable releases expose the administrator token only to the privileged merge step.
**File:** `.github/scripts/release-workflow-contract_test.py`.
**How:** Assert the secret name, normal-release condition, missing-secret guard, and token isolation.
- **What:** Release pull requests bypass CI and the merge queue for one exact head.
**File:** `.github/scripts/release-workflow-contract_test.py`.
**How:** Assert `--admin`, `--squash`, and `--match-head-commit`. Reject `--delete-branch` and `--auto`.
- **What:** Tag signing uses the merge commit that GitHub reports.
**File:** `.github/scripts/release-workflow-contract_test.py`.
**How:** Assert merged-state parsing, ancestry validation against `origin/main`, and local selection of the reported commit.
- **What:** Excluded release modes cannot request the bypass token.
**File:** `.github/scripts/release-workflow-contract_test.py`.
**How:** Assert the normal-release condition on token creation, identity validation, and privileged merge steps.
- **What:** Maintainer documentation names the token scope, owner, expiration, rotation, ruleset, and protected environment contract.
**Files:** `docs/public/release-process.md`, `.agents/skills/release/SKILL.md`, `AGENTS.md`, and `docs/ci-merge-queue.md`.
**How:** Use the public-doc and harness-file validators.
## Verification results
- `python3 .github/scripts/release-workflow-contract_test.py` passed 31 tests.
- `python3 .github/scripts/lint-action-pinning_test.py` passed 9 tests.
- `node --test scripts/validate-public-docs.test.mjs` passed 61 tests.
- `node scripts/validate-public-docs.mjs` validated 41 pages.
- `python3 scripts/lint-harness-files.test.py` passed 19 tests.
- `python3 .github/scripts/lint-harness-files.py --all` passed 118 files.
- `pre-commit run harness-lint --files AGENTS.md .agents/skills/release/SKILL.md` passed.
- `git diff --check` passed.
- `actionlint` was not available in the local environment. The repository contract tests and commit hooks remain required.
## Implementation waves and parallel candidates
Wave 1 (sequential):
- [x] [task-01-release-workflow](task-01-release-workflow.md)
Wave 2 (sequential, depends on Wave 1):
- [x] [task-02-release-documentation](task-02-release-documentation.md)
Parallel delegation is not authorized. The tasks share the release contract and run sequentially in the primary conversation.
## Post-merge rollout
1. Create a fine-grained personal access token from an organization administrator account.
2. Select only `kdlbs/kandev` and grant `contents: write` permission.
3. Add the token as the `RELEASE_PR_BYPASS_TOKEN` secret in the `release` environment.
4. Record its owner and expiration date in the maintainers' credential inventory.
5. Dispatch a new normal Stable release for `v0.89.0`.
6. Make sure that the release PR merges immediately and the signed tag targets its merge commit.
The implementation delivery closes stale release PR #2751. The next release run removes and recreates its existing release branch.
## Open questions
None.
@@ -0,0 +1,57 @@
---
id: "01-release-workflow"
title: "Repair the Stable release merge"
status: done
wave: 1
depends_on: []
plan: "plan.md"
spec: "../../specs/release-pr-queue-bypass/spec.md"
---
# Task 01: Repair the Stable release merge
## Acceptance
- The regression contract fails before the workflow change and passes afterward.
- A normal Stable release uses the administrator token only to merge one exact pull-request head without CI or queue enrollment.
- Tag signing selects the merge commit reported by GitHub only after that commit appears on `origin/main`.
## Verification
- `python3 .github/scripts/release-workflow-contract_test.py`
- `python3 .github/scripts/lint-action-pinning_test.py`
- `git diff --check -- .github/workflows/release.yml .github/scripts/release-workflow-contract_test.py`
## Files likely touched
- `.github/workflows/release.yml`
- `.github/scripts/release-workflow-contract_test.py`
## Dependencies
None.
## Parallelism
Sequential. The workflow and its contract test form one TDD unit.
## Inputs
- `docs/specs/release-pr-queue-bypass/spec.md`
- `docs/decisions/2026-08-17-release-pr-ruleset-bypass.md`
- The `Create release PR + squash-merge` step in `.github/workflows/release.yml`.
- The protected `release` environment on the `prepare` job.
## Output contract
Report the Red and Green results, changed files, token boundary, and merge-commit guard. Update this task and `plan.md` in the same conversation.
## Results
- Red: `python3 .github/scripts/release-workflow-contract_test.py ReleaseWorkflowContractTest.test_normal_release_uses_admin_token_only_for_exact_head_merge` failed because the administrator-token merge step did not exist.
- Green: the same focused test passed after the workflow change.
- `python3 .github/scripts/release-workflow-contract_test.py` passed 30 tests.
- `python3 .github/scripts/lint-action-pinning_test.py` passed 9 tests.
- `git diff --check -- .github/workflows/release.yml .github/scripts/release-workflow-contract_test.py` passed.
- `RELEASE_PR_BYPASS_TOKEN` is available only to the privileged merge step. `GITHUB_TOKEN` creates the pull request and inspects its merged state.
- The workflow validates the exact pull-request head and checks out GitHub's reported merge commit before tag signing.
@@ -0,0 +1,66 @@
---
id: "02-release-documentation"
title: "Document the Stable release bypass"
status: done
wave: 2
depends_on: ["01-release-workflow"]
plan: "plan.md"
spec: "../../specs/release-pr-queue-bypass/spec.md"
---
# Task 02: Document the Stable release bypass
## Acceptance
- Maintainer documentation states the token permissions, owner requirement, expiration, rotation, environment configuration, and administrator bypass.
- Repository guidance explains that ordinary pull requests still use the merge queue and required CI.
- The repair spec is `shipped`, and the ADR matches the implemented token boundary.
## Verification
- `node --test scripts/validate-public-docs.test.mjs`
- `node scripts/validate-public-docs.mjs`
- `python3 scripts/lint-harness-files.test.py`
- `python3 .github/scripts/lint-harness-files.py --all`
- `git diff --check -- docs/public/release-process.md docs/ci-merge-queue.md .agents/skills/release/SKILL.md AGENTS.md docs/specs/release-pr-queue-bypass/spec.md docs/specs/INDEX.md docs/decisions/2026-08-17-release-pr-ruleset-bypass.md docs/decisions/INDEX.md`
## Files likely touched
- `docs/public/release-process.md`
- `docs/ci-merge-queue.md`
- `.agents/skills/release/SKILL.md`
- `AGENTS.md`
- `docs/specs/release-pr-queue-bypass/spec.md`
- `docs/specs/INDEX.md`
- `docs/decisions/2026-08-17-release-pr-ruleset-bypass.md`
- `docs/decisions/INDEX.md`
## Dependencies
Task 01 must pass before this task starts.
## Parallelism
Sequential. The documents record the workflow contract from Task 01.
## Inputs
- The implemented workflow and contract from Task 01.
- `docs/specs/release-pr-queue-bypass/spec.md`.
- ADR `2026-08-17-release-pr-ruleset-bypass`.
- The existing how-to structure of the release page.
## Output contract
Report the documentation class, changed files, validator results, and external rollout requirements. Update this task and `plan.md` in the same conversation.
## Results
- Public docs updated: `docs/public/release-process.md` remains a how-to guide and now includes the PAT creation and rotation procedure.
- Internal docs updated: `docs/ci-merge-queue.md`, `.agents/skills/release/SKILL.md`, `AGENTS.md`, the repair spec, and the ADR.
- `node --test scripts/validate-public-docs.test.mjs` passed 61 tests.
- `node scripts/validate-public-docs.mjs` validated 41 pages.
- `python3 scripts/lint-harness-files.test.py` passed 19 tests.
- `python3 .github/scripts/lint-harness-files.py --all` passed 118 files.
- `pre-commit run harness-lint --files AGENTS.md .agents/skills/release/SKILL.md` passed.
- `git diff --check` passed.
+31 -2
View File
@@ -37,9 +37,38 @@ The workflow has four mutually exclusive operating modes:
2. Confirm required checks are green on `main` and no release or release PR is active.
3. Confirm merged PR titles/commits use the conventional categories consumed by `cliff.toml`. The workflow generates `CHANGELOG.md` and release notes.
4. Verify platform-sensitive launcher, agentctl, container, and desktop changes on affected targets.
5. Check GitHub/GHCR access, npm trusted publishing, the release-tag GPG key, the Homebrew deploy key, the Scoop deploy key (`SCOOP_BUCKET_DEPLOY_KEY`), and any configured desktop signing/notarization secrets.
5. Check GitHub/GHCR access, npm trusted publishing, `RELEASE_PR_BYPASS_TOKEN`, signing keys, and configured desktop signing or notarization secrets.
6. Update public docs for behavior that is about to ship.
## Configure the release PR bypass
Normal releases use a fine-grained personal access token only for the privileged release PR merge. The normal `GITHUB_TOKEN` creates the branch and PR.
The token owner must remain an organization administrator. The current `main` ruleset grants that role an always-allowed bypass.
1. Sign in to the organization administrator account that will own the token.
2. Open **Settings > Developer settings > Personal access tokens > Fine-grained tokens**.
3. Select **Generate new token**.
4. Enter a clear name, such as `kandev-release-pr-bypass`.
5. Set an expiration date that follows the organization policy.
6. Select `kdlbs` as the resource owner.
7. Under **Repository access**, select **Only select repositories**.
8. Select only the `kandev` repository.
9. Under **Repository permissions**, set **Contents: Read and write**.
10. Leave all other optional permissions set to **No access**.
11. Generate the token and complete organization approval when GitHub requires it.
12. Copy the token before you leave the page.
Add the token to the protected `release` environment:
```bash
gh secret set RELEASE_PR_BYPASS_TOKEN --env release --repo kdlbs/kandev
```
Record the token owner and expiration date in the maintainer credential inventory. Rotate the secret before expiration or an owner-role change.
CAUTION: Never store this token as a repository-wide secret. The token owner can bypass the `main` ruleset.
Normal releases require a dedicated release-tag signing identity and a GitHub Environment named `release`. Restrict that environment to deployments from `main`, but do not configure required reviewers: any maintainer authorized to dispatch the workflow and access the environment may start a normal release. Store the ASCII-armored private key as the environment secret `RELEASE_GPG_PRIVATE_KEY`, its optional passphrase as the environment secret `RELEASE_GPG_PASSPHRASE`, and the exact full fingerprint as the required environment variable `RELEASE_GPG_FINGERPRINT`. Before changing version files or creating the release PR, the workflow rejects a missing, multi-key, or secret-material public-key attachment and requires its sole primary fingerprint to match `RELEASE_GPG_FINGERPRINT`. After the release PR merges, it revalidates the public key from the merged revision, imports the private key, and requires that key to match both the merged key and `RELEASE_GPG_FINGERPRINT`. Do not define this signing material as repository-wide configuration.
The `prepare` job selects its environment by mode: normal releases use `release`, while dry runs, desktop validation, and backfills use `release-validation`. Leave `release-validation` as an empty default environment with no secrets, variables, deployment restrictions, or protection rules so validation and repair modes remain usable without access to the signing identity.
@@ -63,7 +92,7 @@ Use dry run to validate version/changelog preparation. Use desktop validation to
Normal mode performs these stages:
1. **Preflight and prepare version.** Compute the next version from packages and tags, then verify that the committed public key matches the `release` environment fingerprint before changing release files. Update the CLI package/lock, desktop package and Tauri/Cargo manifests, and `CHANGELOG.md`.
2. **Merge and tag.** Open a release branch and PR, squash-merge it, then revalidate the public key from the merged revision before importing the protected signing key. The workflow requires the imported key's full fingerprint to exactly match that merged key and `RELEASE_GPG_FINGERPRINT`, adopts that key's name and email as the tagger identity, and locally verifies the GPG-signed `vX.Y.Z` tag before pushing it.
2. **Merge and tag.** Open a release branch and PR. Use the protected administrator token to squash-merge the exact head without queue or CI latency. Select GitHub's reported merge commit, then revalidate the public key before importing the protected signing key. Verify the signed `vX.Y.Z` tag locally before the push.
3. **Build web and runtimes.** Build the SPA and five runtime targets: Linux x64/arm64, macOS x64/arm64, and Windows x64. Each archive contains `kandev`, the host `agentctl`, and required remote agentctl helpers; the workflow produces an adjacent checksum for each archive.
4. **Build desktop.** Embed the matching runtime and package the same five platform/architecture targets into macOS, Linux, and Windows installer formats.
5. **Build containers.** Publish amd64/arm64 base manifests, enforce the universal-image size gate, then publish multi-architecture universal images.
+1
View File
@@ -264,6 +264,7 @@ System pages (Radarr/Sonarr-style) for status, disk usage, database maintenance,
| [session-subscription-recovery](session-subscription-recovery/spec.md) | draft |
| [npm-nightly-channel](npm-nightly-channel/spec.md) | shipped |
| [scoop-release-automation](scoop-release-automation/spec.md) | shipped |
| [release-pr-queue-bypass](release-pr-queue-bypass/spec.md) | shipped |
| [agent-resume-runtime-recovery](agent-resume-runtime-recovery/spec.md) | shipped |
| [agent-stall-recovery](agent-stall-recovery/spec.md) | approved |
| [mcp-session-observability](mcp-session-observability/spec.md) | approved |
@@ -0,0 +1,55 @@
---
status: shipped
created: 2026-08-17
owner: Kandev maintainers
---
# Stable release PR queue bypass
## Why
Stable releases create a mechanical version commit and must continue after that commit reaches `main`. The merge queue now blocks this release commit on CI that maintainers do not require for generated release changes.
## What
- A normal Stable release creates a release branch and a pull request before it changes `main`.
- The release pull request merges immediately without waiting for pull-request checks or the merge queue.
- A fine-grained personal access token from an organization administrator performs only the privileged pull-request merge.
- The normal `GITHUB_TOKEN` remains responsible for the release branch and pull-request creation.
- The merge operation matches the expected release head commit and uses a squash merge.
- The release pull-request title satisfies the repository title contract.
- The workflow creates the signed tag only after GitHub reports that the pull request merged.
- The signed tag targets the reported release merge commit, even when another commit reaches `main` immediately afterward.
- Dry runs, Desktop validation, Nightly releases, and `backfill_tag` runs do not request the bypass token.
## Permissions
- The token selects only the `kdlbs/kandev` repository and has `contents: write` permission.
- The token owner remains an organization administrator while the token is active.
- The existing organization-administrator entry in the `main` ruleset provides the bypass.
- The protected `release` environment owns the `RELEASE_PR_BYPASS_TOKEN` secret.
- The workflow exposes the token only to the privileged merge step.
## Failure modes
- A missing, expired, revoked, or invalid token stops the release before the pull request merges.
- A token owner without organization-administrator bypass access causes the merge step to fail closed.
- A changed pull-request head causes the matched-head merge to fail closed.
- A closed or unmerged pull request stops the release before tag signing.
- A merge commit that is absent from `origin/main` stops the release before tag signing.
## Scenarios
- **GIVEN** a valid administrator token, **WHEN** a normal Stable release creates its pull request, **THEN** it merges that exact head without CI or queue enrollment.
- **GIVEN** the release pull request merges, **WHEN** another commit reaches `main`, **THEN** the signed release tag still targets the reported release merge commit.
- **GIVEN** the token owner lacks bypass access, **WHEN** the workflow requests the privileged merge, **THEN** the workflow stops before tag creation and publication.
- **GIVEN** the release branch changes after pull-request creation, **WHEN** the workflow requests the privileged merge, **THEN** the head-match guard rejects the merge.
- **GIVEN** an excluded release mode, **WHEN** the prepare job runs, **THEN** it does not use the administrator token.
## Out of scope
- Skipping CI for ordinary pull requests.
- Granting the release workflow direct-push access to `main`.
- Changing merge-queue checks or their workflow triggers.
- Automating personal access token creation or rotation.
- Recovering a release pull request after its originating workflow has stopped.