Skip to content

ci: upload SLSA provenance by release ID, not via the generator's auto-upload#225

Merged
bomly-guy merged 2 commits into
mainfrom
ci/fix-provenance-by-release-id
Jun 30, 2026
Merged

ci: upload SLSA provenance by release ID, not via the generator's auto-upload#225
bomly-guy merged 2 commits into
mainfrom
ci/fix-provenance-by-release-id

Conversation

@bomly-guy

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #223. The v0.15.4 release run failed the same way again (run 28432642577), but with a much worse side effect than the first failure.

Root cause: the provenance job's upload-assets: true relies on softprops/action-gh-release, which resolves its target release by tag. GitHub's "get release by tag" REST endpoint does not return draft releases — they aren't associated with a tag ref until published. Against our still-draft release (from #223's design), it found nothing, and instead of failing cleanly it created a second, non-draft release for the same tag, then failed to upload to that one too (immediately immutable on creation).

The real damage: that second release's mere creation permanently marked the v0.15.4 tag as immutable at the platform level — confirmed in production. Deleting the bad release afterward did not free the tag back up; the original, correct draft (with all 23 real assets) could never be published under that tag. Recovery meant abandoning v0.15.4 entirely and cutting v0.15.5.

Fix

Stop relying on the SLSA generator's built-in upload entirely — it has no way to target a draft release.

  • provenance job: upload-assets is now false. The generator still produces the multiple.intoto.jsonl artifact (available via actions/download-artifact and needs.provenance.outputs.provenance-name), it just doesn't try to attach it to a release.
  • publish job: downloads that artifact, resolves the draft release by listing releases and filtering for the matching tag with draft == true (client-side, never via the by-tag endpoint), uploads the file directly via the GitHub REST API (uploads.github.com, by release ID), then publishes. No tool ever gets a chance to "helpfully" create a duplicate release again.
  • The tag name now flows into the jq filter via jq --arg instead of string-interpolation into the filter expression — same class of injection concern CodeRabbit flagged on the shell command in ci: keep release draft until provenance lands, fixing immutable-release upload failure #223, just in jq syntax this time, so fixed proactively rather than waiting for another round-trip.
  • dev-docs/CI.md documents the failure mode and why the fix avoids it (including the "tag gets permanently poisoned" detail, since that's the genuinely sharp edge here).

Recovery already done (outside this PR)

  • Deleted the duplicate empty published release and the now-permanently-stuck orphaned draft for v0.15.4.
  • v0.15.4 is unrecoverable as a tag; the next Auto Version run will need to cut v0.15.5.

Verification

  • goreleaser check / make test pass locally (only the pre-existing brews deprecation warning remains).
  • Sanity-checked the jq --arg draft-selection logic against a synthetic JSON array containing both a draft and a published release for the same tag — correctly selects only the draft's ID.
  • Verified the actual provenance file recovered from the failed run (multiple.intoto.jsonl) against the real SHA256SUMS already on the draft release — hashes matched exactly for all 20 subjects, confirming the artifact itself was always correct; only the upload mechanism was broken.

What I could not test locally: the draft lookup → upload → publish sequence end-to-end, since it depends on GitHub Actions' artifact passing and the real REST API. Needs verification on the next real tagged release.

Test plan

  • goreleaser check / make test pass locally
  • jq --arg draft-selection logic sanity-checked against synthetic dual-release data
  • Next release (v0.15.5): release job creates a draft, provenance job produces the artifact without attempting upload, publish job finds the draft by ID, uploads provenance, and publishes successfully
  • No duplicate or orphaned release appears for the new tag
  • notify-landing-yank.yml fires correctly off the release: published event

🤖 Generated with Claude Code

…o-upload

The v0.15.4 release run failed the same way again, but with a worse
side effect: the provenance job's upload-assets: true relies on
softprops/action-gh-release, which finds the target release by tag.
GitHub's "get release by tag" API does not return draft releases, so
against our still-draft release it found nothing and created a
*second*, non-draft release for the same tag instead of failing
cleanly. That second release immediately and permanently marked the
tag as immutable -- confirmed in production: deleting the bad release
afterward did not free the tag back up, and the original (correct)
draft, with all 23 assets, could never be published under that tag.
Recovery required abandoning v0.15.4 and cutting v0.15.5.

Fix: stop relying on the generator's built-in upload entirely.

- provenance job: upload-assets is now false. The generator still
  produces the multiple.intoto.jsonl artifact, just doesn't try to
  attach it to a release.
- publish job: downloads the provenance artifact, resolves the draft
  release by listing releases and filtering for the matching tag with
  draft == true (never by tag name), uploads the file directly via the
  GitHub REST API, then publishes. This never gives any tool a chance
  to "helpfully" create a duplicate release.
- Tag name flows through env + jq --arg rather than string
  interpolation into a --jq filter, avoiding the same class of
  injection issue CodeRabbit flagged on the shell command earlier in
  this PR.
- dev-docs/CI.md documents the failure mode and why the fix avoids it.

Validated locally: goreleaser check / make test pass, and the jq
selection logic was sanity-checked against a synthetic
draft+published pair for the same tag. The actual draft lookup,
upload, and publish sequence can only be verified by a real release
run 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

Warning

Review limit reached

@bomly-guy, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4016e717-ca15-4700-b413-f5025ddef1a7

📥 Commits

Reviewing files that changed from the base of the PR and between c17951d and 4ab8468.

📒 Files selected for processing (2)
  • .github/workflows/release.yml
  • dev-docs/CI.md
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/fix-provenance-by-release-id

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 c17951d22274615b5a6f0d9e04d86174f085d8d1 to 4ab8468338119615012f849b80da8805a2d2143b.

Overview

Status Manifests Dependencies Findings Duration
⚠️ Warnings +0 / ~1 / -0 +1 / ~0 / -0 1 introduced / 0 persisted / 0 resolved 1m 0s

Dependency Changes

Summary: 1 added, 0 changed, 0 removed.

Added Dependencies

Change Package Version Direct? Scope Licenses
added actions:download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c Yes runtime -

Vulnerabilities

✅ No vulnerability changes.

License Changes

✅ No license changes.

Project Posture

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

Policy Findings

Summary: 1 introduced, 0 persisted, 0 resolved.

Introduced Findings

Status Category Severity ID Package Fixed In Title
⚠️ introduced license WARNING UNKNOWN-3egl-t4dh-eqo3 download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c - Package license is unknown

Legend: ✅ resolved · ❌ failing · ⚠️ warning

@bomly-guy bomly-guy merged commit 1e04a49 into main Jun 30, 2026
13 checks passed
@bomly-guy bomly-guy deleted the ci/fix-provenance-by-release-id branch June 30, 2026 09:49
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