Skip to content

ci: keep release draft until provenance lands, fixing immutable-release upload failure#223

Merged
bomly-guy merged 2 commits into
mainfrom
ci/fix-immutable-release-draft-publish
Jun 30, 2026
Merged

ci: keep release draft until provenance lands, fixing immutable-release upload failure#223
bomly-guy merged 2 commits into
mainfrom
ci/fix-immutable-release-draft-publish

Conversation

@bomly-guy

@bomly-guy bomly-guy commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #222. The first real release attempt with the new Signed-Releases pipeline (run 28429258009) failed in the provenance job:

##[error]Cannot upload assets to an immutable release. - https://docs.github.com/rest

Root cause: GitHub's immutable releases feature (GA Oct 2025) is enabled on this org/repo. Once a release is published, no further assets can be added — by anyone. My PR #222 design published the release inside the release job (GoReleaser, draft: false), then tried to attach multiple.intoto.jsonl from a separate downstream provenance job — which is exactly the pattern immutable releases forbids.

This is unrelated to the earlier 404 Not Found upload failure on the same tag (that was a transient GitHub API hiccup during the release job itself, already resolved by deleting the stale draft and re-running).

Fix

Matches GitHub's own recommended workflow: create as draft → attach all assets → publish once.

  • .goreleaser.yaml: release.draft is now true. GoReleaser creates the release and uploads its assets without publishing.
  • release.yml: the provenance job now uploads to the still-draft release (drafts are exempt from the immutability restriction — confirmed in GitHub's docs). A new publish job, gated on needs: [release, provenance], mints a fresh release-bot app token and runs gh release edit --draft=false once both finish — preserving the existing requirement that publishing use an app-attributed token so release: published cascades to notify-landing-yank.yml (default GITHUB_TOKEN-triggered events don't start new workflow runs).
  • dev-docs/CI.md: documents the draft → provenance → publish sequencing and why.

Known tradeoff

The Homebrew/Scoop/WinGet manifest PRs are opened by GoReleaser before publish (same as before), so their release-asset URLs are briefly (well under a minute, until the publish job finishes) not yet publicly downloadable. Documented in dev-docs/CI.md; not fixed here since it's a narrow window with low practical impact — package-manager bots/reviewers don't act on a PR within seconds of it opening.

Verification

  • goreleaser check accepts the config (only the pre-existing, unrelated brews deprecation warning remains).
  • make test passes.
  • Confirmed via GitHub's docs that draft releases are exempt from the immutable-releases asset-upload restriction.

What I could not test locally: the draft → provenance → publish → release: published cascade can only be verified by a real tagged release running in GitHub Actions.

Test plan

  • goreleaser check / make test pass locally
  • Next release attempt (re-running on the existing v0.15.3 tag, or a fresh tag): release job creates a draft, provenance job successfully attaches multiple.intoto.jsonl, publish job flips it to published
  • notify-landing-yank.yml fires correctly off the release: published event from the publish job's app token
  • Released assets (binaries, SHA256SUMS, SHA256SUMS.sigstore.json, multiple.intoto.jsonl) are all present and downloadable once published

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Release artifacts are now created as drafts first, then published only after provenance is attached and validation completes.
    • GitHub release publication now happens in a dedicated final step for tagged releases.
  • Documentation

    • Updated CI and release-process guidance to match the new draft → provenance → publish flow.
    • Clarified when release downloads become publicly available and how signed release assets are produced.

…se upload failure

The v0.15.3 release run (#222's first real release) failed: the SLSA
provenance job tried to upload multiple.intoto.jsonl to the release
after GoReleaser had already published it, and GitHub's immutable
releases feature (GA Oct 2025) rejects asset uploads to any release
once published — "Cannot upload assets to an immutable release."

Per GitHub's own guidance, the fix is to keep the release as a draft
until every asset is attached, and publish last:

- .goreleaser.yaml: release.draft is now true. GoReleaser creates the
  release and uploads its assets (archives, packages, checksums,
  signature) without publishing.
- release.yml: the provenance job now uploads to the still-draft
  release (drafts are exempt from the immutability restriction). A new
  `publish` job, gated on `needs: [release, provenance]`, mints a fresh
  release-bot app token and runs `gh release edit --draft=false` once
  both are done — preserving the existing requirement that publishing
  use an app-attributed token so `release: published` cascades to
  notify-landing-yank.yml.
- dev-docs/CI.md: documents the new draft -> provenance -> publish
  sequencing and the immutable-releases constraint driving it.

Known minor tradeoff: the Homebrew/Scoop/WinGet manifest PRs are
opened by GoReleaser before publish, so their release-asset URLs are
briefly (well under a minute) not yet publicly downloadable until the
`publish` job finishes. Documented in dev-docs/CI.md; not fixed here
since it's a narrow timing window with low practical impact.

Validated locally: `goreleaser check` accepts the config (only the
pre-existing, unrelated `brews` deprecation warning remains), and
`make test` passes. The draft/publish/token-cascade behavior itself
can only be verified by a real tagged release in GitHub Actions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/fix-immutable-release-draft-publish

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Bomly Diff Summary

Compared d40bcb91fda70f2a89a50eb59960e483d69f6d2d to 8c71b5b1aebf2d7c9f46d0d150f9dbc29171d180.

Overview

Status Manifests Dependencies Findings Duration
✅ Pass +0 / ~0 / -0 +0 / ~0 / -0 0 introduced / 0 persisted / 0 resolved 1m 6s

Dependency Changes

✅ No dependency changes.

Vulnerabilities

✅ No vulnerability changes.

License Changes

✅ No license changes.

Project Posture

✅ No project posture changes (--matchers +scorecard was not selected).

Policy Findings

✅ No policy differences were identified.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release.yml:
- Around line 144-159: The Publish release step is interpolating github.ref_name
directly in the shell command, which can allow shell command substitution in a
malicious tag name. Update the release job in release.yml by passing the tag
through env alongside GH_TOKEN and change the gh release edit invocation to use
a quoted environment variable instead of github.ref_name directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c2952826-e8c7-477d-a046-ae06d2f58c41

📥 Commits

Reviewing files that changed from the base of the PR and between d40bcb9 and 42c857d.

📒 Files selected for processing (3)
  • .github/workflows/release.yml
  • .goreleaser.yaml
  • dev-docs/CI.md

Comment thread .github/workflows/release.yml Outdated
Comment on lines +144 to +159
- name: Publish release
# Finalizes the draft into a published release now that every asset —
# GoReleaser's archives/packages/checksums/signature, plus the SLSA
# provenance file from the `provenance` job — is attached. Must run
# last: immutable releases reject new assets once published.
#
# Uses the release-bot app token, not the default GITHUB_TOKEN: GitHub
# does not start new workflow runs from events triggered by the
# default token, so a release published with it would never fire the
# `release: published` trigger on "Release lifecycle sync"
# (notify-landing-yank.yml) — and the landing-page docs sync would
# silently never run. An app token is attributed to the app, so the
# publish event cascades.
env:
GH_TOKEN: ${{ steps.release-token.outputs.token }}
run: gh release edit "${{ github.ref_name }}" --draft=false --repo bomly-dev/bomly-cli

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Workflow excerpt:\n'
sed -n '140,162p' .github/workflows/release.yml

printf '\nGit ref-name constraint probe:\n'
python3 - <<'PY'
import subprocess, shlex, textwrap

tests = [
    "safe-tag",
    "v1.2.3",
    "tag with space",
    "tag$(id)",
    "tag`id`",
    "tag;id",
    "tag:id",
    "tag^id",
    "tag?id",
    "tag[id]",
    "tag~id",
    "tag@{id}",
]

for t in tests:
    # check-ref-format validates refs, so use refs/tags/<name>
    ref = f"refs/tags/{t}"
    p = subprocess.run(
        ["git", "check-ref-format", "--allow-onelevel", ref],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    print(f"{ref!r}: {'OK' if p.returncode == 0 else 'INVALID'}")
PY

Repository: bomly-dev/bomly-cli

Length of output: 1621


Avoid interpolating github.ref_name in run github.ref_name is expanded before Bash parses the line, so a tag like tag$(id) can trigger command substitution here. Pass it through env: and use "$RELEASE_TAG" instead.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 159-159: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 144 - 159, The Publish release
step is interpolating github.ref_name directly in the shell command, which can
allow shell command substitution in a malicious tag name. Update the release job
in release.yml by passing the tag through env alongside GH_TOKEN and change the
gh release edit invocation to use a quoted environment variable instead of
github.ref_name directly.

Source: Linters/SAST tools

Address CodeRabbit/zizmor finding on #223: github.ref_name was
interpolated directly into the run: shell command, which expands
before Bash parses the line. Pass it through env instead and reference
it as a quoted shell variable, so a crafted tag name can't inject
shell commands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bomly-guy bomly-guy merged commit 5fe9c8a into main Jun 30, 2026
13 checks passed
@bomly-guy bomly-guy deleted the ci/fix-immutable-release-draft-publish branch June 30, 2026 08:58
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