Skip to content

Make published CLI releases immutable without breaking asset publication #64

Closed

Description

@rmcdaniel

Problem

The published CLI is an existing customer-installable surface. Its release workflow builds exact-source artifacts, generates SHA256SUMS, emits GitHub build-provenance attestations, and verifies public downloads. The repository-level immutable-release control is currently disabled, so future published-release assets remain editable by repository administrators.

The exact pinned softprops/action-gh-release source was checked before changing the setting. For stable releases it already creates a draft, uploads assets, and then publishes. For prereleases, however, it creates a published prerelease unless the workflow explicitly requests a draft; its own diagnostic explains that immutable repositories require a separate draft-then-publish flow for prerelease assets.

Therefore the repository setting must remain unchanged until prerelease publication is compatible. This is future hardening of the existing release channel, not a Security gate on CLI or other pre-RC development.

Acceptance

  • Preserve the existing stable release flow and exact-source, checksum, attestation, and public-download checks.
  • At a natural release-tooling checkpoint, make prerelease publication upload its complete asset set before publication without changing resolver metadata or tag/source rules.
  • Verify stable, prerelease, rerun, and latest-release behavior in focused tests.
  • Then enable immutable releases for durable-workflow/cli and verify the repository API reports the control enabled.
  • Do not mutate or delete an existing release to demonstrate the control.
  • Record the first subsequent protected release as end-to-end evidence when it occurs.

No current development or release candidate waits on this issue.

Activity

  1. added
    authority:githubGitHub is the authoritative lifecycle record for this work
    kind:defectA public product behavior is incorrect
    priority:P2Normal-priority product work
    repo:cliOwned by the CLI repository
    completion:evidence-requiredClose only after all explicit acceptance and operational evidence is public
    status:in-progressApproved work is actively being implemented or validated
    status:readyReady for implementation
    priority:P3Lower-priority product improvement
    and removed
    priority:P2Normal-priority product work
    status:in-progressApproved work is actively being implemented or validated
    on Oct 26, 2026
  2. rmcdaniel commented on Oct 26, 2026

    @rmcdaniel
    MemberAuthor

    Stable 2.3.0 artifact-integrity verification — 2026-10-26 UTC

    Security independently downloaded the complete public 2.3.0 release asset set. The published SHA256SUMS file verifies all nine other assets: four standalone binaries, the PHAR, Homebrew formula, both installers, and verify-release.sh.

    The host GitHub CLI 2.45.0 predates the attestation verifier, so no host package was changed. Instead, official GitHub CLI 2.103.3 was downloaded into a bounded temporary directory and accepted against its official published checksum. Its trusted root and each public attestation bundle were then retained temporarily for offline verification.

    All ten assets, including SHA256SUMS, independently passed cryptographic SLSA provenance verification with exactly one accepted attestation each. Verification enforced:

    • repository durable-workflow/cli;
    • signer workflow durable-workflow/cli/.github/workflows/release.yml;
    • source ref refs/tags/2.3.0; and
    • exact source commit 1762a78453b64dc1a1ffa9c59b81d8b412ee06b9.

    No downloaded executable was run. No release asset, tag, source, repository setting, workflow, or channel metadata changed. Temporary downloads, bundles, trusted-root data, and verifier are removed after this receipt.

    This confirms the existing 2.3.0 customer-installable surface has working checksums and provenance. #64 remains only the future immutable-release setting/prerelease-flow hardening already classified P3 and nonblocking; nothing in this verification adds a release or development gate.

  3. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    2.3.0 post-publication recovery acceptance — 2026-10-27 03:46 UTC

    This closes the misleading red status on the original stable publisher run without changing the release, tag, assets, channel, workflow, repository settings, or future #64 work.

    The exact tag-push run 39095673458 used commit 1762a78453b64dc1a1ffa9c59b81d8b412ee06b9. Source resolution, release preflight, all four platform builds, the immutable producer handoff, handoff digest validation, release-source verification, SLSA attestation, and GitHub Release creation all passed. Only the combined post-publication Verify public release downloads step failed, five seconds after publication.

    The run's retained release-evidence artifact (12123479946, artifact SHA-256 541a439e9b7f867993854316c3a76778efb6919c87a842d5d15b4d4abdf021e8) proves the workflow had already downloaded all ten supported public assets successfully at 2026-10-18T04:06:08Z, bound to tag/ref 2.3.0 and the exact release commit. The step therefore failed later in its installer/channel/upgrade acceptance, not during artifact creation, attestation, upload, or public-download availability.

    Fresh recovery repeated the unchanged 2.3.0 acceptance paths against the immutable public release:

    • all ten anonymous public download endpoints passed;
    • the published SHA256SUMS verified all nine other assets;
    • GitHub's attestation service returns one SLSA v1 statement covering all ten exact public digests, with builder/workflow durable-workflow/cli/.github/workflows/release.yml@refs/tags/2.3.0 and resolved source commit 1762a78453b64dc1a1ffa9c59b81d8b412ee06b9 (the independent cryptographic verification is already recorded above);
    • the downloaded Linux x86_64 binary reported dw 2.3.0 and the exact shortened release commit in a network-disabled, read-only, unprivileged container;
    • the version-pinned installer and the default stable-channel installer both downloaded, checksum-verified, installed, selected, and executed that same binary with status=ready in bounded unprivileged containers;
    • the unchanged release-channel verifier resolved stable to 2.3.0; both dw upgrade --dry-run and apply resolved 2.3.0 and passed the unchanged exact result verifier as noop.

    The original failure is therefore consistent with immediate post-create channel propagation, not an artifact-integrity or customer-installability defect. CLI 2.3.0 remains accepted. #64 stays future nonblocking immutable-release/prerelease hardening; there is no incident, republish, credential action, operator action, or development/release gate.

  4. rmcdaniel commented on Nov 1, 2026

    @rmcdaniel
    MemberAuthor

    Security source/API review against exact CLI main 639355dc92d49825b4806ee39b4e4203a31218f1: the issue remains correctly scoped and nonblocking, and the safe implementation can preserve the stable path unchanged.

    Current evidence:

    • The official repository endpoint (GET /repos/durable-workflow/cli/immutable-releases, API version 2026-03-10) returns {"enabled":false,"enforced_by_owner":false}. No setting was changed.
    • Existing stable release 2.3.0 reports immutable:false, as expected because GitHub applies the control only to future releases after enablement.
    • The exact workflow still pins softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 (v3.0.2) and passes prerelease/make_latest but no draft input.
    • The exact pinned action source computes a newly created release as draft = prerelease === true ? input_draft === true : true. Therefore the existing stable path already creates a draft, uploads assets, then finalizes it; the existing prerelease path creates a published prerelease first and then attempts asset upload. This is the precise immutable-release incompatibility.
    • The same pinned action exposes the concrete release id, and its v3.0.2 source supports discovering/reusing an existing draft. GitHub's official immutable-release guidance requires create draft → attach all assets → publish draft: https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases . Exact action source: https://github.com/softprops/action-gh-release/blob/3d0d9888cb7fd7b750713d6e236d1fcb99157228/src/github.ts .

    Minimal safe source design:

    1. Leave the current stable release step semantically unchanged and gate it to prerelease != true.
    2. For a prerelease only, invoke the same pinned action with the complete dist/* set, draft: true, prerelease: true, make_latest: false, and retain its output id.
    3. Before publication, read that exact release ID and require: expected tag/name, draft:true, prerelease:true, no latest designation, the complete expected ten public asset names, every asset in uploaded state, and matching size/digest evidence for the local staged files. Do not resolve an arbitrary draft by title at this boundary.
    4. Re-run the existing immutable tag/source check immediately before finalization.
    5. Publish that exact release ID with draft:false, prerelease:true, make_latest:false; then retain the existing public-download, checksum, installer/channel and attestation acceptance. A direct ID-bound API update is simpler than a second asset-capable action invocation and cannot accidentally select a different draft.
    6. Preserve the current rerun split: a fully public asset set goes through metadata reconciliation without replacing assets; an interrupted draft run may be resumed only after the exact draft/tag/assets are revalidated. Unexpected duplicate or partially foreign draft state must fail closed rather than be published.

    Focused contracts should prove:

    • stable continues through the existing one-call draft/upload/finalize path;
    • prerelease is not public and cannot become latest before every asset is present;
    • a failure before finalization leaves an editable draft, not a partial public release;
    • rerun resumes the validated draft without duplicate publication or asset replacement after publication;
    • wrong release ID, moved tag, missing/extra asset, digest mismatch, non-draft state and make_latest:true all refuse finalization; and
    • stable/prerelease metadata and latest-release selection remain unchanged after publication.

    Only after that source is merged and qualified should the repository setting be enabled, followed by a readback of enabled:true and the first subsequent protected release acceptance. Do not mutate an existing release as a test.

    Local non-publishing checks: the current release-metadata Node suite passes 5/5, and git diff --check passes. The clean isolated worktree has no PHP runtime, so I did not install tooling merely to repeat hosted PHP contracts.

    No release, tag, asset, repository setting, workflow, credential or channel metadata changed. General retains implementation and setting ownership; Security's next step is exact-head review of that source/contract change, not another read of the unchanged setting.

  5. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    All acceptance criteria are now supported by the existing merged changes and protected release evidence. Closing this completed hardening issue.

    This reconciliation uses existing qualification evidence. It changes no source, repository setting, tag, release or asset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    authority:githubGitHub is the authoritative lifecycle record for this workcompletion:evidence-requiredClose only after all explicit acceptance and operational evidence is publickind:defectA public product behavior is incorrectpriority:P3Lower-priority product improvementrepo:cliOwned by the CLI repositorystatus:readyReady for implementation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions