ci: keep release draft until provenance lands, fixing immutable-release upload failure#223
Conversation
…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>
|
Caution Review failedAn error occurred during the review process. Please try again later. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Bomly Diff SummaryCompared Overview
Dependency Changes✅ No dependency changes. Vulnerabilities✅ No vulnerability changes. License Changes✅ No license changes. Project Posture✅ No project posture changes ( Policy Findings✅ No policy differences were identified. |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
.github/workflows/release.yml.goreleaser.yamldev-docs/CI.md
| - 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 |
There was a problem hiding this comment.
🔒 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'}")
PYRepository: 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>
Summary
Follow-up to #222. The first real release attempt with the new Signed-Releases pipeline (run 28429258009) failed in the
provenancejob: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
releasejob (GoReleaser,draft: false), then tried to attachmultiple.intoto.jsonlfrom a separate downstreamprovenancejob — which is exactly the pattern immutable releases forbids.This is unrelated to the earlier
404 Not Foundupload failure on the same tag (that was a transient GitHub API hiccup during thereleasejob 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.draftis nowtrue. GoReleaser creates the release and uploads its assets without publishing.release.yml: theprovenancejob now uploads to the still-draft release (drafts are exempt from the immutability restriction — confirmed in GitHub's docs). A newpublishjob, gated onneeds: [release, provenance], mints a fresh release-bot app token and runsgh release edit --draft=falseonce both finish — preserving the existing requirement that publishing use an app-attributed token sorelease: publishedcascades tonotify-landing-yank.yml(defaultGITHUB_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
publishjob finishes) not yet publicly downloadable. Documented indev-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 checkaccepts the config (only the pre-existing, unrelatedbrewsdeprecation warning remains).make testpasses.What I could not test locally: the draft → provenance → publish →
release: publishedcascade can only be verified by a real tagged release running in GitHub Actions.Test plan
goreleaser check/make testpass locallyv0.15.3tag, or a fresh tag):releasejob creates a draft,provenancejob successfully attachesmultiple.intoto.jsonl,publishjob flips it to publishednotify-landing-yank.ymlfires correctly off therelease: publishedevent from thepublishjob's app tokenSHA256SUMS,SHA256SUMS.sigstore.json,multiple.intoto.jsonl) are all present and downloadable once published🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation