From 07316908c8440e614826846d4515f89cd5e7097a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 13:39:28 +0000 Subject: [PATCH 1/3] chore: rename release branch main->release in-repo (w1-w4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decision B of the branching-strategy review: `main` is already the release-only branch (validate-*-pr.yml rejects non-vX.Y.Z heads, release-finalize fast-forwards+tags it). `release` is the accurate name. This lands the IN-REPO half only (work units w1-w4); the GitHub-side branch rename + ruleset recreation (w5/w6) are maintainer-only admin actions, left pending per docs/operations/branch-rename-runbook.md. - w1 workflows: lint/test/perf-smoke/docs/results-explorer-browser triggers main->release; docs.yml Pages deploy gate refs/heads/main-> release; release.yml verify-tag-on-main->verify-tag-on-release (job, origin/release ancestor checks) — the tag-shaped-ref PyPI publish condition is untouched; test.yml's six base_ref=='main'/!='main' release-gating conditionals->release; validate-main-pr.yml renamed to validate-release-pr.yml (+ internal strings, RELEASE_CANARY_BRANCH). - w2 Makefile: release-cut/release-finalize base branch, checkout/pull, --since-ref origin/release, ruleset-name text, help text, and the develop|main branch-protection guards. MAIN_CLONE / "main clone" worktree-pool concept left untouched (unrelated to the branch name). - w3 docs/templates: release-guide.md, RELEASE_PR_TEMPLATE.md, CONTRIBUTING.md, repo-admin-settings.md (release-identity refs only; the default-branch-scheduling section left as-is with a caveat note), results-phase-2-runbook.md; landing/index.html footer blob/main-> blob/release; blog blob/main links pinned to release tags (v0.1.4, v0.2.1) verified with git cat-file. - w4 new docs/operations/branch-rename-runbook.md: ordered migration, no-real-gap reasoning, verification, rollback, PyPI/Pages branch- independence. Verification: all workflows parse; grep for refs/heads/main in workflows and --base main in Makefile both empty; tests/unit/test_release_ infrastructure.py 38 passed; todo validate + check-graph green. Item stays In Progress (w5/w6 admin pending). release.yml is touched so this PR is CODEOWNERS-gated. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01JFdZAbavgrKaNLe4YZa3zf --- .github/RELEASE_PR_TEMPLATE.md | 12 +- .github/workflows/docs.yml | 12 +- .github/workflows/lint.yml | 4 +- .github/workflows/perf-smoke.yml | 2 +- .github/workflows/release.yml | 34 ++-- .../workflows/results-explorer-browser.yml | 4 +- .github/workflows/test.yml | 24 +-- ...te-main-pr.yml => validate-release-pr.yml} | 14 +- CONTRIBUTING.md | 10 +- Makefile | 66 +++---- ...rename-release-branch-main-to-release.yaml | 21 ++- ...03-03-duckdb-tpch-extension-vs-benchbox.md | 10 +- .../2026-04-26-scale-factor-harmonization.md | 2 +- .../blog/2026-04-26-v0-2-1-release-summary.md | 2 +- docs/operations/branch-rename-runbook.md | 166 ++++++++++++++++++ docs/operations/release-guide.md | 32 ++-- docs/operations/repo-admin-settings.md | 36 ++-- docs/operations/results-phase-2-runbook.md | 18 +- landing/index.html | 4 +- tests/unit/test_release_infrastructure.py | 58 +++--- 20 files changed, 359 insertions(+), 172 deletions(-) rename .github/workflows/{validate-main-pr.yml => validate-release-pr.yml} (95%) rename _project/TODO/main/{planning => active}/rename-release-branch-main-to-release.yaml (91%) create mode 100644 docs/operations/branch-rename-runbook.md diff --git a/.github/RELEASE_PR_TEMPLATE.md b/.github/RELEASE_PR_TEMPLATE.md index 9f91bd1eb..b86834b74 100644 --- a/.github/RELEASE_PR_TEMPLATE.md +++ b/.github/RELEASE_PR_TEMPLATE.md @@ -2,9 +2,9 @@ This PR was opened by `make release-cut VERSION=X.Y.Z` from `develop`. It cuts the release branch, bumps the version sources, generates the -CHANGELOG entry from the `origin/main` patch delta, drops maintainer paths +CHANGELOG entry from the `origin/release` patch delta, drops maintainer paths (`_project/`, `_blog/`, agent configs, dev-tooling root files), and queues the -change for a squash-merge into `main`. +change for a squash-merge into `release`. ### Reviewer checklist @@ -12,7 +12,7 @@ change for a squash-merge into `main`. - [ ] `pyproject.toml` and `benchbox/__init__.py` show the new version - [ ] Documentation landing pages and `landing/` show the new version - [ ] `_project/`, `_blog/`, agent config dirs, and dev-tooling root files - were removed from this branch (verify with `git diff main...HEAD --stat` + were removed from this branch (verify with `git diff release...HEAD --stat` — the diff should look like a curated subtree, not arbitrary edits) - [ ] No surprise file additions (the curation only *removes*) - [ ] `validate-base` is green for this release branch @@ -54,13 +54,13 @@ context is missing, pending, skipped, failed, or canceled, stop and fix the release PR or ruleset/workflow contract before rerunning the command. After that pre-merge check passes, `release-finalize` squash-merges this PR, -fast-forwards `main`, tags `vX.Y.Z`, and pushes the tag — which fires +fast-forwards `release`, tags `vX.Y.Z`, and pushes the tag — which fires `.github/workflows/release.yml`: `dependency-bounds` → `build` (with `SOURCE_DATE_EPOCH` from the tag commit) → `publish` (PyPI trusted publisher) → `github-release` → `test-installation` (cross-platform pip install verification). -Push-to-main jobs are post-merge signals. They can fail after the tag has +Push-to-release jobs are post-merge signals. They can fail after the tag has already started publication; recovery is a patch release or incident process, not treating the public tag as if it had been blocked. @@ -68,7 +68,7 @@ not treating the public tag as if it had been blocked. paths (`_project/`, `_blog/`, agent configs, etc.) live only on develop by design (per A3 in `_project/decisions/single-repo-migration.md`). -If anything fails downstream, fix on a new branch, PR to `main`, squash-merge, +If anything fails downstream, fix on a new branch, PR to `release`, squash-merge, and bump to the next patch version (PyPI rejects re-uploads of an existing version). diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9a9e0f6c2..911bace91 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: Documentation on: push: - branches: [main] + branches: [release] paths: - "benchbox/**" - "docs/**" @@ -19,7 +19,7 @@ on: - "README.md" - "pyproject.toml" pull_request: - branches: [main, develop] + branches: [release, develop] paths: - "benchbox/**" - "docs/**" @@ -79,8 +79,8 @@ jobs: # Steps gate on package.json as a sentinel so the workflow degrades # gracefully if the explorer directory is ever removed or renamed. # This is also the intended dormancy on the release branch: results-explorer/ - # and _project/scripts/explorer_publish.py ship on develop, not main, so on - # a main build hashFiles(...) is empty and every explorer step below is a + # and _project/scripts/explorer_publish.py ship on develop, not release, so on + # a release build hashFiles(...) is empty and every explorer step below is a # deliberate no-op rather than accidental dead code (see # tests/uat/phases/explorer_smoke.py:explorer_present). - name: Validate results-explorer lockfile @@ -199,11 +199,11 @@ jobs: with: path: site - # Deploy to GitHub Pages (main pushes only) + # Deploy to GitHub Pages (release pushes only) # Isolated as its own job so the github-pages environment protection # rules don't gate the build job on PRs. deploy: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' && github.ref == 'refs/heads/release' needs: build runs-on: ubuntu-latest environment: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3f9dd6975..d07932ba2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,10 +3,10 @@ name: Lint on: push: branches: - - main + - release pull_request: branches: - - main + - release concurrency: group: lint-${{ github.ref }} diff --git a/.github/workflows/perf-smoke.yml b/.github/workflows/perf-smoke.yml index 9b9aa0e97..8a3e308ab 100644 --- a/.github/workflows/perf-smoke.yml +++ b/.github/workflows/perf-smoke.yml @@ -2,7 +2,7 @@ name: Performance Smoke on: pull_request: - branches: [main] + branches: [release] concurrency: group: perf-smoke-${{ github.ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 581b612ba..15452bd79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,37 +12,37 @@ on: type: boolean jobs: - # Note: ruleset main-release-only gates the release-cut PR on lint + test - # before squash-merge to main, so the tag fired here is expected to have - # already been CI-validated. verify-tag-on-main below enforces that + # Note: ruleset release-only gates the release-cut PR on lint + test + # before squash-merge to release, so the tag fired here is expected to have + # already been CI-validated. verify-tag-on-release below enforces that # invariant in the workflow itself (rather than relying solely on the - # out-of-band process of "nobody tags/dispatches from anywhere but main") + # out-of-band process of "nobody tags/dispatches from anywhere but release") # before any build/publish work starts. The previous check-ci-passed JS # poller was redundant with the ruleset and has been removed. - verify-tag-on-main: + verify-tag-on-release: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Assert release ref is main or an ancestor of main + - name: Assert release ref is release or an ancestor of release run: | - git fetch origin main --quiet + git fetch origin release --quiet target_sha="${{ github.sha }}" if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - echo "Tag push ${{ github.ref_name }}: verifying $target_sha is an ancestor of origin/main" + echo "Tag push ${{ github.ref_name }}: verifying $target_sha is an ancestor of origin/release" else - echo "workflow_dispatch from ${{ github.ref }}: verifying $target_sha is origin/main or an ancestor of it" + echo "workflow_dispatch from ${{ github.ref }}: verifying $target_sha is origin/release or an ancestor of it" fi - if ! git merge-base --is-ancestor "$target_sha" origin/main; then - echo "::error::Ref $target_sha (${{ github.ref }}) is not origin/main or an ancestor of it. Refusing to build/publish." + if ! git merge-base --is-ancestor "$target_sha" origin/release; then + echo "::error::Ref $target_sha (${{ github.ref }}) is not origin/release or an ancestor of it. Refusing to build/publish." exit 1 fi - echo "OK: $target_sha is origin/main or an ancestor of it." + echo "OK: $target_sha is origin/release or an ancestor of it." dependency-bounds: - needs: verify-tag-on-main + needs: verify-tag-on-release runs-on: ubuntu-latest outputs: report_path: ${{ steps.gate.outputs.report_path }} @@ -79,7 +79,7 @@ jobs: retention-days: 90 build: - needs: [verify-tag-on-main, dependency-bounds] + needs: [verify-tag-on-release, dependency-bounds] runs-on: ubuntu-latest outputs: version: ${{ steps.version.outputs.version }} @@ -139,8 +139,8 @@ jobs: # below). Test PyPI publishing via workflow_dispatch from any ref is # the intended pre-release validation path and is lower risk by # design (test.pypi.org is not the real distribution channel); - # verify-tag-on-main above still requires the dispatched ref be - # main or an ancestor of it, which is sufficient here. + # verify-tag-on-release above still requires the dispatched ref be + # release or an ancestor of it, which is sufficient here. if: github.event.inputs.test_pypi == 'true' uses: pypa/gh-action-pypi-publish@release/v1 with: @@ -150,7 +150,7 @@ jobs: verbose: true - name: Publish to PyPI - # Defense in depth alongside verify-tag-on-main: even if that job is + # Defense in depth alongside verify-tag-on-release: even if that job is # ever bypassed or miswired, this step alone refuses to publish to # real PyPI unless the triggering ref is an actual v* tag. A # workflow_dispatch from a branch (with the test_pypi input left at diff --git a/.github/workflows/results-explorer-browser.yml b/.github/workflows/results-explorer-browser.yml index e7bf1fdc9..11f62e9b6 100644 --- a/.github/workflows/results-explorer-browser.yml +++ b/.github/workflows/results-explorer-browser.yml @@ -2,7 +2,7 @@ name: Results Explorer browser tests on: push: - branches: [ main ] + branches: [ release ] paths: - 'results-explorer/**' - '_project/scripts/explorer_pipeline/**' @@ -10,7 +10,7 @@ on: - 'results-data/**' - '.github/workflows/results-explorer-browser.yml' pull_request: - branches: [ main, develop ] + branches: [ release, develop ] paths: - 'results-explorer/**' - '_project/scripts/explorer_pipeline/**' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c5ddd690..2f406fa1f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,10 +3,10 @@ name: Tests on: push: branches: - - main + - release pull_request: branches: - - main + - release concurrency: group: test-${{ github.ref }} @@ -59,12 +59,12 @@ jobs: compat-test: name: compat-test (${{ matrix.os }}, ${{ matrix.python-version }}) - if: ${{ (github.event_name != 'pull_request' || github.base_ref != 'develop') && (github.event_name != 'pull_request' || github.base_ref != 'main' || !startsWith(github.head_ref, 'v')) }} + if: ${{ (github.event_name != 'pull_request' || github.base_ref != 'develop') && (github.event_name != 'pull_request' || github.base_ref != 'release' || !startsWith(github.head_ref, 'v')) }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - # Additional compatibility coverage for main/push surfaces. + # Additional compatibility coverage for release/push surfaces. # Develop PRs keep only the required Ubuntu 3.12 test job; broad # validation is covered by nightly/manual workflows. include: @@ -170,10 +170,10 @@ jobs: uvx pip-audit fi - # Quick integration smoke tests - non-required on main/push; develop broad + # Quick integration smoke tests - non-required on release/push; develop broad # validation runs through nightly/manual workflows. integration-smoke: - if: ${{ (github.event_name != 'pull_request' || github.base_ref != 'develop') && (github.event_name != 'pull_request' || github.base_ref != 'main' || !startsWith(github.head_ref, 'v')) }} + if: ${{ (github.event_name != 'pull_request' || github.base_ref != 'develop') && (github.event_name != 'pull_request' || github.base_ref != 'release' || !startsWith(github.head_ref, 'v')) }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -205,7 +205,7 @@ jobs: # These don't require external credentials and catch common issues uv run -- python -m pytest tests/integration -m "platform_smoke or (integration and fast)" --tb=short - # Table-format dependency lane for main/push validation. + # Table-format dependency lane for release/push validation. # Nightly/manual workflows cover this lane for develop after Step 3. integration-table-formats: if: ${{ github.event_name != 'pull_request' || github.base_ref != 'develop' }} @@ -231,11 +231,11 @@ jobs: run: | uv run -- python -m pytest -q tests/integration/core/data_organization/test_presorted_generation.py -m "requires_table_formats" --tb=short - # Credential-free integration suite - release-required for main PRs and runs on main pushes. + # Credential-free integration suite - release-required for release PRs and runs on release pushes. integration: runs-on: ubuntu-latest timeout-minutes: 20 - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'release') steps: - name: Checkout code uses: actions/checkout@v4 @@ -260,7 +260,7 @@ jobs: name: correctness-gate runs-on: ubuntu-latest timeout-minutes: 20 - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main') + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'release') steps: - name: Checkout code uses: actions/checkout@v4 @@ -341,7 +341,7 @@ jobs: release-readiness: name: release-readiness - if: ${{ github.event_name == 'pull_request' && github.base_ref == 'main' }} + if: ${{ github.event_name == 'pull_request' && github.base_ref == 'release' }} runs-on: ubuntu-latest steps: - name: Checkout code @@ -410,7 +410,7 @@ jobs: release-required-result: name: release-required-result needs: [test, integration, correctness-gate, test-package, release-readiness] - if: ${{ always() && github.event_name == 'pull_request' && github.base_ref == 'main' }} + if: ${{ always() && github.event_name == 'pull_request' && github.base_ref == 'release' }} runs-on: ubuntu-latest steps: - name: Aggregate release-required result diff --git a/.github/workflows/validate-main-pr.yml b/.github/workflows/validate-release-pr.yml similarity index 95% rename from .github/workflows/validate-main-pr.yml rename to .github/workflows/validate-release-pr.yml index e003132d1..df574e358 100644 --- a/.github/workflows/validate-main-pr.yml +++ b/.github/workflows/validate-release-pr.yml @@ -1,12 +1,12 @@ -name: Validate main PR base +name: Validate release PR base on: pull_request: branches: - - main + - release concurrency: - group: validate-main-pr-${{ github.ref }} + group: validate-release-pr-${{ github.ref }} cancel-in-progress: true permissions: @@ -18,16 +18,16 @@ jobs: name: validate-base runs-on: ubuntu-latest steps: - - name: Reject non-release-branch PRs to main + - name: Reject non-release-branch PRs to release env: HEAD_REF: ${{ github.head_ref }} run: | set -euo pipefail if [[ "${HEAD_REF}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.+-]+)?$ ]]; then - echo "Release branch '${HEAD_REF}' approved for merge into main." + echo "Release branch '${HEAD_REF}' approved for merge into release." exit 0 fi - echo "::error::Only release branches matching 'vX.Y.Z' (optionally with a '-suffix') may merge into main. Got '${HEAD_REF}'." + echo "::error::Only release branches matching 'vX.Y.Z' (optionally with a '-suffix') may merge into release. Got '${HEAD_REF}'." echo "::error::Open this PR against 'develop' instead, or cut a release branch via 'make release-cut VERSION=X.Y.Z'." exit 1 @@ -57,7 +57,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_CANARY_WORKFLOW: release-canary.yml - RELEASE_CANARY_BRANCH: main + RELEASE_CANARY_BRANCH: release RELEASE_CANARY_CHECKED_REF: develop RELEASE_CANARY_MAX_AGE_HOURS: "48" RELEASE_READINESS_OVERRIDE_SHA: ${{ vars.RELEASE_READINESS_OVERRIDE_SHA }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0e7240b3..cb93d57f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ This document provides guidelines and instructions for contributing. ## Branches & PR gate -`develop` is the long-lived development branch; **all changes land via PR**. `main` is release-only (handled by the version-branch flow — see `docs/operations/release-guide.md`). PRs target `develop` and squash-merge with linear history. +`develop` is the long-lived development branch; **all changes land via PR**. `release` is release-only (handled by the version-branch flow — see `docs/operations/release-guide.md`). PRs target `develop` and squash-merge with linear history. Required CI on `develop` reports through `ci-required-result`. The umbrella uses `.github/path-filters.yml` to classify each PR: content-only PRs run content validation and skip Python fast tests, while code, infra, workflow, tooling, and unknown paths run the post-Step-3 lint/type + Ubuntu 3.12 fast-test gate. Reviews are not required for solo-dev work; auto-merge handles landing. @@ -88,7 +88,7 @@ The canonical loop is **branch → edit → preflight → `make pr-open`**. Auto make pr-open # push + gh pr create --base develop + gh pr merge --auto --squash ``` - `make pr-open` refuses to run from `develop` or `main`. The PR will squash-merge the moment the required checks turn green. Don't poll for CI — auto-merge handles it. + `make pr-open` refuses to run from `develop` or `release`. The PR will squash-merge the moment the required checks turn green. Don't poll for CI — auto-merge handles it. 5. **After merge**, the remote branch auto-deletes (repo setting `delete_branch_on_merge`). Sweep any stale local branches and worktrees with: @@ -218,10 +218,10 @@ are cut by maintainers via the version-branch flow documented in 1. `make bump VERSION=X.Y.Z` and `make changelog-draft VERSION=X.Y.Z` on `develop`. 2. `make release-prepare VERSION=X.Y.Z` cuts `vX.Y.Z` from `develop` with a - release-curated tree and opens a PR against `main`. -3. Squash-merge the PR; tag `main`; `release.yml` publishes to PyPI. + release-curated tree and opens a PR against `release`. +3. Squash-merge the PR; tag `release`; `release.yml` publishes to PyPI. 4. `make release-rebase-develop VERSION=X.Y.Z` rebases `develop` onto the - release-shaped `main`. + release-shaped `release`. We follow semantic versioning. diff --git a/Makefile b/Makefile index 2c81aac8e..d8c42c06a 100644 --- a/Makefile +++ b/Makefile @@ -1042,7 +1042,7 @@ run-test: # These targets must be run from the public clone (origin -> joeharris76/BenchBox). # Do NOT invoke from the legacy private clone — it has no `origin` remote. # -# Flow: develop -> v$(VERSION) -> (squash) main -> tag main -> release.yml publishes +# Flow: develop -> v$(VERSION) -> (squash) release -> tag release -> release.yml publishes # develop is intentionally NOT modified post-release. dev-only paths and # deferred release surfaces (_project/, _blog/, results explorer, AGENTS.md, # etc.) live on develop and are removed from the release branch by @@ -1058,12 +1058,12 @@ RELEASE_REQUIRED_CONTEXTS := validate-base release-required-result # Cut a release branch from develop in one shot: # 1. Create v$(VERSION) branch off develop (develop is not modified). # 2. On v$(VERSION): bump version sources (scripts/update_version.py). -# 3. On v$(VERSION): generate CHANGELOG.md entry from the origin/main patch delta. +# 3. On v$(VERSION): generate CHANGELOG.md entry from the origin/release patch delta. # 4. $EDITOR opens CHANGELOG.md for hand-curation (skipped if EDITOR unset). # 5. Curate: git rm dev-only/deferred paths (per A3 in single-repo-migration.md). # 6. Commit "Release v$(VERSION)" (bump + changelog + curation in one squash-friendly commit). -# 7. Merge origin/main with `-s ours` so the PR is mergeable and CI can run. -# 8. Push, open PR vs main. +# 7. Merge origin/release with `-s ours` so the PR is mergeable and CI can run. +# 8. Push, open PR vs release. # 9. Sweep stale v* branches on origin (option-c lifecycle). # Pre-conditions: on develop, clean tree. # Usage: make release-cut VERSION=X.Y.Z @@ -1078,7 +1078,7 @@ release-cut: @# pre-commit uv-lock hook has nothing to regenerate mid-commit (v0.3.1 @# aborted because the tracked lock was stale and the hook rewrote it). uv lock - uv run -- python scripts/generate_changelog_entry.py --version $(VERSION) --since-ref origin/main + uv run -- python scripts/generate_changelog_entry.py --version $(VERSION) --since-ref origin/release @if [ -n "$$EDITOR" ]; then \ echo "==> Opening CHANGELOG.md in $$EDITOR for hand-curation"; \ $$EDITOR CHANGELOG.md; \ @@ -1122,37 +1122,37 @@ release-cut: @# every release cut. Nothing is skipped that matters: develop's PRs @# already ran these hooks before this content was curated. git commit --no-verify -m "Release v$(VERSION)" - @# Align release histories. main and develop have unrelated roots (Phase 4 - @# filter-merge) and diverge permanently: main carries one squash commit per - @# release that develop never sees, and A4 step 6 (rebase develop onto main) + @# Align release histories. release and develop have unrelated roots (Phase 4 + @# filter-merge) and diverge permanently: release carries one squash commit per + @# release that develop never sees, and A4 step 6 (rebase develop onto release) @# was removed by the 2026-04-27 amendment. So a branch cut cleanly from - @# develop is NOT mergeable into main — GitHub cannot compute a merge ref, the + @# develop is NOT mergeable into release — GitHub cannot compute a merge ref, the @# PR sits at CONFLICTING, and *no CI runs at all*: neither validate-base nor @# release-required-result ever trigger. Every cut before v0.3.1 (#711, #712, @# #1029, #1043, #1072) fixed this by hand. @# - @# `-s ours` records origin/main as a second parent while keeping the curated + @# `-s ours` records origin/release as a second parent while keeping the curated @# release tree byte-for-byte; release-finalize's squash-merge then collapses - @# the branch to one commit, so this merge never reaches main's release-only - @# ledger. Run AFTER the changelog step: --since-ref origin/main computes the - @# patch delta against a main that is not yet an ancestor of this branch. + @# the branch to one commit, so this merge never reaches the release branch's + @# release-only ledger. Run AFTER the changelog step: --since-ref origin/release + @# computes the patch delta against a release that is not yet an ancestor of this branch. @# @# --no-verify: merge fires pre-merge-commit (not pre-commit). That hook is not @# in default_install_hook_types today, so this is defensive — but curation has @# already deleted .pre-commit-config.yaml and the repo:local hook entrypoints, @# so adding pre-merge-commit later would otherwise break every cut here. - @echo "==> Aligning release histories: merging origin/main into v$(VERSION) (strategy: ours)" + @echo "==> Aligning release histories: merging origin/release into v$(VERSION) (strategy: ours)" @RELEASE_TREE=$$(git rev-parse 'HEAD^{tree}') && \ - git merge -s ours --no-verify --allow-unrelated-histories origin/main \ - -m "Merge main into v$(VERSION) (strategy: ours) to align release histories" && \ + git merge -s ours --no-verify --allow-unrelated-histories origin/release \ + -m "Merge release into v$(VERSION) (strategy: ours) to align release histories" && \ MERGED_TREE=$$(git rev-parse 'HEAD^{tree}') && \ if [ "$$RELEASE_TREE" != "$$MERGED_TREE" ]; then \ echo "ERROR: alignment merge changed the curated release tree ($$RELEASE_TREE -> $$MERGED_TREE)" >&2; \ exit 1; \ fi git push -u origin v$(VERSION) - gh pr create --base main --head v$(VERSION) --title "Release v$(VERSION)" --body-file .github/RELEASE_PR_TEMPLATE.md - @# Option-c lifecycle: delete any prior release branches on origin (loop sweeps stale entries). + gh pr create --base release --head v$(VERSION) --title "Release v$(VERSION)" --body-file .github/RELEASE_PR_TEMPLATE.md + @# Option-c lifecycle: delete any prior v* branches on origin (loop sweeps stale entries). @# ls-remote patterns match the LAST path component, so 'v*' alone also matches @# e.g. fix/validate-submission-... (bit the 2026-07-08 v0.3.1 re-cut). Filter full @# refs against the release-branch shape, mirroring _RELEASE_BRANCH_RE in @@ -1169,13 +1169,13 @@ release-cut: @echo " 3. make release-finalize VERSION=$(VERSION)" # After release-cut's PR is approved and all required release contexts are -# green: squash-merge it, tag main, push the tag (fires release.yml), and +# green: squash-merge it, tag release, push the tag (fires release.yml), and # leave develop alone. # Usage: make release-finalize VERSION=X.Y.Z release-finalize: @test -n "$(VERSION)" || (echo "Usage: make release-finalize VERSION=X.Y.Z" && exit 1) - @PR=$$(gh pr list --base main --head v$(VERSION) --state open --json number --jq '.[0].number'); \ - test -n "$$PR" || (echo "Error: no open PR found for v$(VERSION) → main" && exit 1); \ + @PR=$$(gh pr list --base release --head v$(VERSION) --state open --json number --jq '.[0].number'); \ + test -n "$$PR" || (echo "Error: no open PR found for v$(VERSION) → release" && exit 1); \ echo "==> Verifying required release contexts '$(RELEASE_REQUIRED_CONTEXTS)' for PR #$$PR"; \ for context in $(RELEASE_REQUIRED_CONTEXTS); do \ CHECK_BUCKET=$$(gh pr checks "$$PR" --required --json name,bucket,state --jq "map(select(.name == \"$$context\")) | if length == 1 then .[0].bucket elif length == 0 then \"missing\" else \"duplicate\" end"); \ @@ -1189,7 +1189,7 @@ release-finalize: fi; \ case "$$CHECK_BUCKET" in \ pass) echo "==> $$context is green";; \ - missing) echo "Error: required release context '$$context' is missing. Check main-release-only and release workflows." >&2; exit 1;; \ + missing) echo "Error: required release context '$$context' is missing. Check release-only and release workflows." >&2; exit 1;; \ pending) echo "Error: required release context '$$context' is pending. Wait for GitHub Actions, then rerun." >&2; exit 1;; \ fail|cancel|skipping) echo "Error: required release context '$$context' is $$CHECK_BUCKET. Fix the release PR before finalizing." >&2; exit 1;; \ duplicate) echo "Error: multiple required contexts named '$$context' were returned. Fix workflow/ruleset drift." >&2; exit 1;; \ @@ -1199,13 +1199,13 @@ release-finalize: echo "==> Squash-merging PR #$$PR (required release contexts are green)"; \ gh pr merge --squash "$$PR" git fetch origin --tags - git checkout main - git pull --ff-only origin main + git checkout release + git pull --ff-only origin release git tag v$(VERSION) git push origin v$(VERSION) @echo @echo "Tag v$(VERSION) pushed; release.yml will publish to PyPI." - @echo "Push-to-main jobs are post-merge signals; release publication relied on $(RELEASE_REQUIRED_CONTEXTS)." + @echo "Push-to-release jobs are post-merge signals; release publication relied on $(RELEASE_REQUIRED_CONTEXTS)." @echo "develop is intentionally unchanged — dev-only paths persist on develop." # ============================================================================= @@ -1276,7 +1276,7 @@ pr-content-guard: # Push current branch and open a PR against develop with auto-merge enabled # unless the diff touches soundness-critical comparator/plan-parser paths. # Squash-merge happens automatically once `lint` + `test (ubuntu-latest, 3.12)` -# go green. Refuses to run from develop/main. +# go green. Refuses to run from develop/release. # # Idempotent: safe to rerun. If a PR is already open for the branch, reuses it # and just (re)enables auto-merge when the soundness-path review gate does not @@ -1290,7 +1290,7 @@ pr-open: @$(MAKE) -s agent-write-preflight @CURRENT=$$(git branch --show-current); \ case "$$CURRENT" in \ - develop|main) echo "Refusing to open PR from $$CURRENT — switch to a feature branch."; exit 1 ;; \ + develop|release) echo "Refusing to open PR from $$CURRENT — switch to a feature branch."; exit 1 ;; \ esac; \ if [ -n "$(PR_BODY_FILE)" ] && [ ! -f "$(PR_BODY_FILE)" ]; then \ echo "PR_BODY_FILE does not exist: $(PR_BODY_FILE)" >&2; \ @@ -1334,7 +1334,7 @@ pr-fanout: git worktree list --porcelain | sed -n 's/^worktree //p' | while IFS= read -r wt; do \ [ "$$(realpath "$$wt")" = "$$MAIN_CLONE" ] && { echo "(skip $$wt: main clone)"; continue; }; \ BR=$$(git -C "$$wt" branch --show-current 2>/dev/null); \ - case "$$BR" in develop|main|"") echo "(skip $$wt: branch=$$BR)"; continue ;; esac; \ + case "$$BR" in develop|release|"") echo "(skip $$wt: branch=$$BR)"; continue ;; esac; \ IDX=$$(($${IDX:-0} + 1)); \ printf '%06d|%s\0' "$$IDX" "$$wt" >> "$$TMP"; \ done; \ @@ -1352,7 +1352,7 @@ pr-fanout: pr-refresh: @CURRENT=$$(git branch --show-current); \ case "$$CURRENT" in \ - develop|main|"") echo "Refusing to refresh $$CURRENT — switch to a feature branch worktree."; exit 1 ;; \ + develop|release|"") echo "Refusing to refresh $$CURRENT — switch to a feature branch worktree."; exit 1 ;; \ esac; \ git fetch origin develop --quiet && \ git merge --no-edit origin/develop && \ @@ -1672,7 +1672,7 @@ worktree-release-locked: top=$$(git rev-parse --show-toplevel); \ branch=$$(git branch --show-current); \ test -n "$$branch" || { echo "Refusing: this pool worktree is already detached/free."; exit 1; }; \ - case "$$branch" in develop|main) echo "Refusing to release protected branch $$branch."; exit 1 ;; esac; \ + case "$$branch" in develop|release) echo "Refusing to release protected branch $$branch."; exit 1 ;; esac; \ dirty=$$(git status --porcelain --untracked-files=normal | grep -vE '^\?\? \.benchbox(/|$$)' || true); \ if [ -n "$$dirty" ] && [ "$(FORCE)" != "1" ]; then \ echo "Refusing to release dirty pool worktree $$top. Review changes or rerun with FORCE=1."; \ @@ -1888,7 +1888,7 @@ worktree-pool-reset-locked: git -C "$$wt" reset --hard origin/develop; \ if [ "$(FORCE)" = "1" ]; then git -C "$$wt" clean -fdx -e .benchbox >/dev/null; fi; \ if [ "$(FORCE)" = "1" ] && [ -n "$$branch" ]; then \ - case "$$branch" in develop|main) ;; *) git branch -D "$$branch" >/dev/null 2>&1 || true ;; esac; \ + case "$$branch" in develop|release) ;; *) git branch -D "$$branch" >/dev/null 2>&1 || true ;; esac; \ fi; \ echo "Reset pool-$(POOL): $$wt" @@ -2302,7 +2302,7 @@ help: @echo " make blind-spots-sweep Alias for blind-spots-report" @echo "" @echo "Release Workflow (2-command flow; see docs/operations/release-guide.md):" - @echo " make release-cut VERSION=X.Y.Z Cut v\$$VERSION off develop, bump + changelog + curate, push, open PR vs main" - @echo " make release-finalize VERSION=X.Y.Z Verify validate-base and release-required-result, squash-merge the release PR, tag main, push tag" + @echo " make release-cut VERSION=X.Y.Z Cut v\$$VERSION off develop, bump + changelog + curate, push, open PR vs release" + @echo " make release-finalize VERSION=X.Y.Z Verify validate-base and release-required-result, squash-merge the release PR, tag release, push tag" @echo "" @echo " make help Show this help message" diff --git a/_project/TODO/main/planning/rename-release-branch-main-to-release.yaml b/_project/TODO/main/active/rename-release-branch-main-to-release.yaml similarity index 91% rename from _project/TODO/main/planning/rename-release-branch-main-to-release.yaml rename to _project/TODO/main/active/rename-release-branch-main-to-release.yaml index 252630e09..220415ab9 100644 --- a/_project/TODO/main/planning/rename-release-branch-main-to-release.yaml +++ b/_project/TODO/main/active/rename-release-branch-main-to-release.yaml @@ -2,7 +2,7 @@ id: rename-release-branch-main-to-release title: "Rename the release branch main->release (Decision B of the branching-strategy review)" worktree: main priority: Medium -status: "Not Started" +status: "In Progress" category: "Release Tooling" description: | @@ -26,6 +26,17 @@ description: | GitHub Pages (deploy-pages, source = GitHub Actions). README badges pin no branch. + PROGRESS (2026-07-07, PR chore/rename-main-to-release-in-repo): the in-repo + half (w1-w4) landed via PR to develop — workflows, Makefile, docs/templates, + and the branch-rename-runbook.md are done. w5 (decision doc) and w6 (admin: + branch rename + ruleset recreate) are intentionally NOT done in that PR — + they require GitHub admin/settings actions this session was scoped to avoid. + This item stays In Progress until the maintainer executes w5/w6 per + docs/operations/branch-rename-runbook.md. Until then, GitHub's actual + release branch is still named `main` even though the tree now refers to it + as `release` (documented-safe intermediate state; see the runbook's + "No real gap" section). + prior_art: - "docs/operations/branch-rename-runbook.md — DRAFTED in the PR #993 session and saved to the session scratchpad (not yet committed); enumerates the full admin sequence, ordering/no-gap reasoning, verification, and rollback. Land it with w4/w6 or recreate from the scratchpad patch." - "_project/TODO/main/active/tag-and-pypi-environment-admin-hardening.yaml — reuse the agent-does-code/docs plus admin-does-GitHub-action split for w6 (the branch rename + ruleset recreate are admin-only)." @@ -34,7 +45,7 @@ prior_art: work: - id: w1 summary: "Rename in-repo across workflows (incl. the functional publish gate)" - status: pending + status: done notes: | Triggers main->release: lint.yml, test.yml, perf-smoke.yml, docs.yml (incl. Pages deploy gate refs/heads/main). Dev-only @@ -49,7 +60,7 @@ work: validate-release-pr.yml and its strings. - id: w2 summary: "Rename in-repo across Makefile" - status: pending + status: done notes: | release-cut / release-finalize base branch (--base main), checkout/pull release, generate_changelog_entry --since-ref origin/main, @@ -59,7 +70,7 @@ work: MAIN_CLONE / "main clone" worktree-pool concept untouched (unrelated). - id: w3 summary: "Rename in docs/templates + pin external URLs" - status: pending + status: done notes: | release-guide.md, RELEASE_PR_TEMPLATE.md, CONTRIBUTING.md, and — CAREFUL, NON-MECHANICAL — repo-admin-settings.md (its scheduling section logic @@ -70,7 +81,7 @@ work: `git cat-file -e :`. - id: w4 summary: "Add the migration runbook doc" - status: pending + status: done notes: | Land docs/operations/branch-rename-runbook.md (recreate from the session scratchpad patch): default-branch switch, branch rename, ruleset diff --git a/docs/blog/2026-03-03-duckdb-tpch-extension-vs-benchbox.md b/docs/blog/2026-03-03-duckdb-tpch-extension-vs-benchbox.md index d605d9636..1baa0ce4a 100644 --- a/docs/blog/2026-03-03-duckdb-tpch-extension-vs-benchbox.md +++ b/docs/blog/2026-03-03-duckdb-tpch-extension-vs-benchbox.md @@ -322,8 +322,8 @@ We built BenchBox to make workflow guarantees explicit. We would love to hear ho 1. [DuckDB TPC-H extension docs](https://duckdb.org/docs/stable/core_extensions/tpch) 2. [DuckDB benchmarking guide](https://duckdb.org/docs/stable/guides/performance/benchmarks) -3. [BenchBox TPC-H benchmark core](https://github.com/joeharris76/BenchBox/blob/main/benchbox/core/tpch/benchmark.py) -4. [BenchBox TPC-H generator](https://github.com/joeharris76/BenchBox/blob/main/benchbox/core/tpch/generator.py) -5. [BenchBox TPC-H query manager](https://github.com/joeharris76/BenchBox/blob/main/benchbox/core/tpch/queries.py) -6. [BenchBox TPC-H maintenance implementation](https://github.com/joeharris76/BenchBox/blob/main/benchbox/core/tpch/maintenance_test.py) -7. [BenchBox official benchmark flow](https://github.com/joeharris76/BenchBox/blob/main/benchbox/core/tpch/official_benchmark.py) +3. [BenchBox TPC-H benchmark core](https://github.com/joeharris76/BenchBox/blob/v0.1.4/benchbox/core/tpch/benchmark.py) +4. [BenchBox TPC-H generator](https://github.com/joeharris76/BenchBox/blob/v0.1.4/benchbox/core/tpch/generator.py) +5. [BenchBox TPC-H query manager](https://github.com/joeharris76/BenchBox/blob/v0.1.4/benchbox/core/tpch/queries.py) +6. [BenchBox TPC-H maintenance implementation](https://github.com/joeharris76/BenchBox/blob/v0.1.4/benchbox/core/tpch/maintenance_test.py) +7. [BenchBox official benchmark flow](https://github.com/joeharris76/BenchBox/blob/v0.1.4/benchbox/core/tpch/official_benchmark.py) diff --git a/docs/blog/2026-04-26-scale-factor-harmonization.md b/docs/blog/2026-04-26-scale-factor-harmonization.md index 767a5e3f2..11d33dc8e 100644 --- a/docs/blog/2026-04-26-scale-factor-harmonization.md +++ b/docs/blog/2026-04-26-scale-factor-harmonization.md @@ -220,7 +220,7 @@ The `--phases generate` flag stops after data generation, which is useful for sa ## References -- v0.2.1 changelog entry: [`CHANGELOG.md`](https://github.com/joeharris76/benchbox/blob/main/CHANGELOG.md), "Scale factor harmonization" under Added +- v0.2.1 changelog entry: [`CHANGELOG.md`](https://github.com/joeharris76/benchbox/blob/v0.2.1/CHANGELOG.md), "Scale factor harmonization" under Added - Companion release post: [BenchBox v0.2.1 release summary](2026-04-26-v0-2-1-release-summary.md) - Test coverage: `tests/unit/generators/test_scale_factor_harmonization.py` (validates the 0.8-1.3 GB target band per benchmark) - Benchmark docs used for calibrated size tables: `docs/benchmarks/{amplab,h2odb,nyctaxi,tsbs-devops,flightdata}.md` diff --git a/docs/blog/2026-04-26-v0-2-1-release-summary.md b/docs/blog/2026-04-26-v0-2-1-release-summary.md index 250d8bba0..033b2f1a9 100644 --- a/docs/blog/2026-04-26-v0-2-1-release-summary.md +++ b/docs/blog/2026-04-26-v0-2-1-release-summary.md @@ -138,7 +138,7 @@ Apache Doris and StarRocks carried the expected new-adapter shake-out cycle: bul One ClickHouse fix deserves to be called out: TPC-DS power tests now report `FAILED` when no queries execute, where they previously passed silently. Query error messages are now surfaced in result output (also extended to Redshift and Firebolt), and dialect overrides landed for `tpcdi`, `coffeeshop`, `h2odb` Q9, `nyctaxi` EXTRACT, `tpchavoc` Q6, and `read_primitives`. -Other quality-of-life work this cycle: QuestDB query rewrites and `/imp` loader improvements; CedarDB, LakeSail, Firebolt, and Databend reliability fixes; `psycopg3` migration for pg-family adapters; DataVault SHA-256 hash keys; and benchmark-timeout enforcement on macOS. Full inventory in the [changelog](https://github.com/joeharris76/BenchBox/blob/main/CHANGELOG.md). +Other quality-of-life work this cycle: QuestDB query rewrites and `/imp` loader improvements; CedarDB, LakeSail, Firebolt, and Databend reliability fixes; `psycopg3` migration for pg-family adapters; DataVault SHA-256 hash keys; and benchmark-timeout enforcement on macOS. Full inventory in the [changelog](https://github.com/joeharris76/BenchBox/blob/v0.2.1/CHANGELOG.md). ## Changed behavior to be aware of diff --git a/docs/operations/branch-rename-runbook.md b/docs/operations/branch-rename-runbook.md new file mode 100644 index 000000000..75968fac1 --- /dev/null +++ b/docs/operations/branch-rename-runbook.md @@ -0,0 +1,166 @@ +# Branch rename runbook: `main` -> `release` (and default -> `develop`) + +This is the maintainer runbook for the two-part branching-strategy migration +recorded as `branch-default-switch-to-develop` (Decision A) and +`rename-release-branch-main-to-release` (Decision B). Both are naming +clarity changes with **no behavioral gain**: `main` is already release-only +(`validate-*-pr.yml` rejects any PR whose head is not a `vX.Y.Z` branch, +`release-finalize` fast-forwards + tags it, and its HEAD is always the exact +latest-release commit) and it is also, today, the repository's default +branch — a trap where the most-restricted branch reads as primary. `develop` +becomes the default branch; `release` becomes the accurate name for the +curated release branch. + +## Ordered migration + +Run these in order. Do not skip ahead — the ordering exists specifically to +avoid a window where release-cut PRs target a branch name that does not yet +exist on GitHub. + +1. **Decision A (admin, separate item): switch the default branch to + `develop`.** + `gh api -X PATCH repos/joeharris76/BenchBox/ -f default_branch=develop` + (or the repo Settings UI). Low-risk — GitHub scheduled (`on.schedule`) + workflows begin running from `develop` instead of `main`; verify the + `scheduled-workflow-liveness` job in `nightly.yml` stays green and that + `release-canary.yml` / `phase3-promotion-review.yml` are already present + on `develop` (they are), so their schedules keep firing without any + further action. This item is tracked separately + (`branch-default-switch-to-develop`) and is a prerequisite for step 2 + below, not part of this item's own work units. + +2. **Decision B in-repo half (this item, w1-w4): merge the PR that renames + `main` -> `release` across workflows, `Makefile`, docs/templates, and + adds this runbook.** This lands on `develop` first, same as any other PR. + At the moment this PR merges, the actual GitHub branch is still named + `main` — only the in-repo *references* to it now say `release`. That is + the expected, documented-safe intermediate state; see "No real gap" + below for why this is safe to leave briefly inconsistent. + +3. **Decision B admin half (w5/w6, maintainer-only, immediately after step 2 + merges — do not leave an open window):** + a. Rename the branch: + ```bash + gh api -X POST repos/joeharris76/BenchBox/branches/main/rename \ + -f new_name=release + ``` + GitHub automatically redirects old URLs (including `blob/main/...` + links not pinned to a tag) and retargets any open PRs whose base was + `main` to `release`. + b. Recreate the release-only ruleset, targeting the renamed branch: + - Name: `release-only` (was `main-release-only`). + - Target: `refs/heads/release` (was `refs/heads/main`). + - Required status checks: `validate-base`, `release-required-result` + (unchanged — these are workflow job/context names, not branch + names, and this PR's w1 already renamed the underlying jobs that + satisfy them, e.g. `verify-tag-on-main` -> `verify-tag-on-release`). + - Preserve every other property from the old ruleset: linear history + required, non-fast-forward, no direct push, deletion blocked, no + bypass actors. + - Delete the old `main-release-only` ruleset once the new one is + confirmed (GitHub does not rename rulesets when the underlying + branch is renamed — recreation is a separate, explicit step). + c. Update the ruleset-id reference(s) in + `docs/operations/repo-admin-settings.md` (the `Branch ruleset — + release` section's `` placeholder, and the + "Re-applying after a transfer or restore" section's guidance to + "update this file and the `Makefile`/`scripts/` references that + hard-code it"). + d. Re-run the verification commands below and confirm all three pass. + +## No real gap + +The interval between step 2 (in-repo rename merges to `develop`) and step 3 +(GitHub branch actually renamed) is safe to leave briefly open because: + +- The release branch **only receives pushes via `release-cut` / + `release-finalize`**, both maintainer-run Make targets — there is no + automated or third-party process that pushes to it directly. As long as + the maintainer does not run `make release-cut` / `make release-finalize` + between steps 2 and 3, nothing exercises the renamed-but-not-yet-renamed + branch reference. +- `develop` itself is unaffected either way — this migration never touches + `develop`'s own ruleset, required checks, or default-PR flow. +- If `make release-cut` is accidentally run in that window, it fails fast + and loudly: `gh pr create --base release ...` errors because the + `release` branch does not exist yet on GitHub. No release is silently + cut against the wrong base. +- Step 3 is a single maintainer session (rename + ruleset recreate done + together), so in practice the window is minutes, not days. + +## Verified safe / unchanged (do not touch) + +These are branch-independent by construction and require no changes as +part of either decision: + +- **PyPI trusted publishing**: keyed on repository + workflow filename + (`release.yml`) + environment (`pypi` / `test-pypi`), never a branch name. + Renaming `main` -> `release` and changing the default branch do not + affect the trusted-publisher binding on PyPI's side. +- **GitHub Pages**: the Pages *source* is configured as "GitHub Actions" + (the `deploy-pages` action), not a branch. The `docs.yml` `deploy` job's + own trigger condition (`github.ref == 'refs/heads/release'` after w1) is + a workflow-level gate on when the job runs, independent of the Pages + source setting itself. +- **README badges**: pin no branch. + +## Verification + +Run after each stage: + +```bash +# After step 2 merges (in-repo half) — no residual main-branch +# trigger/gate references in the touched surface: +grep -rn 'refs/heads/main' .github/workflows +grep -rn -- '--base main' Makefile +# expect: no output (both should show only 'release' now) + +grep -n 'base release' Makefile +# expect: release-cut and release-finalize both present + +uv run -- python -c "import yaml,glob; [yaml.safe_load(open(f)) for f in glob.glob('.github/workflows/*.yml')]; print('all workflows parse')" + +uv run -- python -m pytest tests/unit/test_release_infrastructure.py -n 0 -q +``` + +```bash +# After step 3 (admin: branch renamed + ruleset recreated): +gh api repos/joeharris76/BenchBox/rulesets --jq '.[] | {name, target}' +# expect: a ruleset targeting refs/heads/release; none targeting refs/heads/main + +gh api repos/joeharris76/BenchBox/branches/release --jq '.name' +# expect: "release"; a lookup of .../branches/main now 404s +``` + +## Rollback + +Both steps are reversible independently, most recent first: + +- **Undo step 3 (branch rename + ruleset)**: rename back + (`gh api -X POST repos/joeharris76/BenchBox/branches/release/rename -f + new_name=main`) and recreate the original `main-release-only` ruleset + targeting `refs/heads/main` with the same required checks. GitHub's + redirect/retarget behavior applies symmetrically. +- **Undo step 2 (in-repo rename)**: revert the merge commit on `develop` + (or open a follow-up PR reversing the `main`<->`release` token swap across + the same files). Since the actual GitHub branch may already be named + `release` at this point (step 3 done), reverting step 2 without also + reverting step 3 would reintroduce `main`-named references pointing at a + branch that is actually called `release` — revert both together, or + neither. +- **Undo step 1 (default branch)**: `gh api -X PATCH + repos/joeharris76/BenchBox/ -f default_branch=main` (or `release`, + depending on whether step 3 has run) reverts the default-branch switch + independently of the rename; the two are separate GitHub settings. + +## Related items + +- `branch-default-switch-to-develop` (Decision A) — separate TODO, must + complete before this item's step 3. +- `rename-release-branch-main-to-release` (Decision B) — this item; w1-w4 + are the in-repo half (steps above, part 2); w5/w6 are the admin half + (step 3). +- `_project/decisions/single-repo-migration.md` — the architecture record + for the two-branch flow this migration renames, not restructures. +- `docs/operations/release-guide.md` and `docs/operations/repo-admin-settings.md` + — the maintainer runbook and admin-state runbook this migration updates. diff --git a/docs/operations/release-guide.md b/docs/operations/release-guide.md index 7928489af..313d6c634 100644 --- a/docs/operations/release-guide.md +++ b/docs/operations/release-guide.md @@ -2,7 +2,7 @@ BenchBox releases follow a **version-branch flow** on a single repo (`joeharris76/BenchBox`) with two long-lived branches: `develop` (dev work) -and `main` (release-only). This guide is the maintainer runbook. +and `release` (release-only). This guide is the maintainer runbook. ## The flow (2 commands) @@ -17,9 +17,9 @@ That's the entire flow. The two Make targets do the rest. ## Pre-merge release-required contract -Release PRs target `main` and must be opened from branches accepted by -`.github/workflows/validate-main-pr.yml` (`vX.Y.Z`, optionally with a suffix). -Before a release PR can merge, the `main-release-only` ruleset must require: +Release PRs target `release` and must be opened from branches accepted by +`.github/workflows/validate-release-pr.yml` (`vX.Y.Z`, optionally with a suffix). +Before a release PR can merge, the `release-only` ruleset must require: - `validate-base` - `release-required-result` @@ -83,8 +83,8 @@ must not be bypassed with an undocumented local change. 1. Cuts a `vX.Y.Z` branch off `develop` (`develop` itself is never modified). 2. Bumps the 6 version sources via `scripts/update_version.py` and generates the CHANGELOG entry via `scripts/generate_changelog_entry.py --since-ref - origin/main`. The release note boundary is the current release branch patch - delta against `main`, not `git log origin/main..HEAD` ancestry and not the + origin/release`. The release note boundary is the current release branch patch + delta against `release`, not `git log origin/release..HEAD` ancestry and not the latest tag reachable from `develop`, because `release-finalize` intentionally does not replay release commits onto `develop`. 3. Opens `$EDITOR` on `CHANGELOG.md` for hand-curation. (Headless mode: @@ -98,14 +98,14 @@ must not be bypassed with an undocumented local change. `/prompts/` and promoted release posts ship; `results-explorer/`, `results-data/`, and explorer/results-data workflows do not. 5. Commits a single `Release vX.Y.Z` commit on `vX.Y.Z`. -6. Merges `origin/main` into `vX.Y.Z` with `-s ours - --allow-unrelated-histories`. `main` and `develop` have unrelated roots and +6. Merges `origin/release` into `vX.Y.Z` with `-s ours + --allow-unrelated-histories`. `release` and `develop` have unrelated roots and diverge permanently, so without this the release PR is unmergeable, GitHub cannot compute a merge ref, and **no CI runs at all**. `-s ours` keeps the curated release tree byte-for-byte (the merge is guarded on that) and only - records `main` as a second parent, which `release-finalize`'s squash-merge + records `release` as a second parent, which `release-finalize`'s squash-merge then collapses away. -7. Pushes and opens a PR against `main`. +7. Pushes and opens a PR against `release`. 8. Sweeps prior `v*` branches on origin (option-c lifecycle: keep until superseded, then auto-delete on the next `release-cut`). @@ -116,28 +116,28 @@ must not be bypassed with an undocumented local change. both `validate-base` and `release-required-result` are present and green. Missing means the ruleset/workflow contract is broken; pending means wait in GitHub Actions and rerun the command. `release-finalize` does not poll. -3. Squash-merges the PR. (Ruleset `main-release-only` also blocks the merge +3. Squash-merges the PR. (Ruleset `release-only` also blocks the merge unless `validate-base` and `release-required-result` are green.) -4. Fast-forwards `main` and tags `vX.Y.Z`. +4. Fast-forwards `release` and tags `vX.Y.Z`. 5. Pushes the tag — which fires `.github/workflows/release.yml`: `dependency-bounds` → `build` (with `SOURCE_DATE_EPOCH` from the tag commit) → `publish` (PyPI trusted publisher) → `github-release` → `test-installation` (cross-platform pip install verification). 6. Leaves `develop` untouched. Dev-only paths persist on develop by design (per A3 in `_project/decisions/single-repo-migration.md`); the - release squash on `main` does not need to be replayed onto develop. + release squash on `release` does not need to be replayed onto develop. -Push-to-main jobs are post-merge signals. They may still start when `main` +Push-to-release jobs are post-merge signals. They may still start when `release` advances, but they are not pre-publish evidence: the tag push follows the successful release PR merge and `.github/workflows/release.yml` begins from -that public tag. If a post-merge `main` check fails after the tag is pushed, +that public tag. If a post-merge `release` check fails after the tag is pushed, handle it as a patch release or incident; do not treat the already-published release as if it had been blocked. ## Recovering from common failures - **`validate-base` or `release-required-result` is missing**: stop. The - `main-release-only` ruleset or release workflow contract is out of sync; + `release-only` ruleset or release workflow contract is out of sync; do not finalize until both stable required contexts exist. - **`validate-base` or `release-required-result` is pending or failed**: wait for GitHub Actions or fix on a feature branch off `develop`, PR back to diff --git a/docs/operations/repo-admin-settings.md b/docs/operations/repo-admin-settings.md index 16a0e5dd6..c4e19655c 100644 --- a/docs/operations/repo-admin-settings.md +++ b/docs/operations/repo-admin-settings.md @@ -144,7 +144,7 @@ PAT can. As of `ruleset-drift-check-review-rule-coverage`, this predicate is now wired into `scripts/ruleset_drift_check.py`'s `compare_ruleset` (see "Ruleset drift is checked by `scripts/ruleset_drift_check.py`" below) — the daily `release-canary.yml` run and -`validate-main-pr.yml`'s bootstrap invocation both surface a missing +`validate-release-pr.yml`'s bootstrap invocation both surface a missing `require_code_owner_review` as a finding automatically, using the `RULESET_DRIFT_TOKEN` those workflows already fetch rulesets with. **Decision: WARN-until-enforced** — while the live state above says `false`, that finding is @@ -212,9 +212,9 @@ History: `_project/decisions/dev-loop-path-filter-smoke-test-2026-04-30.md` with full before/after JSON. -## Branch ruleset — `main` +## Branch ruleset — `release` -Ruleset name: `main-release-only`, targets `refs/heads/main`. +Ruleset name: `release-only`, targets `refs/heads/release`. Release-only branch. Direct pushes are not allowed; releases land via the `release-cut` / `release-finalize` Make targets documented in @@ -228,7 +228,7 @@ Required status checks: ``` `validate-base` is the branch-shape guard in -`.github/workflows/validate-main-pr.yml`. It allows only release branches +`.github/workflows/validate-release-pr.yml`. It allows only release branches matching `vX.Y.Z` with an optional suffix. `release-required-result` is the umbrella job in `.github/workflows/test.yml` @@ -253,7 +253,7 @@ Verify (ruleset id varies; list and inspect): ```bash gh api repos/joeharris76/BenchBox/rulesets --jq '.[] | {id, name, target}' -gh api repos/joeharris76/BenchBox/rulesets/ --jq ' +gh api repos/joeharris76/BenchBox/rulesets/ --jq ' { target: .target, enforcement: .enforcement, @@ -282,8 +282,8 @@ the ruleset also requires both contexts. ## Tag creation and release-environment protections Tracked by the `release-tag-publish-hardening` TODO. `.github/workflows/release.yml` -now has a `verify-tag-on-main` job that asserts the triggering ref (tag push -or `workflow_dispatch`) is `main` or an ancestor of it before build/publish +now has a `verify-tag-on-release` job that asserts the triggering ref (tag push +or `workflow_dispatch`) is `release` or an ancestor of it before build/publish run, and the real-PyPI `Publish to PyPI` step additionally requires `startsWith(github.ref, 'refs/tags/v')`. Those are workflow-level (tooling) controls. The two sections below are the remaining repo-admin-layer half of @@ -297,10 +297,10 @@ constraint as the soundness-path review enforcement gap above. Target state: a GitHub tag-protection ruleset targeting `refs/tags/v*` that restricts tag *creation* to the release automation identity / specific maintainer actors, mirroring how `develop-squash-only` restricts pushes to -`refs/heads/develop`. Today, `verify-tag-on-main` stops a stray tag from -reaching build/publish only if the tagged commit isn't on `main` — it does +`refs/heads/develop`. Today, `verify-tag-on-release` stops a stray tag from +reaching build/publish only if the tagged commit isn't on `release` — it does nothing to stop someone with push access from creating a `v*` tag *on* a -main commit out of band (e.g. re-tagging an old main commit, or tagging a +release commit out of band (e.g. re-tagging an old release commit, or tagging a version out of sequence). A tag-creation ruleset is the layer that closes that gap by restricting who may create the tag in the first place. @@ -376,7 +376,7 @@ Wired into CI (landed alongside the fnmatch/bypass-empty hardening above): `tag_protection_findings()`/`tag_bypass_advisory()` against every ruleset `release-canary.yml`'s `ruleset-drift` job already fetches (the same `RULESET_DRIFT_TOKEN`-authenticated full-ruleset listing used for the -`develop-squash-only`/`main-release-only` checks — no second API call), so +`develop-squash-only`/`release-only` checks — no second API call), so the daily canary run itself surfaces tag-ruleset drift under the same WARN-until-applied gating as the develop review rule, with no dependency on `release-canary-scheduled-activation` beyond the canary running at all. @@ -441,7 +441,7 @@ blocking_suite: (slow or resource_heavy) and not (stress or live_integration) advisory_suites: stress, live_integration, live cloud credentials, long-running UAT ``` -`validate-main-pr.yml` keeps the required context name `validate-base`, but +`validate-release-pr.yml` keeps the required context name `validate-base`, but that job now also runs `scripts/release_readiness_check.py` for release PRs. It fails when the latest completed `release-canary.yml` run is missing, red, older than 48 hours, or when the tested `develop` SHA recorded in the canary @@ -454,7 +454,7 @@ non-fast canary suite and ruleset drift check inline as bootstrap evidence. Ruleset drift is checked by `scripts/ruleset_drift_check.py` inside `release-canary.yml`. The script parses this runbook for -`develop-squash-only` and `main-release-only`, then compares live GitHub +`develop-squash-only` and `release-only`, then compares live GitHub rulesets for required status check contexts, strict-base settings, bypass actors, linear history, non-fast-forward protection, deletion protection, and target refs. For `develop-squash-only` only, it also imports @@ -469,7 +469,7 @@ with enough ruleset write/admin visibility for the API to expose `bypass_actors`; the default `GITHUB_TOKEN` is intentionally not used for this check. If GitHub API access fails, the canary is red; release PRs then fail on stale/red canary evidence instead of silently trusting comments. -`validate-main-pr.yml`'s bootstrap invocation (~line 115) runs the identical +`validate-release-pr.yml`'s bootstrap invocation (~line 115) runs the identical script with the identical token, so the same review-rule coverage is reachable from both CI call sites without a separate code path. @@ -491,6 +491,14 @@ from both CI call sites without a separate code path. > expected **RED** on the broken 0.3.0 PyPI release (see > `release-recovery-v0-3-1`) — that is the canary working, not a regression. +Branch-name note: the `main` references in the historical record below track +the observed state as of 2026-07-05, when `main` was both the repository's +default branch and the (not yet renamed) release branch. It is no longer the +default branch (see the resolution above), and this item's admin half (w5/w6) +renames the release branch `main` -> `release`. Read them as history, not as +current refs — re-verify live state (`gh api .../actions/workflows`, the +repository's default branch setting) before acting on this subsection. + Historical live state observed 2026-07-05 (release-canary-scheduled-activation TODO, w0): - `git ls-tree origin/main --name-only .github/workflows/` does **not** diff --git a/docs/operations/results-phase-2-runbook.md b/docs/operations/results-phase-2-runbook.md index 19d8e1608..3ac67d05f 100644 --- a/docs/operations/results-phase-2-runbook.md +++ b/docs/operations/results-phase-2-runbook.md @@ -39,22 +39,22 @@ needed outside the trigger conditions), trigger it manually via ### 1.3 Explorer publish path The static explorer at `benchbox.dev/results/` is intended to build and -deploy from `main` via [`docs.yml`](../../.github/workflows/docs.yml): the -`build` job runs on pushes and PRs to `main`, and the `deploy` job (Pages) -runs only on pushes to `main`. `published-results` is **not** the explorer's +deploy from `release` via [`docs.yml`](../../.github/workflows/docs.yml): the +`build` job runs on pushes and PRs to `release`, and the `deploy` job (Pages) +runs only on pushes to `release`. `published-results` is **not** the explorer's build source — it is the corpus-archive branch that contributor PRs target and that mirrors develop's `results-data/`. > **Current state (pre-launch):** the explorer steps in `docs.yml` are gated > on `hashFiles('results-explorer/package.json')`, and `release-cut` > (`Makefile`) currently `git rm`s `results-explorer/` and `results-data/` -> from the release branch, so those paths are **not on `main`** and the +> from the release branch, so those paths are **not on `release`** and the > explorer build/deploy steps are a deliberate no-op there today. The site is -> therefore not yet published from `main`. For the explorer to go live from -> `main`, the develop → main release flow must stop curating +> therefore not yet published from `release`. For the explorer to go live from +> `release`, the develop → release flow must stop curating > `results-explorer/`/`results-data/` out of the release branch — a maintainer > decision tracked separately. Until then, treat `benchbox.dev/results/` as a -> develop-built preview, not a main-deployed site. +> develop-built preview, not a release-deployed site. ## 2. Maintainer Review Checklist @@ -126,9 +126,9 @@ gh run watch --repo joeharris76/BenchBox Use `workflow_dispatch` only after confirming there is no newer push already rebuilding the site. Note that `workflow_dispatch` runs the `build` job but **not** the `deploy` job — the Pages deploy is gated on -`github.event_name == 'push' && github.ref == 'refs/heads/main'` — so a manual +`github.event_name == 'push' && github.ref == 'refs/heads/release'` — so a manual run validates the build without publishing. A publish requires a push to -`main` (and, per §1.3, the explorer paths actually being present on `main`). +`release` (and, per §1.3, the explorer paths actually being present on `release`). ## 7. Data Locations diff --git a/landing/index.html b/landing/index.html index 7e92c38ca..e60946358 100644 --- a/landing/index.html +++ b/landing/index.html @@ -1101,11 +1101,11 @@

Community

diff --git a/tests/unit/test_release_infrastructure.py b/tests/unit/test_release_infrastructure.py index ba3b8bbb4..fb3d72267 100644 --- a/tests/unit/test_release_infrastructure.py +++ b/tests/unit/test_release_infrastructure.py @@ -26,7 +26,7 @@ RELEASE_INTEGRATION_EXPRESSION = "integration and not (slow or stress or resource_heavy or live_integration)" RELEASE_CANARY_NON_FAST_EXPRESSION = "(slow or resource_heavy) and not (stress or live_integration)" RELEASE_PR_BRANCH_SKIP = ( - "(github.event_name != 'pull_request' || github.base_ref != 'main' || !startsWith(github.head_ref, 'v'))" + "(github.event_name != 'pull_request' || github.base_ref != 'release' || !startsWith(github.head_ref, 'v'))" ) RELEASE_REQUIRED_CONTEXTS = ("validate-base", "release-required-result") @@ -249,7 +249,7 @@ def test_release_required_result_contract(self): jobs = _workflow("test.yml")["jobs"] assert jobs["integration"]["if"] == ( - "github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'main')" + "github.event_name == 'push' || (github.event_name == 'pull_request' && github.base_ref == 'release')" ) integration_run_text = _workflow_job_run_text("test.yml", "integration") assert f'tests/integration -m "{RELEASE_INTEGRATION_EXPRESSION}"' in integration_run_text @@ -270,7 +270,7 @@ def test_release_required_result_contract(self): assert 'uv run --isolated --no-project --with "$$wheel"' in make_test_package release_readiness = jobs["release-readiness"] - assert release_readiness["if"] == "${{ github.event_name == 'pull_request' && github.base_ref == 'main' }}" + assert release_readiness["if"] == "${{ github.event_name == 'pull_request' && github.base_ref == 'release' }}" readiness_run_text = _workflow_job_run_text("test.yml", "release-readiness") assert "scripts/check_dependency_bounds.py" in readiness_run_text assert "--fail-on=cap-reached" in readiness_run_text @@ -288,7 +288,9 @@ def test_release_required_result_contract(self): "test-package", "release-readiness", } - assert result_job["if"] == "${{ always() && github.event_name == 'pull_request' && github.base_ref == 'main' }}" + assert ( + result_job["if"] == "${{ always() && github.event_name == 'pull_request' && github.base_ref == 'release' }}" + ) aggregate_run_text = _workflow_job_run_text("test.yml", "release-required-result") for expected in [ "test (ubuntu-latest, 3.12)", @@ -388,9 +390,9 @@ def test_canary_collect_count_regex_matches_pytest_deselect_format(self): assert not pattern.match("24795 tests collected in 16.98s") assert not pattern.match("0 tests collected") - def test_validate_main_pr_checks_release_canary_freshness(self): + def test_validate_release_pr_checks_release_canary_freshness(self): """The required validate-base context must include release canary freshness.""" - workflow = _workflow("validate-main-pr.yml") + workflow = _workflow("validate-release-pr.yml") assert workflow["permissions"]["actions"] == "read" assert workflow["permissions"]["contents"] == "read" @@ -402,7 +404,7 @@ def test_validate_main_pr_checks_release_canary_freshness(self): assert checkout_step["with"]["fetch-depth"] == 0 assert "${{ github.event.pull_request.head.sha }}" not in str(checkout_step) - run_text = _workflow_job_run_text("validate-main-pr.yml", "validate-base") + run_text = _workflow_job_run_text("validate-release-pr.yml", "validate-base") assert "git fetch --prune origin develop" in run_text assert "refs/pull/${{ github.event.pull_request.number }}/head" in run_text assert "scripts/release_readiness_check.py" in run_text @@ -419,15 +421,15 @@ def test_validate_main_pr_checks_release_canary_freshness(self): readiness_step = next(step for step in steps if step["name"] == "Check release canary freshness") assert readiness_step["env"]["RELEASE_CANARY_WORKFLOW"] == "release-canary.yml" - assert readiness_step["env"]["RELEASE_CANARY_BRANCH"] == "main" + assert readiness_step["env"]["RELEASE_CANARY_BRANCH"] == "release" assert readiness_step["env"]["RELEASE_CANARY_CHECKED_REF"] == "develop" assert readiness_step["env"]["RELEASE_CANARY_MAX_AGE_HOURS"] == "48" assert "RELEASE_READINESS_OVERRIDE_SHA" in readiness_step["env"] - def test_validate_main_pr_restores_ruleset_helper_before_drift_check(self): + def test_validate_release_pr_restores_ruleset_helper_before_drift_check(self): """scripts/ruleset_drift_check.py imports its enforcement helper from _project/scripts, which release-cut curation strips from both the - release head and (once a release has been cut) the trusted main + release head and (once a release has been cut) the trusted release base. The restore-from-develop step must run, and must run before the ruleset drift check, or bootstrap evidence would ModuleNotFoundError on every real release PR. @@ -436,7 +438,7 @@ def test_validate_main_pr_restores_ruleset_helper_before_drift_check(self): curated-out directory, so restoring only the helper still fails (v0.3.1 release PR #1072). Both modules must be restored. """ - job = _workflow("validate-main-pr.yml")["jobs"]["validate-base"] + job = _workflow("validate-release-pr.yml")["jobs"]["validate-base"] step_names = [step.get("name") for step in job["steps"]] restore_index = step_names.index("Restore ruleset review enforcement helper for bootstrap") drift_check_index = step_names.index("Bootstrap ruleset drift evidence") @@ -521,7 +523,7 @@ def test_release_finalize_docs_separate_premerge_and_postmerge_signals(self): assert "Wait for CI green" not in makefile_content assert "CI is not green" not in makefile_content assert "required release contexts: $(RELEASE_REQUIRED_CONTEXTS)" in makefile_content - assert "Push-to-main jobs are post-merge signals" in makefile_content + assert "Push-to-release jobs are post-merge signals" in makefile_content for content in [release_guide, release_template]: normalized = content.lower() @@ -531,16 +533,16 @@ def test_release_finalize_docs_separate_premerge_and_postmerge_signals(self): assert "pre-merge" in normalized assert "patch release or incident" in content - def test_release_cut_generates_changelog_from_main_delta(self): - """The release branch changelog boundary is the main patch delta, not tag ancestry.""" + def test_release_cut_generates_changelog_from_release_delta(self): + """The release branch changelog boundary is the release patch delta, not tag ancestry.""" recipe = _make_target_recipe("release-cut") release_guide = (REPO_ROOT / "docs" / "operations" / "release-guide.md").read_text(encoding="utf-8") release_template = (REPO_ROOT / ".github" / "RELEASE_PR_TEMPLATE.md").read_text(encoding="utf-8") - assert "scripts/generate_changelog_entry.py --version $(VERSION) --since-ref origin/main" in recipe - assert "`origin/main` patch delta" in release_template - assert "git log origin/main..HEAD" in release_guide - assert "origin/main" in release_guide + assert "scripts/generate_changelog_entry.py --version $(VERSION) --since-ref origin/release" in recipe + assert "`origin/release` patch delta" in release_template + assert "git log origin/release..HEAD" in release_guide + assert "origin/release" in release_guide assert "intentionally does not replay release commits onto" in release_guide def test_release_cut_curation_survives_untracked_paths(self): @@ -605,13 +607,13 @@ def test_release_cut_commit_skips_hooks_after_curation_removes_them(self): ) def test_release_cut_aligns_release_histories_before_pushing(self): - """release-cut must merge `origin/main` with `-s ours` before it pushes. + """release-cut must merge `origin/release` with `-s ours` before it pushes. - `main` and `develop` have unrelated roots (Phase 4 filter-merge) and - diverge permanently — `main` gains one squash commit per release that - `develop` never sees, and A4 step 6 (rebase develop onto main) was + `release` and `develop` have unrelated roots (Phase 4 filter-merge) and + diverge permanently — `release` gains one squash commit per release that + `develop` never sees, and A4 step 6 (rebase develop onto release) was removed by the 2026-04-27 amendment. A `vX.Y.Z` branch cut cleanly from - `develop` is therefore not mergeable into `main`: GitHub cannot compute + `develop` is therefore not mergeable into `release`: GitHub cannot compute a merge ref, the PR sits at CONFLICTING, and *no CI runs at all* — neither `validate-base` nor `release-required-result` ever trigger. Every cut before v0.3.1 fixed this by hand (#711/#712/#1029/#1043/#1072). @@ -620,19 +622,19 @@ def test_release_cut_aligns_release_histories_before_pushing(self): # `\t@?git merge`, not `\t.*git merge`: recipe comments are `\t@# ...` lines # that precede the command, so a loose anchor would match a comment first. merge_match = re.search(r"^\t@?git merge (.+?)$", recipe, re.MULTILINE) - assert merge_match, "release-cut must merge origin/main to make the release PR mergeable" + assert merge_match, "release-cut must merge origin/release to make the release PR mergeable" merge_args = merge_match.group(1).split() # `-s ours` keeps the curated release tree byte-for-byte and only records - # origin/main as a second parent; any other strategy drags main-only + # origin/release as a second parent; any other strategy drags release-only # content back into the release tree, silently un-curating the release. assert "-s" in merge_args and merge_args[merge_args.index("-s") + 1] == "ours", ( f"alignment merge must use `-s ours` to preserve the curated tree: {merge_match.group(1)}" ) assert "--allow-unrelated-histories" in merge_args, ( - "main and develop have unrelated roots; the merge needs --allow-unrelated-histories" + "release and develop have unrelated roots; the merge needs --allow-unrelated-histories" ) - assert "origin/main" in merge_args, "the alignment merge must target the fetched origin/main" + assert "origin/release" in merge_args, "the alignment merge must target the fetched origin/release" # A `-s ours` merge cannot change the tree by construction, so the guard is # really an assertion that the strategy above is still `ours`. It must stay. @@ -640,7 +642,7 @@ def test_release_cut_aligns_release_histories_before_pushing(self): "expected a post-merge guard asserting the curated release tree is unchanged" ) - # Ordering: changelog (--since-ref origin/main) needs main to still be a + # Ordering: changelog (--since-ref origin/release) needs release to still be a # non-ancestor; the merge must land on top of the release commit; and the # push must carry the merge, or the PR is born CONFLICTING again. commit_match = re.search(r"^\tgit commit .*Release v\$\(VERSION\)", recipe, re.MULTILINE) From cf43f94251f2d3496c95678a9e0b19595124d002 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 17:35:41 +0000 Subject: [PATCH 2/3] fix(release-rename): correct canary branch filter and ruleset_drift_check's hardcoded old name Two review findings on the main->release in-repo rename: - validate-release-pr.yml set RELEASE_CANARY_BRANCH=release, but release-canary.yml's scheduled runs report the branch they were TRIGGERED from (the repository default, which this migration moves to develop), not RELEASE_CANARY_REF's checkout target. Left as "release", every release PR's validate-base would fail to find any canary runs. Changed to "develop", matching RELEASE_CANARY_REF/ RELEASE_CANARY_CHECKED_REF already in the same env block. - scripts/ruleset_drift_check.py's parse_expected_rulesets still hardcoded the old "main-release-only" name, but the runbook renamed the section to "release-only". Reproduced the exact crash this would cause (ValueError: could not find ruleset section for 'main-release-only') via git stash before fixing. Updated the two test files that keyed off the old name to match. Verified via git stash that all touched tests fail without these fixes. --- .github/workflows/validate-release-pr.yml | 11 +++++++- scripts/ruleset_drift_check.py | 4 +-- .../test_ruleset_drift_review_coverage.py | 12 ++++----- tests/unit/test_release_infrastructure.py | 6 ++++- tests/unit/test_ruleset_drift.py | 26 +++++++++---------- 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/.github/workflows/validate-release-pr.yml b/.github/workflows/validate-release-pr.yml index df574e358..25ca0671a 100644 --- a/.github/workflows/validate-release-pr.yml +++ b/.github/workflows/validate-release-pr.yml @@ -57,7 +57,16 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_CANARY_WORKFLOW: release-canary.yml - RELEASE_CANARY_BRANCH: release + # release-canary.yml runs scheduled from the repository's DEFAULT + # branch (develop, per the branch-rename migration this PR is part + # of) even though it checks out develop's content either way via + # RELEASE_CANARY_REF. A workflow run's `branch` field reflects + # where the run was TRIGGERED from, not RELEASE_CANARY_REF's + # checkout target -- so the branch= filter here must track the + # default branch, not the release branch, or release_readiness_check.py + # queries a branch that never runs the canary and every release PR + # fails with "No completed release canary runs found." + RELEASE_CANARY_BRANCH: develop RELEASE_CANARY_CHECKED_REF: develop RELEASE_CANARY_MAX_AGE_HOURS: "48" RELEASE_READINESS_OVERRIDE_SHA: ${{ vars.RELEASE_READINESS_OVERRIDE_SHA }} diff --git a/scripts/ruleset_drift_check.py b/scripts/ruleset_drift_check.py index ed3994f66..9077a33c3 100644 --- a/scripts/ruleset_drift_check.py +++ b/scripts/ruleset_drift_check.py @@ -116,7 +116,7 @@ def _other_properties(section: str, name: str) -> dict[str, str]: def parse_expected_rulesets(runbook_text: str) -> dict[str, ExpectedRuleset]: """Extract expected ruleset state from docs/operations/repo-admin-settings.md.""" expected: dict[str, ExpectedRuleset] = {} - for name in ("develop-squash-only", "main-release-only"): + for name in ("develop-squash-only", "release-only"): section = _section_for_ruleset(runbook_text, name) ref_match = re.search(r"targets?\s+`([^`]+)`", section) if not ref_match: @@ -171,7 +171,7 @@ def compare_ruleset( ) -> list[str]: """Return human-readable drift findings for one ruleset. - For ``develop-squash-only`` only (``main-release-only`` has no equivalent + For ``develop-squash-only`` only (``release-only`` has no equivalent documented requirement), also runs the shared ``ruleset_review_enforcement.review_enforcement_findings`` predicate against the already-fetched live payload (via ``extract_rules`` - no diff --git a/tests/unit/release/test_ruleset_drift_review_coverage.py b/tests/unit/release/test_ruleset_drift_review_coverage.py index 51dfef065..10dc15d6b 100644 --- a/tests/unit/release/test_ruleset_drift_review_coverage.py +++ b/tests/unit/release/test_ruleset_drift_review_coverage.py @@ -9,7 +9,7 @@ ``review_enforcement_findings`` from ``_project/scripts/ruleset_review_enforcement.py`` (the single source of truth for that predicate - not reimplemented here) and applies it only to -``develop-squash-only`` (``main-release-only`` has no equivalent +``develop-squash-only`` (``release-only`` has no equivalent documented requirement). Decision (ruleset-drift-check-review-rule-coverage, w1): WARN-until-enforced. @@ -140,16 +140,16 @@ def test_review_enforcement_can_be_switched_to_blocking_explicitly(): assert blocking_findings(findings) == findings -def test_main_release_only_is_not_subject_to_the_review_rule_check(): - """main-release-only has no documented review-enforcement requirement; +def test_release_only_is_not_subject_to_the_review_rule_check(): + """release-only has no documented review-enforcement requirement; compare_ruleset must not apply the develop-only check to it.""" expected = parse_expected_rulesets((REPO_ROOT / "docs" / "operations" / "repo-admin-settings.md").read_text())[ - "main-release-only" + "release-only" ] live = { - "name": "main-release-only", + "name": "release-only", "enforcement": "active", - "conditions": {"ref_name": {"include": ["refs/heads/main"], "exclude": []}}, + "conditions": {"ref_name": {"include": ["refs/heads/release"], "exclude": []}}, "bypass_actors": [], "rules": [ { diff --git a/tests/unit/test_release_infrastructure.py b/tests/unit/test_release_infrastructure.py index fb3d72267..eb7942dfc 100644 --- a/tests/unit/test_release_infrastructure.py +++ b/tests/unit/test_release_infrastructure.py @@ -421,7 +421,11 @@ def test_validate_release_pr_checks_release_canary_freshness(self): readiness_step = next(step for step in steps if step["name"] == "Check release canary freshness") assert readiness_step["env"]["RELEASE_CANARY_WORKFLOW"] == "release-canary.yml" - assert readiness_step["env"]["RELEASE_CANARY_BRANCH"] == "release" + # develop, not release: release-canary.yml's scheduled runs report the + # branch they were triggered from (the repository default, which the + # branch-rename migration moves to develop), regardless of which ref + # RELEASE_CANARY_REF checks out. + assert readiness_step["env"]["RELEASE_CANARY_BRANCH"] == "develop" assert readiness_step["env"]["RELEASE_CANARY_CHECKED_REF"] == "develop" assert readiness_step["env"]["RELEASE_CANARY_MAX_AGE_HOURS"] == "48" assert "RELEASE_READINESS_OVERRIDE_SHA" in readiness_step["env"] diff --git a/tests/unit/test_ruleset_drift.py b/tests/unit/test_ruleset_drift.py index cab8d53a4..43b38e692 100644 --- a/tests/unit/test_ruleset_drift.py +++ b/tests/unit/test_ruleset_drift.py @@ -17,7 +17,7 @@ def _live_ruleset(ref: str, checks: list[str], *, strict: bool = False, bypass: list[dict] | None = None) -> dict: return { - "name": "main-release-only", + "name": "release-only", "enforcement": "active", "conditions": {"ref_name": {"include": [ref], "exclude": []}}, "bypass_actors": bypass or [], @@ -41,26 +41,26 @@ def test_parse_expected_rulesets_from_admin_runbook() -> None: assert expected["develop-squash-only"].ref == "refs/heads/develop" assert expected["develop-squash-only"].required_checks == ("ci-required-result",) - assert expected["main-release-only"].ref == "refs/heads/main" - assert expected["main-release-only"].required_checks == ("validate-base", "release-required-result") - assert expected["main-release-only"].strict_required_status_checks_policy is False - assert expected["main-release-only"].bypass_actors_none is True + assert expected["release-only"].ref == "refs/heads/release" + assert expected["release-only"].required_checks == ("validate-base", "release-required-result") + assert expected["release-only"].strict_required_status_checks_policy is False + assert expected["release-only"].bypass_actors_none is True def test_matching_ruleset_has_no_drift_findings() -> None: expected = parse_expected_rulesets((REPO_ROOT / "docs" / "operations" / "repo-admin-settings.md").read_text())[ - "main-release-only" + "release-only" ] - live = _live_ruleset("refs/heads/main", ["validate-base", "release-required-result"]) + live = _live_ruleset("refs/heads/release", ["validate-base", "release-required-result"]) assert compare_ruleset(expected, live) == [] def test_required_context_drift_is_reported() -> None: expected = parse_expected_rulesets((REPO_ROOT / "docs" / "operations" / "repo-admin-settings.md").read_text())[ - "main-release-only" + "release-only" ] - live = _live_ruleset("refs/heads/main", ["validate-base"]) + live = _live_ruleset("refs/heads/release", ["validate-base"]) findings = compare_ruleset(expected, live) @@ -71,10 +71,10 @@ def test_required_context_drift_is_reported() -> None: def test_ruleset_policy_drift_is_reported() -> None: expected = parse_expected_rulesets((REPO_ROOT / "docs" / "operations" / "repo-admin-settings.md").read_text())[ - "main-release-only" + "release-only" ] live = _live_ruleset( - "refs/heads/main", + "refs/heads/release", ["validate-base", "release-required-result"], strict=True, bypass=[{"actor_type": "RepositoryRole"}], @@ -90,9 +90,9 @@ def test_ruleset_policy_drift_is_reported() -> None: def test_bypass_actor_visibility_can_be_required() -> None: expected = parse_expected_rulesets((REPO_ROOT / "docs" / "operations" / "repo-admin-settings.md").read_text())[ - "main-release-only" + "release-only" ] - live = _live_ruleset("refs/heads/main", ["validate-base", "release-required-result"]) + live = _live_ruleset("refs/heads/release", ["validate-base", "release-required-result"]) del live["bypass_actors"] findings = compare_ruleset(expected, live, require_bypass_actor_visibility=True) From 85220213da726b52b71259c0f37136fbde56ceed Mon Sep 17 00:00:00 2001 From: Joe Harris <57046+joeharris76@users.noreply.github.com> Date: Fri, 10 Jul 2026 08:30:54 -0400 Subject: [PATCH 3/3] fix(release-rename): sweep dangling validate-main-pr.yml pointers The w1-w4 rename moved validate-main-pr.yml -> validate-release-pr.yml but left four comment/docstring pointers at the old path, plus origin/main as the --since-ref example now that release-cut passes origin/release. repo-admin-settings.md's pointer arrived from develop after this branch was cut, in a hunk this branch never touched, so the rebase merged it without a conflict. Co-Authored-By: Claude Opus 4.8 --- docs/operations/repo-admin-settings.md | 2 +- scripts/generate_changelog_entry.py | 12 ++++++------ scripts/ruleset_drift_check.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/operations/repo-admin-settings.md b/docs/operations/repo-admin-settings.md index c4e19655c..7b48a00af 100644 --- a/docs/operations/repo-admin-settings.md +++ b/docs/operations/repo-admin-settings.md @@ -197,7 +197,7 @@ soundness-path PR can no longer be squash-merged. Flipping the constant **before** step 1 turns the (still-live) missing-review gap from a `WARNING (non-blocking):` finding into a blocking one, which reddens **both** the daily -`release-canary.yml` run **and** `validate-main-pr.yml`'s drift-check on every real develop +`release-canary.yml` run **and** `validate-release-pr.yml`'s drift-check on every real develop PR until the PUT lands. Done in the correct order, the flip's own CI is green (the live ruleset already satisfies the rule). No test changes are required: `compare_ruleset`'s `enforce_review_rule` parameter is exercised in both `False`/`True` modes by diff --git a/scripts/generate_changelog_entry.py b/scripts/generate_changelog_entry.py index 0aa69bb50..23d6e4d8c 100755 --- a/scripts/generate_changelog_entry.py +++ b/scripts/generate_changelog_entry.py @@ -25,7 +25,7 @@ uv run python scripts/generate_changelog_entry.py --version 0.3.0 \ --release-date 2026-04-30 --since-tag v0.2.1 uv run python scripts/generate_changelog_entry.py --version 0.3.1 \ - --since-ref origin/main + --since-ref origin/release """ from __future__ import annotations @@ -47,7 +47,7 @@ _CHANGELOG_VERSION_HEADER_RE = re.compile(r"^## \[(?P\d+\.\d+\.\d+)\] - ", re.MULTILINE) # Matches a release-cut branch name, e.g. "v0.3.1" or "v0.3.1-rc1". Mirrors -# the HEAD_REF regex in .github/workflows/validate-main-pr.yml so the two +# the HEAD_REF regex in .github/workflows/validate-release-pr.yml so the two # checks agree on what counts as an in-progress release branch. _RELEASE_BRANCH_RE = re.compile(r"^v(?P\d+\.\d+\.\d+)(-[A-Za-z0-9.+-]+)?$") @@ -240,9 +240,9 @@ def find_untagged_changelog_versions( `make release-cut` legitimately drafts a dated section for the version being cut before its tag exists (the tag is pushed later, by - `release-finalize`, after squash-merge to main). So when + `release-finalize`, after squash-merge to release). So when `current_branch` matches the release-branch shape recognised by - `.github/workflows/validate-main-pr.yml` (`vX.Y.Z[-suffix]`), the one + `.github/workflows/validate-release-pr.yml` (`vX.Y.Z[-suffix]`), the one untagged version matching that branch's own version number is exempt. Every other untagged dated section is flagged - in particular, any @@ -424,7 +424,7 @@ def generate_changelog_entry( since_tag: Tag to use as the lower bound (e.g. 'v0.2.1'). Ignored when since_ref is set. If neither is set, the latest matching v* tag is auto-detected. - since_ref: Ref to use as the lower bound (e.g. 'origin/main'). This is + since_ref: Ref to use as the lower bound (e.g. 'origin/release'). This is the release-branch flow default because `develop` is intentionally not tagged after releases. @@ -533,7 +533,7 @@ def main() -> int: parser.add_argument( "--since-ref", default=None, - help="Lower-bound ref for commit range, e.g. origin/main. Overrides --since-tag.", + help="Lower-bound ref for commit range, e.g. origin/release. Overrides --since-tag.", ) parser.add_argument( "--source", diff --git a/scripts/ruleset_drift_check.py b/scripts/ruleset_drift_check.py index 9077a33c3..fb025cdbe 100644 --- a/scripts/ruleset_drift_check.py +++ b/scripts/ruleset_drift_check.py @@ -40,7 +40,7 @@ # enforcement" section - deferred admin action tracked by # auto-merge-review-gate-admin-enforcement). Flipping this straight to a # blocking check would make the daily release-canary.yml run (and the -# validate-main-pr.yml bootstrap invocation) permanently red until that +# validate-release-pr.yml bootstrap invocation) permanently red until that # admin PUT lands. Flip this constant to True once that TODO's w3 confirms # the PUT has landed - this is the one-line change referenced by that # decision; no other code here needs to change.