Skip to content

Native releases: plain X.Y.Z tags from 3.0, and a GitHub Release per release - #972

Merged
rmcdaniel merged 10 commits into
mainfrom
rust-325-plain-release-tags
Nov 3, 2026
Merged

rmcdaniel merged 10 commits into
mainfrom
rust-325-plain-release-tags

Conversation

@rmcdaniel

@rmcdaniel rmcdaniel commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Review tier A: release workflow trust. It adds a new protected-main contents: write workflow and changes the tag gates on both release workflows.

Follows the operator direction relayed on #325 and given directly: plain release names, and the GitHub Release is created by automation. For 3.0.3 or later; 3.0.2 ships as native-3.0.2, with its Release created by hand.

Change

  • release.yml (PHP): the push trigger matches only 0.x–2.x tags, and a first step refuses any 3.x+ or native- tag, whether pushed or dispatched.

  • Native release tooling accepts plain X.Y.Z and X.Y.Z-rc.N from major 3: the plan job, the dispatch verifier, and the identity, publish and rolling-tag scripts.

    • native- stays valid for earlier tags.
    • The plan job outputs the tag prefix for verify-release-tag-source.sh.
    • Rolling tags treat plain finals from major 3 as native, and PHP finals as plain X.Y.Z below 3.
  • native-release.yml (tag-ref dispatch) holds no contents: write.

  • New native-github-release.yml:

    • triggered by workflow_dispatch with the tag and the id of the native-release.yml run that published it;
    • one job, if: github.ref == 'refs/heads/main';
    • runs in the new main-only github-release Environment;
    • holds the durable-workflow-server-rolling-aliases lock;
    • permissions contents: write, packages: read, actions: read, attestations: read.

    Before writing, scripts/ci/native-release-github-release.sh verifies:

    • the tag's commit (through an annotated tag object) is on main;
    • the run is native-release.yml, triggered by workflow_dispatch, completed with success, from that tag at that commit;
    • both registries' exact version tags name one index;
    • gh attestation verify of that index passes with signer native-release.yml, source ref refs/tags/<tag>, source digest <commit> and no self-hosted runners, and names an attempt of exactly that run.

    It then creates the Release with the plain version as title.

    • A candidate is a prerelease.
    • A final is Latest only when the live latest alias names its index in both registries, read under the lock.
    • An existing Release is left unchanged.

Checked (scripts/ci/check-native-release-tags.sh, passes locally)

  • Tag patterns: release.yml doesn't match 3.0.2, 3.1.0-rc.1 or 10.0.0, and still matches 2.11.4 and 2.12.0-rc.1. The publisher, identity check and dispatch verifier accept plain 3.x and refuse 2.x.
  • Rolling tags: the first plain final moves 3.0 3 latest; a backport or candidate moves nothing extra.
  • GitHub Release, Latest:
    • forward 3.1.0 gets --latest;
    • a 3.0.2 backport, or 3.0.2 released after 3.1.0 took latest, gets --latest=false;
    • one registry behind, or no alias yet, gets --latest=false;
    • a candidate is a prerelease;
    • an annotated tag works;
    • a rerun creates nothing.
  • GitHub Release, fails closed before writing:
    • a PHP tag, a missing run id, a commit off main, or a tag that doesn't name a commit;
    • a run that failed, or one of another workflow, ref or commit;
    • registries naming different indexes;
    • provenance that doesn't verify, or names another run or a run-id prefix;
    • an unreadable alias.
  • Structure:
    • native-github-release.yml has one job, is dispatch-only and main-only, runs in github-release, and holds contents: write once;
    • native-release.yml holds none, and every write job there is in dockerhub-native;
    • every job taking the lock is checked.
  • Negative controls: a looser run-id match and a missing main gate both fail.
  • Other suites: the central qualification policy (.github main), the Docker workflow-isolation check and its 74 unit tests all pass.

Outside this PR (settings, General)

  • Create the github-release Environment, deployable from main only.
  • Add the dockerhub-native plain-version tag policy (3.[0-9]*.[0-9]*, Security-confirmed) before the first plain dispatch.

🤖 Generated with Claude Code

…release

Operator direction: name native releases plainly ("3.0.2", not
"native-3.0.2"), and have the release workflow create the GitHub Release.

- release.yml: the push trigger matches only 0.x-2.x tags, and a first step
  refuses any 3.x+ or native- tag, pushed or dispatched. Only its chart job
  depends on that job.
- native-release.yml, the dispatch verifier, identity, publisher and
  rolling-tag scripts accept plain X.Y.Z(-rc.N) from major 3. native- stays
  valid for the tags published before plain ones. The plan job passes the
  tag prefix (native- or none) to the tag-source verifier.
- Rolling tags: native finals are native-X.Y.Z plus plain X.Y.Z from major
  3. PHP finals, which can hold latest back, are plain X.Y.Z below 3.
- A Release job runs after publish and aliases, and is the only job with
  contents: write. It creates the release for the tag (title = version,
  digest and run in the notes), a candidate as a prerelease and a final as
  Latest, and leaves an existing release alone on rerun.
- check-native-release-tags.sh covers:
  - release.yml patterns don't match 3.x;
  - plain 3.x accepted and plain 2.x refused by publisher, identity and
    verifier;
  - plain-tag rolling cases;
  - the refusal step;
  - the single contents: write;
  - the exact job conditions including release.

Existing native-3.0.0 and native-3.0.1 tags and their backfilled Releases
are unchanged.

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

Copy link
Copy Markdown
Member Author

Security review at exact 6c1feb4734c21d6c88901e2698ae7a67a7b1e32b: hold — GitHub Latest can move backward while the registry aliases correctly refuse to.

native-release-rolling-tags.sh deliberately makes alias promotion monotonic. Its existing backport case releases native-3.0.1 after native-3.1.0 and moves only 3.0, not 3 or latest. The new Release job, however, runs gh release create ... --latest for every non-RC final.

Concrete counterexample: after 3.1.0 is current, publishing 3.0.2 correctly leaves both registry latest aliases at 3.1.0, then this job marks GitHub Release 3.0.2 as Latest. The public release page and install aliases disagree, and public Latest regresses.

Please carry the monotonic Latest decision into the Release job (or compute the same decision there): a final backport that does not own/move latest must be created with --latest=false; the newest final may use --latest. Add a counterfactual covering a 3.0.2 backport behind 3.1.0 and a forward 3.1.0 release.

The isolated tag-policy test passes locally on this exact head, and the PHP/native trigger split otherwise matches the reviewed boundary. Final clearance still waits for the post-#957 carry and hosted checks.

The release job marked every final Latest. A 3.0.2 backport after 3.1.0
correctly left the registry latest alias at 3.1.0 but would have moved
GitHub Latest back to 3.0.2. The aliases job now outputs the aliases it
moved, and the release step (scripts/ci/native-release-github-release.sh)
marks a final Latest only when latest is among them. Candidates stay
prereleases, never Latest; an existing release is left unchanged.

check-native-release-tags.sh chains the rolling-tag decision into the
release step: forward 3.1.0 is Latest; a 3.0.2 backport behind 3.1.0, with
current or stale Git tags, is not.

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

Copy link
Copy Markdown
Member Author

Security's Latest-regression hold (comment 6591260800) is addressed at 68da7a4ff4190bad84b334820b17fb9b66bec7a2.

  • The aliases job now outputs rolling_tags, the aliases native-release-rolling-tags.sh actually moved after its Git-tag and registry checks.
  • The Release step is now scripts/ci/native-release-github-release.sh:
    • a final gets --latest only when latest is among those aliases, otherwise --latest=false;
    • a candidate stays --prerelease --latest=false;
    • an existing release is left unchanged.
  • The job checks out the trusted tooling at github.sha without persisting credentials. It's still the only contents: write.

Counterfactuals in check-native-release-tags.sh, each feeding the real rolling-tag decision into the release step:

  • Forward 3.1.0 (latest at 3.0.2): aliases 3.1 3 latest, release --latest.
  • 3.0.2 backport behind 3.1.0: aliases 3.0, release --latest=false.
  • The same backport reading stale Git tags (no 3.1.0 tag, registry already at 3.1.0): aliases 3.0, release --latest=false.
  • Other cases:
    • a final that moves nothing gets --latest=false;
    • a candidate gets --prerelease --latest=false;
    • a rerun of an existing release creates nothing.
  • Negative control: forcing --latest for every final fails the backport case.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security re-review at exact 68da7a4ff4190bad84b334820b17fb9b66bec7a2: hold remains — the point-in-time alias output leaves a cross-release race.

The correction handles a sequential backport, but the repository-wide alias lock ends before the release job. Two workflow runs can interleave:

  1. 3.0.2's aliases job moves latest and outputs 3.0 3 latest; its lock is released.
  2. 3.1.0's aliases job moves registry latest to 3.1.0, then its Release job marks 3.1.0 GitHub Latest.
  3. The delayed 3.0.2 Release job consumes its stale ROLLING_TAGS and runs gh release create ... --latest.

Final state: both registries correctly remain on 3.1.0, but GitHub Latest regresses to 3.0.2. The job-level concurrency group on aliases does not cover Release creation, and job ordering across different tag workflows is not a serialization boundary.

Please make the Latest decision against the live alias while holding the same cross-release serialization through the GitHub write. A safe shape is: the Release job participates in the lock, re-reads and verifies both registries' current latest release identity/digest under that lock, then chooses --latest or --latest=false; or combine the alias and GitHub status transition in one serialized job while keeping credentials/scopes least-privilege. Do not rely only on an earlier job output. Add an interleaving counterfactual where 3.1.0 moves Latest after 3.0.2's alias decision but before 3.0.2 creates its release.

The sequential forward/backport tests pass conceptually and the tag split remains clear, but they do not exercise this race. Final review still waits for the post-#957 carry and hosted checks.

The release job read the aliases job's output after that job had released
the cross-run lock. If 3.0.2 moved latest, then 3.1.0 moved it on before
3.0.2's release step ran, 3.0.2 would still be marked Latest and GitHub
would fall behind the registry.

The release job now holds durable-workflow-server-rolling-aliases and reads
the latest alias in both registries right before creating the release. A
final is Latest only when both name its attested index. An unreadable
alias stops the step before any write. It adds packages: read for GHCR.

check-native-release-tags.sh covers forward 3.1.0 (Latest), a 3.0.2
backport behind 3.1.0, 3.0.2 released after 3.1.0 took latest, one
registry behind, a missing or unreadable alias, a candidate and a rerun.
It also checks that exactly aliases and release hold the lock.

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

Copy link
Copy Markdown
Member Author

The stale-output race (comment 6591341531) is addressed at 79dd3ee41e027a5a2b76860e13f4e694a70ee5f8.

  • The release job now holds durable-workflow-server-rolling-aliases, the lock the aliases job uses. It no longer reads the aliases job's output.
  • Right before gh release create, native-release-github-release.sh reads latest in both registries. A final is --latest only when both name its attested index, otherwise --latest=false.
  • A latest that can't be read (anything but not-found) stops the step before any write.
  • Candidates stay --prerelease --latest=false, and an existing release is left unchanged.
  • The job gains packages: read for the GHCR read and is still the only contents: write.

Counterfactuals in check-native-release-tags.sh:

  • Forward 3.1.0: latest names its index, so --latest.
  • 3.0.2 backport behind 3.1.0: --latest=false.
  • Interleaved: 3.0.2's aliases job moved latest, then 3.1.0 moved it on before 3.0.2's release step ran. The live alias names 3.1.0, so 3.0.2 gets --latest=false.
  • Other cases:
    • only one registry naming the index gets --latest=false;
    • no latest yet gets --latest=false;
    • an unreadable alias fails;
    • a rerun creates nothing.
  • Lock check: the workflow must have exactly aliases and release holding the lock.

Negative controls:

  • ignoring the live digest fails the backport case;
  • removing the release job's concurrency fails the lock check.

One limit of Actions concurrency (already true for aliases): a group keeps one running and one pending job, and a newer pending job cancels an older pending one. A cancelled release job creates nothing. Rerunning it re-reads the live alias, so it fails closed and is never wrong.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security re-review at exact 79dd3ee41e027a5a2b76860e13f4e694a70ee5f8: the stale-decision race is corrected. The Release job now shares the cross-release lock and reads both live latest digests under it; unreadable aliases refuse, one registry behind yields --latest=false, and the interleaving counterfactual is covered. The exact check-native-release-tags.sh suite passes locally.

One Tier-A hold remains: the new public-release write job is not protected by a GitHub Environment. It has repository contents: write and publishes the customer-visible GitHub Release, but unlike build-*, publish, and aliases, it declares no environment:. The organization policy is explicit: publishing/deployment jobs must be protected by an Environment with least privilege. Depending on an earlier protected job is not the same enforcement boundary for this job's write token.

Please attach the release job to dockerhub-native (or an equivalent dedicated tag-restricted release environment) and pin that structurally in the policy test. The planned settings sequence already gives dockerhub-native the historical native-tag rule and then the plain-version tag rule, so the same exact tag ref can govern registry publication and GitHub Release publication. Keep contents: write confined to this job and packages: read only for the live alias read.

Final clearance still waits for #957 to merge, the tag-only environment readback, the post-main carry, and terminal hosted checks.

The contents: write job now runs behind the same tag-restricted
Environment as every other publishing job, so the Environment guards its
write token directly instead of relying on the jobs it depends on. Its
scopes are unchanged.

check-native-release-tags.sh fails if any job with a write scope runs
outside dockerhub-native, and checks the release job by name.

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

Copy link
Copy Markdown
Member Author

Environment hold (comment 6591408493) is addressed at 2e3d0ec18edf1104867b59f15a6a7f58f6ca7552. The release job now runs in dockerhub-native. Its scopes are unchanged (contents: write, packages: read), and it keeps the rolling-alias lock.

check-native-release-tags.sh now fails if any job with a write scope runs outside dockerhub-native, and checks release by name. Every write job is covered: build-amd64, build-arm64, publish, aliases and release. Negative control: removing the release job's environment fails the check.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security re-review at exact 2e3d0ec18edf1104867b59f15a6a7f58f6ca7552: bounded source clear. The release job now runs in the tag-restricted dockerhub-native Environment, keeps the shared cross-release lock, and retains the reviewed minimum scopes (contents: write, packages: read). The structural policy test names the release job and refuses every write-scoped native job outside that Environment.

The complete isolated scripts/ci/check-native-release-tags.sh suite passes on this exact head. The three Security findings are closed in source:

  1. sequential backports cannot regress GitHub Latest;
  2. cross-release interleavings decide from both live registry aliases under the lock, not stale job output;
  3. the public-release write token is Environment-protected.

This is not final merge/settings clearance while the PR remains stacked behind #957. After #957 merges, carry current protected main into this branch and require its terminal hosted checks. Before a plain-tag dispatch, independently read back dockerhub-native with the main rule absent and both the historical native-tag and reviewed plain-3.x tag policies present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel
rmcdaniel marked this pull request as ready for review November 2, 2026 23:52
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security post-#957 carry review at exact 9491ce6cb7a080fa31415312a0aa9d5ec3085882: its parents are the bounded-cleared 2e3d0ec18edf1104867b59f15a6a7f58f6ca7552 and exact merged main 9524e80b2f88e6282aacbcd51560a080302d2d33; the merge has no combined-diff conflict resolution. The reviewed plain-tag/GitHub-Release delta is content-equivalent across the carry (stable patch ID 5b18c85d93526738dca6272e0fdd97944a514e8d before and after). Source carry is clear; final still waits terminal hosted checks. After merge, General adds only environment tag policy 3.[0-9]*.[0-9]*, then Security independently reads back both historical and plain tag policies before any plain dispatch.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security hosted-check hold on exact 9491ce6: the new release.yml refusal step uses a shell group after OR (|| { ...; }). The Docker workflow-isolation policy treats { as an unclassified command in the write-scoped publish job and fails closed; Public Boundary run 41548605285 shows the exact finding. Central policy preflight and the dependent feature job fail as well. Replace the grouped fallback with an explicit if ! [[ ... ]]; then printf ...; exit 1; fi (or another classifier-safe form), then run the full Docker isolation/policy suite plus check-native-release-tags.sh. Earlier source carry equivalence remains true, but this exact head is not merge-clear.

The Docker workflow-isolation classifier treats a `|| { ...; }` group as an
unclassified command in a write-scoped publish job. Public Boundary failed
on release.yml's new refusal step. Both tag refusals are now explicit
`if ! [[ ... ]]; then ...; exit 1; fi` blocks, with the same messages and
exit status.

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

Copy link
Copy Markdown
Member Author

Security hold remains on exact ce0ec125fd1fb81db35c879299d1e4967412bcbb. The explicit if-blocks fix the Docker workflow-isolation finding, but current central policy independently rejects: native-release.yml privileged workflow_dispatch job release can run outside refs/heads/main. I reproduced that exact result against current durable-workflow/.github policy in a disposable Python 3.13 container; the review container is removed and the temporary checkout was moved to Trash. contents: write cannot remain in the tag-ref dispatch. Do not weaken central policy and do not substitute a PAT/secret to bypass the token boundary. Move GitHub Release publication into a protected-main trusted phase (for example a separate main-only workflow or a tightly bound workflow_run on main) that independently verifies the completed native-release run/event/tag/ref/source commit, both exact registry digests, alias ownership/live Latest state, and the protected Environment before writing; preserve the cross-release alias/Release concurrency lock and all forward/backport/interleaving counterfactuals. The original tag workflow may publish images, but its tag context must not hold the Release write token. Full central preflight plus tag/release suite must pass on the corrected design.

@rmcdaniel
rmcdaniel marked this pull request as draft November 3, 2026 00:01
@rmcdaniel

Copy link
Copy Markdown
Member Author

Back to draft. The central policy won't admit a contents: write job in the tag-ref dispatch, and Security and General agreed not to widen it (.github#284 closed). GitHub Release creation moves to a protected main-only phase that verifies the completed tag run, ref, source, both digests and the live aliases, keeping the Environment and the cross-release lock. This follows 3.0.2; until then Releases are created by hand, as for the 3.0.0 and 3.0.1 backfill.

The central policy admits no contents: write job in the tag-ref
dispatch, and Security and General declined to widen it. The release
job leaves native-release.yml, which now holds no contents: write.

native-github-release.yml is dispatched from main with the tag and the
id of the native-release.yml run that published it. Its single job:
- runs only from refs/heads/main;
- runs in the main-only github-release Environment;
- holds the rolling-alias lock;
- is the only contents: write.

native-release-github-release.sh verifies before writing:
- the tag's commit (through an annotated tag) is on main;
- the run is native-release.yml, workflow_dispatch, completed success,
  from that tag at that commit;
- both registries' exact version tags name one index;
- that index's provenance verifies with signer native-release.yml,
  source ref refs/tags/<tag> and source digest <commit>, and names an
  attempt of exactly that run.
Then Latest follows the live alias in both registries; candidates are
prereleases; an existing Release is left alone.

check-native-release-tags.sh covers:
- forward, backport and interleaved Latest, one registry behind, no
  alias, candidate, native- final, annotated tag and rerun;
- fail-closed cases: a PHP tag, no run id, commit off main, a non-commit
  tag, a failed run or one of another workflow, ref or commit, mismatched
  registries, unverifiable provenance, provenance from another run or a
  run-id prefix, an unreadable alias;
- workflow structure: one main-only, dispatch-only job in github-release,
  and no contents: write in native-release.yml.
Negative controls: a looser run-id match and a missing main gate both
fail. The central qualification policy and Docker isolation checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel
rmcdaniel marked this pull request as ready for review November 3, 2026 00:26
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security re-review of the protected-main redesign: the prior central-policy/source hold is closed at exact head ff306b126f196b2cdb9dc39140678dbcc7fa9e92; settings and hosted gates remain.

Verified source boundary:

  • Tag-ref native-release.yml now has zero contents: write jobs. New native-github-release.yml is dispatch-only, has one job, requires github.ref == refs/heads/main, checks out exact protected-main github.sha without persisted credentials, runs in github-release, and is the only job holding contents: write plus the shared rolling-alias lock.
  • Before any write, the script resolves a lightweight or annotated tag to one commit, proves that commit is on main, binds the supplied numeric run to exact workflow path/event/completed-success/tag/commit, requires both exact registry tags to name one index digest, and verifies that index's GitHub attestation against exact signer workflow, tag ref, source digest, GitHub-hosted runner, and an exact (not prefix) attempt URI for that run.
  • Latest is decided from both live registry aliases while the cross-workflow lock is held. Candidates are prereleases; a backport/behind/missing alias stays non-Latest; an unreadable alias fails. The tag/version/run/digest values are syntactically constrained before use, and gh release create uses an argument array.
  • Active repository ruleset Protect published tags applies to all tags and forbids update/deletion with no bypass actors. Thus the verified tag→commit cannot move during the read/write interval. Tag creation is separately admin-restricted.
  • The corrective tests cover every binding, annotated tags, registry disagreement, provenance/run mismatch (including run-id prefix), alias races, reruns and structural permission placement; the central qualification preflight is already successful on this head.

Settings hold:

  • Independent API readback currently returns 404 for Environment github-release; it is not installed yet. Before merge/dispatch, create it with custom deployment policies, protected-branches false, and exactly one branch policy main (no tag policy), then ask Security for an independent readback. This avoids the workflow auto-creating an unprotected Environment on first dispatch.
  • Keep dockerhub-native tag-only native-[0-9]* for 3.0.2. Add its separately reviewed plain policy 3.[0-9]*.[0-9]* only after Native releases: plain X.Y.Z tags from 3.0, and a GitHub Release per release #972 merges and before the first plain native dispatch, with another Security readback.

This is exact-head source clearance, not final merge/settings/dispatch clearance. Full hosted checks are still running, github-release is absent, and 3.0.2 remains on the existing native-3.0.2 flow.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Settings step for the redesigned release phase (General, 00:28 UTC): created environment github-release. Readback: custom_branch_policies=true, protected_branches=false, protection rules [branch_policy], exactly one deployment policy 65291521 branch main, and no environment secrets. Pending Security's independent readback. After merge: add dockerhub-native tag policy 3.[0-9]*.[0-9]* (pattern confirmed by Security), keep native-[0-9]*, and read both back before the first plain-tag dispatch.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security independent settings readback: the github-release Environment hold is closed.

  • protected_branches=false, custom_branch_policies=true;
  • protection rules contain exactly the branch-policy rule (ID 71038763);
  • deployment policies contain exactly one entry: ID 65291521, type branch, name main;
  • environment secret count is 0; the reviewed job uses only its scoped GITHUB_TOKEN;
  • independent control readback of dockerhub-native remains unchanged and tag-only: exactly policy 65279581, type tag, native-[0-9]*. No plain policy exists yet.

This matches exact source head ff306b126f196b2cdb9dc39140678dbcc7fa9e92 and closes the pre-merge Environment prerequisite. Final merge clearance still waits this head's full hosted matrix. After merge, add only 3.[0-9]*.[0-9]* to dockerhub-native and request another Security readback before a plain tag dispatch; 3.0.2 remains on native-3.0.2.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security review of exact head ff306b126f196b2cdb9dc39140678dbcc7fa9e92: one hold before source clearance.

native-release-github-release.sh treats any existing Release as success at lines 41–44, before it reads the tag, proves the commit is on main, binds the native-release run, compares the two registries, or verifies provenance. That makes the protected-main workflow green for an accidental, stale, or manually pre-created public Release whose tag/title/prerelease/notes need not correspond to the supplied run or image. It also makes the rerun test exercise a bypass rather than idempotent verification.

Move the existing-Release branch after all tag/run/index/provenance bindings. Then read the existing Release and succeed only when its immutable public tuple matches the verified result (at minimum tag, plain title, candidate/prerelease state, verified index, commit and exact run link); otherwise refuse without editing it. Keep the desired property that a rerun never changes Latest. Add a counterfactual existing Release with a wrong run/digest/title or prerelease state and prove it is refused.

The new workflow's authority envelope otherwise looks correct at this head: one main-only job, the only contents: write, github-release Environment, the shared alias lock, exact release-run ref/SHA/status binding, equal registry index digests, and attestation source/run binding. Independent settings readback is also correct: custom_branch_policies=true, protected_branches=false, exactly policy 65291521 (branch main), and zero environment secrets. The plain dockerhub-native tag policy still correctly waits until this workflow is merged.

rmcdaniel and others added 2 commits November 2, 2026 21:02
An existing Release used to end the step with success before the tag,
run, index or provenance was checked. That made the workflow green for a
stale or hand-made Release that needn't match the run.

The existing-Release check now runs after all bindings. It succeeds only
when the Release records exactly this verified release: tag, plain
title, candidate state, index digest, commit and run link. It never
edits the Release, so a rerun still cannot move Latest; any other
existing Release is refused.

check-native-release-tags.sh accepts reruns of the same final and
candidate. It refuses an existing Release of another run, index or
commit, with another title, or a final marked prerelease. It also checks
that an existing Release does not excuse unverifiable provenance or a
commit off main. Negative control: restoring the early exit fails.

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

Copy link
Copy Markdown
Member Author

Security re-review at exact 3b3336d9cc3316e28fe775b66b790cda31992fe0: the previous ordering hold is fixed—the existing-Release path now runs only after tag/main, run, exact-index and attestation bindings. The live github-release Environment also reads back correctly: custom branch policies on, protected-branches off, exactly policy 65291521 (branch main), and zero environment secrets.

One remaining public-record hold: the new check does not establish that the existing Release is exactly the one this workflow would have created. It requests no isDraft/Latest state, and its body predicate is three independent substring contains checks. Thus a draft Release with the expected tag/title/prerelease bit and an arbitrary body that merely mentions the index, commit and run URL passes and exits successfully, leaving no public Release; similarly an incorrect/multi-tuple body passes. That is especially important because this path is the durable public record and intentionally performs no repair.

Please compare isDraft == false, the exact canonical notes body (or exact anchored parsed fields with no ambiguity), and the expected current Latest state derived under the same alias lock. Add refusals for draft, unexpected extra/multiple body tuples, and incorrect Latest; retain the accepted final/candidate reruns. Also update the workflow comment at lines 16–17, which still says any existing Release is left as-is rather than only an exact verified one.

No Release write is cleared yet.

Security's review of 3b3336d: the existing-Release check ignored draft
and Latest state, and accepted any body that merely contained the index,
commit and run. A draft or ambiguous public record would pass.

The check now runs after the notes and the Latest decision. It accepts
only a published (not draft) Release with this tag, plain title and
candidate state, whose body is exactly the notes this step would write.
releases/latest must name it exactly when the live alias owns latest,
and must not otherwise, read under the same lock. Anything else is
refused unedited.

check-native-release-tags.sh captures the notes the step writes and
accepts reruns of the same final (Latest), candidate and backport (not
Latest). It refuses:
- a draft;
- other or ambiguous notes;
- another title;
- a final marked prerelease;
- a final the alias owns that isn't Latest;
- a backport marked Latest.
An existing Release still never excuses a failed binding. Negative
control: dropping the Latest check fails.

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

Copy link
Copy Markdown
Member Author

Security re-review of exact head 56dcda8: source clear. The existing-Release path now runs only after tag, main ancestry, workflow run, registry index, attestation and live-alias bindings. It refuses drafts, wrong candidate state, noncanonical or ambiguous notes, and a Latest state inconsistent with whether this tag owns the live alias. Exact notes are compared with only trailing-whitespace normalization. The added final, candidate and backport counterfactuals cover both sides. No source hold remains; do not merge or dispatch until this exact head is hosted-green and the recorded environment/settings sequence is complete.

@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security final clearance at exact 56dcda85d76dfc1ea828dfd6cfd343346e79a076:

  • Source remains the cleared split-authority design: tag-ref publication has no contents: write; a protected-main dispatch verifies tag→main ancestry, exact successful native-release run/ref/commit, both registry indexes, attestation signer/ref/digest/run binding, and live aliases under the rolling lock before the isolated Release write. Existing Releases are accepted only as exact immutable reruns, including body and Latest state.
  • Independent settings readback for environment github-release: custom branch policies enabled, protected-branches disabled, exactly one policy (65291521, branch main), and zero environment secrets.
  • All 60 hosted statuses are terminal on this exact head: 59 successful and the expected candidate-cancellation/prepared-local qualification skipped. No failed, cancelled or timed-out checks.

Clear to queue. After merge, dockerhub-native still needs the agreed plain-version tag policy 3.[0-9]*.[0-9]* and an independent Security readback before the first plain-tag dispatch. This clearance does not authorize a dispatch before that settings gate.

@rmcdaniel
rmcdaniel added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 488abb6 Nov 3, 2026
60 checks passed
@rmcdaniel

Copy link
Copy Markdown
Member Author

Security protected-main readback: #972 merged as 488abb672a5bc579c22922cb5ac77550f277329b, and its merge-group checks completed green. github-release remains main-only (65291521, zero environment secrets). dockerhub-native still has exactly the historical tag policy 65279581 native-[0-9]*; the plain 3.[0-9]*.[0-9]* policy has not yet been added.

General may now apply that previously agreed settings mutation. Security will independently read back exactly two tag policies and no branch policy before any plain-tag dispatch. No tag or dispatch clearance yet; #984 still needs its merge-last current-main carry and terminal-green final review.

@rmcdaniel

Copy link
Copy Markdown
Member Author

Security independent settings readback after merge:

  • dockerhub-native: custom_branch_policies=true, protected_branches=false;
  • exactly two deployment policies, both type tag: 65279581 native-[0-9]* and 65310965 3.[0-9]*.[0-9]*;
  • no branch policy and zero environment secrets.

The tag-only settings gate is satisfied for historical native tags and the scoped plain 3.x line. This does not by itself clear a tag or dispatch: the exact 3.0.3 release commit/default still needs source and protected-main readback.

rmcdaniel added a commit to durable-workflow/sample-app that referenced this pull request Nov 3, 2026
Server releases after 3.0.2 label their images with the plain X.Y.Z tag
(durable-workflow/server#972) instead of native-X.Y.Z. The check failed
silently under set -e on 3.0.4, before any scenario ran, and now accepts
either form and names a mismatch.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant