Skip to content

ci: move release jobs to pf-ci-release and publish only from verified tags on main - #91

Merged
luisleo526 merged 9 commits into
mainfrom
ci/pfrel-release
Oct 20, 2026
Merged

luisleo526 merged 9 commits into
mainfrom
ci/pfrel-release

Conversation

@luisleo526

@luisleo526 luisleo526 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Moves the release hub's credential and release jobs to the isolated pf-ci-release runner class (owner rulings CI-HOSTED = B and REL-HOSTED = B: no hosted exception for release jobs), makes every job on that class reachable only from a run at refs/heads/main, and makes publication an explicit operator step.

  1. Runner class. 13 jobs across eight workflows now use the literal ["self-hosted","pf-ci","pf-ci-release"], no hosted fallback: facts-descriptions (descriptions), facts-fanout (web, license), handle-upstream (bump), publish (prepare, publish, failure-report), release-finalize (finalize, notify-consumers), release-receipts (plan, verify), release-reconcile (producers), release-rollback (restore). Strict runner and inventory pins are in tests/test_release_workflows.py.
  2. Trigger conversion (L6 approved 2026-10-21 00:20). publish.yml no longer starts on a v* tag push. It is workflow_dispatch on main with one required tag input. A first, read-only tag-guard job (contents: read, no secrets, no job-level if) refuses unless the run is on refs/heads/main, validates the tag against ^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ before any git command, fetches tags and main with full history, resolves the tag as refs/tags/<tag>^{commit} (never a short name), and requires that commit to be an ancestor of refs/remotes/origin/main (named in full: git resolves a short origin/main against refs/tags/ first, so a tag of that name on a side commit would otherwise pass). It outputs the verified 40-hex SHA. Every later job needs it and checks out that SHA with persist-credentials: false; the SHA also names the candidate image and feeds the revision/build arguments and the release bindings. A failed or skipped guard blocks finalization, and failure-report refuses before any checkout.
  3. No workflow dispatches publish.yml (L6 route (a), 2026-10-21 01:34). A dispatch made with GITHUB_TOKEN runs as github-actions[bot], which the org's authz job refuses, and a dispatch with the App token depends on the App's selected repositories, which are not readable today. So handle-upstream.yml pushes the tag and prints the operator command (a ::notice, or a ::warning on the republish path, and the step summary); its App token keeps contents: write and loses actions: write. Tests pin that no workflow file dispatches publish.yml in any form.

release semantics change: a tag push no longer starts this workflow, and an upstream release no longer starts publication by itself.

OPERATOR STEP. A user with write access, after the tag exists on main and handle-upstream is green:

gh workflow run publish.yml --repo pineforge-4pass/pineforge-release --ref main -f tag=<vX.Y.Z or vX.Y.Z-rc.N>

The same command replays a release. The tag must exist and sit on main; a tag push alone publishes nothing. The republish recovery path of the handler now only prints this command.

Trigger audit: every workflow that uses pf-ci-release

No tag-push, release-event or workflow_run start remains. push appears only limited to branches: [main].

Workflow Trigger declarations (file:line)
facts-descriptions.yml :4 push with branches: [main] at :5; :13 workflow_dispatch; :20 schedule
facts-fanout.yml :4 push with branches: [main] at :5; :8 workflow_dispatch; :15 schedule
handle-upstream.yml :23 repository_dispatch
publish.yml :17 workflow_dispatch with required tag; main-ref checks in tag-guard and prepare
release-finalize.yml :4 workflow_call (inherits the guarded caller's main ref); :9 workflow_dispatch; :15 push with branches: [main] at :16; :18 schedule
release-receipts.yml :4 repository_dispatch; :6 workflow_dispatch; :7 schedule
release-reconcile.yml :4 workflow_dispatch; :5 schedule
release-rollback.yml :4 workflow_dispatch

repository_dispatch and schedule run at the default branch; workflow_dispatch runs at the selected ref, which tag-guard and prepare require to be main. No timeout-minutes is added (owner #827). The runner group admits all of these workflows at refs/heads/main (laneboss).

Tests

tests/test_release_workflows.py extracts the real tag-guard run: script and executes it under bash against temporary real git repositories with literal expected results: a tag on main passes and the output SHA equals that commit; an annotated tag outputs the peeled commit; a bad pattern, a newline-bearing tag, a missing tag, a tree tag, a side-branch tag and a non-main ref all fail with the exact ::error:: text and emit no SHA. The fixture puts decoy tags named origin/main, main, heads/main and remotes/origin/main on the side commit, and the clone gets local branches named like tags (a branch named like a missing tag is refused; a branch named like an honest tag outputs the tag's commit). Mutants that remove the pattern, existence or reachability check, restore the short origin/main, or resolve the tag by its bare name must fail the table. Text pins and mutants cover job dependencies, SHA checkouts, shell interpolation, the failure refusal, and that no workflow file dispatches publish.yml (CLI, API and github-script forms) and that the App-token step has no permission-actions.

Verification record

🤖 Generated with Claude Code

luisleo526 and others added 9 commits October 20, 2026 16:09
Prepare pineforge-release at base b799e90 for the isolated release class.
Change 13 jobs across eight workflows to the literal runs-on array
["self-hosted","pf-ci","pf-ci-release"], with no hosted fallback.

Moved jobs:

- facts-descriptions.yml: descriptions
- facts-fanout.yml: web, license
- handle-upstream.yml: bump
- publish.yml: prepare, publish, failure-report
- release-finalize.yml: finalize, notify-consumers
- release-receipts.yml: plan, verify
- release-reconcile.yml: producers
- release-rollback.yml: restore

The prepare, receipt plan and failure-report jobs hold the automatic
GITHUB_TOKEN through github.token with Contents or Issues write access.
The other moved jobs hold secrets, App tokens or protected environments;
publish also builds the candidate image that finalization publishes.
The reusable publish.yml notify-consumers caller keeps using
release-finalize.yml, whose two executing jobs move to the isolated class.

Add strict runner and inventory pins in tests/test_release_workflows.py.
Must-fail mutations restore hosted labels, the old guarded runner expression,
other pools or a hosted fallback; introduce pull_request or
pull_request_target in mapping, sequence and scalar trigger forms; or remove
or add a job. These cases use the same guard as the actual-workflow pin.
Existing PR-tier routing and its assertions are unchanged.

Workflow edits change only runs-on. Preserve timeouts, credentials, secret
names, permissions, environments, dependencies, triggers and matrix shapes.
Add runner-isolation documentation to README.md and docs/release-chain.md.

Documentation sentence audit: no existing README/CONTRIBUTING/docs sentence
was removed or rewritten. Both documentation edits only add paragraphs or
a section, so there are no removed/rewritten sentences to quote verbatim.

NOT RUN: no repository code, YAML parser, tests, mutations, preflight,
ci-policy-check.py, TypeSafe diff guard or CI was executed on this machine.
REPORT-PFREL.md records NOT RUN for every changed file and the before-line
runner inventory. Review used read-only text and git diff inspection.

PREPARE, DO NOT SHIP: push only after laneboss posts "pf-ci-release live";
until then jobs on that label wait. The supervisor commits the diff.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Owner #827: no time limits. GitHub's own 6 h job maximum stays the only
hang catcher.

`release-finalize.yml` is the one release workflow that the timeout sweep
does not touch on its own branch (ci/tmo-release): this file also carries
the isolated-runner change on ci/pfrel-release, so its single
job-level `timeout-minutes:` key (line 81) is removed here instead and the
two PRs never touch the same file.

Deletion only: git diff --numstat is 0 added, 1 deleted.
Conflicts: .github/workflows/facts-descriptions.yml (1 job) and .github/workflows/facts-fanout.yml (2 jobs). Main removed the job-level `timeout-minutes: 10` (owner #827); this branch moved the same jobs' runs-on to ["self-hosted","pf-ci","pf-ci-release"]. Resolution keeps BOTH: the pf-ci-release runs-on line and no timeout-minutes key. No other file conflicted (docs/release-chain.md and tests/test_release_workflows.py merged automatically).

Validation: NOT RUN here (nothing from the repository runs on the authoring machine); `grep -rnE '^\s*timeout-minutes:' .github` prints nothing after the merge.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Remove publish's tag-push entry point and require one tag input on a main-ref
workflow_dispatch. A separate read-only tag-guard job checks the main run ref,
validates the complete tag shape before git uses the input, fetches tags/main with full
history, requires an existing tag that peels to a commit, and rejects commits
not reachable from origin/main. Export the verified 40-hex commit SHA and tag.

Require tag-guard in every later job. Check out the tagged tree by its verified
SHA with persist-credentials: false, and use that SHA for candidate names, image
revision/build arguments and release bindings. A failed or skipped guard blocks
finalization and makes failure-report refuse before checkout or repository code.
Preserve the annotated-tag re-fetch and release/build gates.

Have handle-upstream dispatch publication on main after either successful tag
push, and replay on main with the existing tag input. The existing App token
authorizes these calls; noop/wait and the release decision stay unchanged.

Pin the trigger policy across every workflow mentioning pf-ci-release, preserve
the isolated runner pins, and add negative controls against real workflow text.
Add CI tests that extract the actual guard script and execute it under bash
against temporary real git repositories with file remotes and literal expected
outcomes, including side-branch refusal and annotated-tag peeling. Execute the
removed-check mutants through the same real-git table. Missing readiness steps
fail assertions rather than being accepted as KeyError. Repair the stale PR
exception inventory to pin the release labels already in the workflows.

OPERATOR STEP: dispatch publish.yml on main with the existing vX.Y.Z or
vX.Y.Z-rc.N tag in the tag input. The tag must exist and sit on main.
release semantics change: a tag push no longer starts this workflow

Removed documentation

The following original text is quoted verbatim from the replaced documentation
and workflow comments. It is superseded by main-ref dispatch with a separate,
validated tag input.

README.md (release-flow diagram):

>   → lockstep VERSION + tag (App) → publish.yml: build and verify candidate

README.md (handler description):

>   commits + tags (idempotent, with half-failed-release recovery and a downgrade
>   guard).

README.md (publication entry point):

> - `publish.yml` — on the pushed tag or explicit replay: checks the pair, waits
>   for upstream artifacts, builds a multi-arch candidate and verifies both child

docs/release-chain.md (runner inventory):

> | `publish.yml` | `prepare`, `publish`, `failure-report` |

docs/release-chain.md (tag hand-off):

> events** sent back to the existing handler. A hub tag hands completion to
> publish/finalize, even while the release is a draft awaiting prerequisites.

docs/release-chain.md (recovery and manual replay):

> Recovery of an existing hub tag explicitly dispatches `publish.yml` **on that
> tag**. It never force-pushes or rewrites it. Manual replay is also available
> through Actions: select `publish`, select the existing tag as its ref, run.

docs/release-chain.md (candidate preparation, reflowed):

> `publish.yml` first reuses `release-candidate.json` if already stored. Otherwise

.github/workflows/publish.yml (header comment):

> Fires on the v* tag that handle-upstream.yml pushes with the App
> token (or a manual tag). The tag value equals VERSION (asserted below).

.github/workflows/handle-upstream.yml (header comment):

> The App-token-pushed tag fires publish.yml (build the combined image -> fan
> out to the MCPs and the app). A GITHUB_TOKEN-pushed tag would NOT re-trigger
> publish.yml; the App token (a non-default credential) does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
release-rules failed on this PR:

  FAIL: test_lines_196_to_225_are_byte_identical_to_the_published_rule
  AssertionError: 'c738188a...' != '35b72f7f...'

The README release-flow section grew by 19 lines (the publish.yml dispatch
text), so the unchanged pair rule moved from lines 196-225 to 215-244. The
pinned SHA-256 is the same: sha256 of README.md lines 215-244 is
35b72f7f5c7466cfc212a71eaf05df98f45e92411fd34a67bfff761556c61601, and so is
that of lines 196-225 on main. Only the line range in the test and its
comment change; the digest and the byte-identical requirement do not.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The tag-guard ran `git merge-base --is-ancestor "$TAG_COMMIT" origin/main`
after `git fetch --tags`. git resolves the short name `origin/main` against
refs/tags/ before refs/remotes/, so anyone who can push tags could create a
tag named `origin/main` on a side-branch commit and have a side-branch release
tag pass the reachability check and be published.

The ancestry check now names refs/remotes/origin/main. The real-git test
fixture adds decoy tags named origin/main, main, heads/main and
remotes/origin/main on the side commit, so the existing side-branch row
(v1.6.4) refuses only with the full ref; a new mutant that puts the short name
back must fail the table, the fetched-main assertion reads the full ref, and
the text pins name it. docs/release-chain.md says the ref is named in full.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…6 route a)

A dispatch made with GITHUB_TOKEN runs as github-actions[bot], which the
org's authz job refuses, and a dispatch with the App token depends on the
App's selected repositories, which nobody can read today. So publication must
not depend on an in-workflow dispatch. The operator doing the cut runs, as a
user with write access, after the tag exists on main and handle-upstream is
green:

  gh workflow run publish.yml --repo pineforge-4pass/pineforge-release --ref main -f tag=<vX.Y.Z or vX.Y.Z-rc.N>

A tag push alone publishes nothing; release semantics change: the handler no
longer starts publication after an upstream release.

handle-upstream.yml: the three `gh workflow run publish.yml` calls added by
2afd7ef are removed. The Re-publish step prints the operator command as a
::warning and to the step summary and exits 0; the bump step and the retag
path print it as a ::notice and to the summary after the tag push. The
`permission-actions: write` line and the GH_TOKEN env lines are removed (the
App token keeps contents: write), and the step is named "Bump VERSION, commit,
tag" again.

Tests: no workflow file may dispatch publish.yml (CLI, API or github-script,
checked over every workflow's text, with mutants for each form); the App-token
step has no permission-actions; the notice steps are pinned and mutated. The
real-git guard table gains rows with LOCAL branches created in the work clone:
a branch v1.6.7 at the main tip with no tag v1.6.7 is refused, a branch v1.6.2
next to the honest tag passes and outputs the tag's commit, and the mutant that
resolves the tag by its bare name fails the first row.

Removed documentation (text and comments added by 2afd7ef and replaced here)

handle-upstream.yml header:

> After pushing the tag, the handler dispatches publish.yml on main with that
> tag as input (build the combined image -> fan out to the MCPs and the app).
> The App token authorizes both the push and the dispatch. Idempotent, with

handle-upstream.yml App token comment: "publication and replay dispatched on
main"; step names "Bump VERSION, commit, tag and dispatch publish.yml on
main"; echo lines "explicitly replaying publish.yml on main for" and
"publish.yml will build + fan out."

README.md:

>   → lockstep VERSION + tag (App) → dispatch publish.yml on main with tag input
>   commits + tags, then dispatches `publish.yml` on `main` with the existing tag
>   input (idempotent, with half-failed-release recovery and a downgrade guard).
> For a manual release or replay, select `publish` in Actions, choose `main` as
> the workflow ref, and supply the existing `vX.Y.Z` or `vX.Y.Z-rc.N` tag in `tag`.

docs/release-chain.md:

> events** sent back to the existing handler. After pushing a hub tag, the handler
> dispatches publication on `main` with that tag input, and finalization continues
> even while the release is a draft awaiting prerequisites.
>
> Recovery of an existing hub tag explicitly dispatches `publish.yml` **on main**
> with the existing tag supplied as the required `tag` input. It never force-pushes
> or rewrites it. Manual release or replay is also available through Actions:
> select `publish`, choose `main` as its ref, supply the existing tag in `tag`, run.
>
> | App installation permissions | Hub: Contents write + Actions write; enabled consumers: Contents read/write, Actions read, Pull requests read |

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Brings in #90 (release-receipts: discover scheduled evidence from repository
artifacts). It touches scripts/release_receipts.py, tests/test_release_binding.py,
tests/test_release_receipts.py and the release-receipts fixtures; this branch
touches none of them, so the merge has no conflict. README.md is unchanged
by #90, so the pair-rule digest pin (lines 215-244) still holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he target

assert_no_publish_dispatch only failed when the literal publish.yml sat next
to the call, so a dispatch by display name, by numeric id or through
github-script's request API passed it (found by CORE-TOP's read of the same
pin in codegen). No workflow of this repository dispatches any workflow, so
the pin now fails on any `workflow run`, `createWorkflowDispatch` or
`actions/workflows/<anything>/dispatches` in any workflow file, whatever the
target; plain repository_dispatch (`repos/<r>/dispatches`) stays allowed and
the quoted operator-command notices stay exempt as before. Mutants added: a
dispatch by display name ("Publish release candidate"), by numeric workflow id
through `gh api`, and through github-script's `github.request`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@luisleo526
luisleo526 merged commit 7c2386f into main Oct 20, 2026
9 checks passed
@luisleo526
luisleo526 deleted the ci/pfrel-release branch October 20, 2026 19:24
luisleo526 added a commit that referenced this pull request Oct 21, 2026
…st (owner 10-21, R6 exception)

Owner decision, 2026-10-21 (main message 1562390657274748981, "pineforge release
改用githuh runner"): the pineforge-release repository runs on GitHub-hosted
runners. This is a STANDING rule, not a 1.6.2-only measure, and the hub does not
return to Cloud Build. It is the owner's R6 / rule-B exception for this repo:
ci-policy-check.py refuses `ubuntu-latest` (runner_allowed, R6), the hub's own
workflows do not call the checker, and no checker change or exemption flag is
made here.

What changes: `runs-on: ["self-hosted","pf-ci","pf-ci-release"]` becomes
`runs-on: ubuntu-latest` on exactly these 14 jobs, each with a comment citing the
owner message id:

  facts-descriptions.yml   descriptions
  facts-fanout.yml         web, license
  handle-upstream.yml      bump
  publish.yml              tag-guard, prepare, publish, failure-report
  release-finalize.yml     finalize, notify-consumers
  release-receipts.yml     plan, verify
  release-reconcile.yml    producers
  release-rollback.yml     restore

Kept unchanged: every #91 guard (main-only tags, tag-guard, environment,
permissions) and the apt-guard steps of this PR. The apt guard is harmless on a
hosted VM. PR-class jobs keep pf-ci-x64.

tests/test_release_workflows.py: the runner pins now expect `ubuntu-latest`
(HOSTED_JOBS, assert_hosted_runners, EXEMPT_RUNNERS); the publish mutation now
swaps `ubuntu-latest` for the release array (count 4); new tests refuse the
release array on each of the 14 jobs and refuse `pf-ci-release` in any
`runs-on` of any workflow. Not run locally (no local test run in this lane);
CI runs it.

Docs, every deleted or rewritten sentence named:
- README.md: "The release and facts-automation jobs use" the array sentence;
  "the isolated release runner class" ; "Push this runner migration only after
  laneboss posts" ; "pf-ci-release live" ; "until then jobs on that label wait".
- docs/release-chain.md: "LB-435 prepares these jobs for the literal";
  "The class has its own runner group restricted to selected workflows";
  "belong in this class" (now "formerly belonged"); "Push this migration only
  after laneboss posts"; "until then jobs on that label wait"; "The runner group
  allowlist must include" ; "The release runner group admits runs at";
  "untouched: pending pf-ci-release, including publication workflows".

Not a pure restore of the pre-#91 values: tag-guard did not exist before #91,
and prepare, failure-report and plan had a multi-line expression with a pool
fallback; all 14 use the plain label.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
luisleo526 added a commit that referenced this pull request Oct 21, 2026
…+ temporary apt guard (LB-504) (#94)

* ci(publish): stop the apt timers first on every pf-ci-release job (LB-504)

Release VMs carry Ubuntu's apt-daily-upgrade.timer, which fired 20-25 min
after boot on pool VMs today and killed runners mid-job. The release image
is not rebaked before 1.6.2, so every job in publish.yml that runs on
pf-ci-release now stops the apt units as its first step, fail-safe: a
failed stop prints the reason and the step still ends 0. No
continue-on-error (the org policy checker reads it as ignoring a failure)
and no time limit.

tests/test_release_workflows.py keeps its pins: the apt guard is pinned as
the exact first step of all four jobs, tag-guard becomes 3 steps and
failure-report 4 (their checkout and refusal steps move one index), the
shell: bash mutation anchor counts 6, and every removal still has to break
a pin.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* ci(release): stop the apt timers first in release-finalize too (LB-504)

The 1.6.2 recovery risk (CORE-TOP 15:11, L6 GO) covers every release-class
job that can run past the apt-daily-upgrade window on pf-ci-release. The
release-finalize workflow runs there too: its `finalize` job assigns the
final tags and its `notify-consumers` job has real steps (the consumer
dispatch), so both are exposed.

Add the same first step to both jobs, byte-identical to the one publish.yml
already carries: stop apt-daily.timer, apt-daily-upgrade.timer, their
services and unattended-upgrades.service, fail-safe through
`|| echo "apt guard: stop failed ($?), continuing"` followed by `exit 0`.
No continue-on-error (the org policy checker treats it as ignoring a
failure) and no timeout-minutes (the owner's no-time-limits rule).

Pin it in tests/test_release_workflows.py: both release-finalize jobs must
start with the exact APT_GUARD_STEP, so a later edit cannot drop the guard
or move it behind a checkout. The pin reuses the constant publish.yml is
already checked against rather than restating the command.

Temporary: laneboss's rebaked release image replaces this step after 1.6.2.

* ci(release): never interrupt an apt upgrade the guard finds already running (LB-504)

The guard added earlier today stopped apt-daily(-upgrade).service along with
the timers. Interrupting dpkg in the middle of an upgrade is worse than the
lost job the guard exists to prevent, so the step no longer touches a
service.

New body (identical in all six pf-ci-release jobs):

  systemctl show -p ActiveState,SubState apt-daily-upgrade.service apt-daily.service || true
  if systemctl is-active --quiet apt-daily-upgrade.service || systemctl is-active --quiet apt-daily.service; then echo "apt guard: upgrade already running, not interrupting"; else sudo systemctl stop apt-daily.timer apt-daily-upgrade.timer || echo "apt guard: timer stop failed ($?), continuing"; fi
  exit 0

- The show line first, so the log records what the guard found.
- apt-daily-upgrade is a oneshot: while it runs its state is "activating"
  and `is-active --quiet` returns non-zero, so the else branch still runs —
  harmless, because stopping a timer never touches a running service.
- Only the two TIMERS are stopped (nothing new starts). No service, no
  unattended-upgrades.service.
- Still fail-safe, no continue-on-error (the org policy checker treats it
  as ignoring a failure), no timeout-minutes, no expression interpolation.

APT_GUARD_STEP in tests/test_release_workflows.py updated to the same bytes,
so the pin still proves the guard is steps[0] of every release job.

Temporary: laneboss's rebaked release image replaces this step after 1.6.2.

* ci(release): run the 14 hub release jobs on GitHub-hosted ubuntu-latest (owner 10-21, R6 exception)

Owner decision, 2026-10-21 (main message 1562390657274748981, "pineforge release
改用githuh runner"): the pineforge-release repository runs on GitHub-hosted
runners. This is a STANDING rule, not a 1.6.2-only measure, and the hub does not
return to Cloud Build. It is the owner's R6 / rule-B exception for this repo:
ci-policy-check.py refuses `ubuntu-latest` (runner_allowed, R6), the hub's own
workflows do not call the checker, and no checker change or exemption flag is
made here.

What changes: `runs-on: ["self-hosted","pf-ci","pf-ci-release"]` becomes
`runs-on: ubuntu-latest` on exactly these 14 jobs, each with a comment citing the
owner message id:

  facts-descriptions.yml   descriptions
  facts-fanout.yml         web, license
  handle-upstream.yml      bump
  publish.yml              tag-guard, prepare, publish, failure-report
  release-finalize.yml     finalize, notify-consumers
  release-receipts.yml     plan, verify
  release-reconcile.yml    producers
  release-rollback.yml     restore

Kept unchanged: every #91 guard (main-only tags, tag-guard, environment,
permissions) and the apt-guard steps of this PR. The apt guard is harmless on a
hosted VM. PR-class jobs keep pf-ci-x64.

tests/test_release_workflows.py: the runner pins now expect `ubuntu-latest`
(HOSTED_JOBS, assert_hosted_runners, EXEMPT_RUNNERS); the publish mutation now
swaps `ubuntu-latest` for the release array (count 4); new tests refuse the
release array on each of the 14 jobs and refuse `pf-ci-release` in any
`runs-on` of any workflow. Not run locally (no local test run in this lane);
CI runs it.

Docs, every deleted or rewritten sentence named:
- README.md: "The release and facts-automation jobs use" the array sentence;
  "the isolated release runner class" ; "Push this runner migration only after
  laneboss posts" ; "pf-ci-release live" ; "until then jobs on that label wait".
- docs/release-chain.md: "LB-435 prepares these jobs for the literal";
  "The class has its own runner group restricted to selected workflows";
  "belong in this class" (now "formerly belonged"); "Push this migration only
  after laneboss posts"; "until then jobs on that label wait"; "The runner group
  allowlist must include" ; "The release runner group admits runs at";
  "untouched: pending pf-ci-release, including publication workflows".

Not a pure restore of the pre-#91 values: tag-guard did not exist before #91,
and prepare, failure-report and plan had a multi-line expression with a pool
fallback; all 14 use the plain label.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* ci(release): run the hub PR-CI jobs on GitHub-hosted ubuntu-latest so authz and release/ready can report (owner 10-21, R304)

Hub PR #94 cannot merge: its required check `authz` has been queued on the
stalled `pf-ci-x64` pool since ~17:17. The owner's standing decision (main
message 1562390657274748981, "pineforge release改用githuh runner") covers every
job in pineforge-release, CI included (CORE-TOP R304). Commit 4 moved the 14
release jobs; this commit moves the remaining 9 PR-CI jobs, so the required
contexts `authz` and `release/ready` are produced on ubuntu-latest.

`runs-on: ["self-hosted","pf-ci","pf-ci-x64"]` becomes `runs-on: ubuntu-latest`,
each with a comment citing the owner message id, on:

  python-test.yml     authz, changes, fingerprint-canonical, release-rules,
                      description-renderer, release-ready-replay, release-ready
  facts-validate.yml  authz, validate

Unchanged: job names and `name:` fields (so the required contexts `authz` and
`release/ready` keep their names), needs, if, permissions, triggers, concurrency,
every step, and the PF_CI_LABELS env lines in publish.yml and
release-receipts.yml (not a runs-on). git grep shows no `runs-on` that is not
ubuntu-latest.

tests/test_release_workflows.py (PrTierWorkflowTest): the runner pins now expect
`ubuntu-latest` (HOSTED_JOBS with the 9 jobs, assert_hosted_runners,
RUNNER_LINE/AUTHZ_RUNNER_LINE, AUTHZ_TEXT with the new authz comment, anchor
counts RUNNER_COUNTS 7 and 2 equal to the real number of lines); the mutations
swap ubuntu-latest for the old pool array and are still refused; a new test
refuses `pf-ci-x64` and `self-hosted` on each of the 9 jobs. Not run: no local
test run; CI runs it.

Rewritten or deleted text, named by 6 consecutive words:
- both authz jobs and the AUTHZ_TEXT pin: "Pool label (needs no toolchain or
  Docker). Switch to pf-ci-crj once laneboss's bulletin says the Cloud Run class
  is live." (now "Hosted label (needs no toolchain or Docker).")
- README.md: "The release and facts-automation jobs use".
- docs/release-chain.md: "keep their existing routing"; "runner array, without
  a hosted fallback or"; "Keep pf-ci-x64 until laneboss posts"; "pf-ci-crj
  live"; "Their first job is the exact canonical authz from".

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(release): find the failure-report refusal step after the apt guard (fixes release-rules on PR #94)

PR #94 `release-rules` failed (run 39575779873, job 119171871167):
ReleaseTagGuardFailureReportTest.test_actual_first_refusal_rejects_failed_skipped_cancelled_or_missing_guard
and ...test_executed_refusal_rejects_successful_exit_or_failure_only_condition
both stopped at `assertIsNotNone(comparison)` in assert_refusal.

Cause: the LB-504 apt guard was put in front of every release job, so in
publish.yml `failure-report` the refusal step is steps[1] and steps[0] is the
apt guard. assert_refusal still read steps[0] ("Stop automatic apt upgrades"),
whose `if` is absent, so the condition regex found nothing. The sibling pin
(`failure_steps[1] == REFUSAL_STEP`, line ~391) had been moved in that commit,
this helper had not.

Fix, the intent unchanged and nothing weakened: assert_refusal now requires
steps[0] to equal APT_GUARD_STEP exactly, takes steps[1] as the refusal, and
requires it to be the step named "Refuse unverified release tag" and a run step
(no `uses`), so a checkout can never stand in for it. Everything after that is
the same: the condition must be exactly needs.tag-guard.result ==/!= 'success'
and the step is executed under bash for success, failure, skipped, cancelled,
empty and unknown guard results (exit 0 only for success, `::error::tag-guard
did not pass` otherwise).

Mutation checks (test_executed_refusal_rejects_successful_exit_or_failure_only_condition),
each must make assert_refusal raise AssertionError:
- exit 1 -> exit 0 in the refusal (existing);
- condition `!= 'success'` -> `== 'failure'` (existing);
- WEAKENED condition: `!= 'success' && needs.prepare.result == 'failure'` (new);
- REMOVED refusal step: the whole "Refuse unverified release tag" step is
  deleted, the checkout becomes steps[1] (new).
The anchors are counted by `mutate` (count must be 1) and the subtest label now
carries both sides of the replacement.

Not run: no local test run on this machine; the PR's CI is the proof.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant