Skip to content

ci: use draft releases to support immutable GitHub releases#516

Open
keelerm84 wants to merge 3 commits intomainfrom
devin/1774991616-immutable-releases
Open

ci: use draft releases to support immutable GitHub releases#516
keelerm84 wants to merge 3 commits intomainfrom
devin/1774991616-immutable-releases

Conversation

@keelerm84
Copy link
Copy Markdown
Member

@keelerm84 keelerm84 commented Mar 31, 2026

Summary

Migrates the release workflow to support GitHub's immutable releases feature. Once a release is published it can no longer be modified, so we now create releases in draft state, upload all artifacts, and only then publish.

Changes across three files:

  1. release-please-config.json — Added top-level "draft": true so release-please creates draft releases for all packages. Added "force-tag-creation": true to every package (not yet supported by release-please, but included for forward compatibility).
  2. release-please.yml — Split release-please pattern — release-please is now invoked twice within the same job:
    • First call with skip-github-pull-request: true — only creates releases (no PRs).
    • Inline tag creation — if any package was released, checks out the repo and creates git tags manually (release-please skips tag creation for drafts). Iterates over all 4 packages (client, server, server-redis, server-otel) using env vars to avoid script injection. Idempotent — skips if the tag already exists.
    • Second call with skip-github-release: true — only creates/updates PRs, and only runs if no releases were created. This ordering ensures tags exist before release-please checks whether a release PR is still needed.
    • All downstream artifact jobs (release-client, release-server, etc.) now depend only on release-please (the former separate create-tags job has been removed).
    • Action pinned to googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 (v4.4.0).
  3. SLSA → actions/attest@v4 — Removed all 7 SLSA provenance jobs (release-{client,server,server-redis}-provenance, release-{client,server,server-redis}-mac-arm64-provenance, plus 2 in the manual workflow). Replaced with inline actions/attest@v4 steps in each build job that decode the base64 hashes into a checksums file and attest in-place.
  4. publish-release-* jobs — Three new jobs (publish-release-client, publish-release-server, publish-release-server-redis) that un-draft their respective release only after all artifact jobs complete.
  5. manual-sdk-release-artifacts.ymlpublish_release input — Added a publish_release boolean input (default: true) and a publish-release job gated on it, so operators can optionally keep the release in draft after manual artifact uploads.

Review & Testing Checklist for Human

  • Split release-please ordering: The two release-please invocations must run in this exact order — releases first, then PRs. If the second call (PR creation) ran when a release was created, it would see no tag and open a duplicate release PR. Verify the if condition on the second call correctly uses != 'true' with && across all 4 packages.
  • needs arrays in publish-release-* jobs: Verify each publish job waits on ALL artifact-uploading jobs for that package before un-drafting. A missing dependency means the release gets published before all artifacts are uploaded — the exact problem we're solving. For example, publish-release-client needs both release-client (3-OS matrix) and release-client-mac-arm64.
  • server-otel has no publish-release job: release-please creates a tag for server-otel if released, and "draft": true means release-please creates a draft release, but there is no publish-release-server-otel job to un-draft it. If server-otel has no artifact uploads this is fine — but confirm the draft release won't stay stuck.
  • actions/attest@v4 (unpinned): The attest action is referenced by major version tag, not a pinned SHA. Verify this aligns with the repo's policy on action pinning (other actions like checkout are SHA-pinned).
  • End-to-end test: Trigger a test release (or review a prior release's output) to confirm: (a) draft release is created, (b) tags are pushed, (c) artifacts upload successfully, (d) attestation succeeds, (e) release is un-drafted. Also test the manual workflow with publish_release: false to verify the release stays in draft.

Notes

  • This follows the split release-please pattern established in ld-relay (commit 1581de9). The key insight is that release-please depends on the tag existing when determining if a release PR is still needed — so tags must be created between the release step and the PR step.
  • The ${{ github.repository }} expression appears in run: blocks (tag creation and publish-release jobs). This value is GitHub-controlled (not user input) so script injection risk is negligible, but worth noting since tag names are deliberately routed through env vars.
  • force-tag-creation has no effect with the current release-please version — it is a forward-compatibility placeholder that will take effect once release-please supports it, at which point the inline tag creation steps can be removed.
  • manual-sdk-release-artifacts.yml's publish_release defaults to true for workflow_dispatch, matching the expectation that manual runs typically want to finalize the release.

Link to Devin session: https://app.devin.ai/sessions/7d5bda4d9dbe4ae0b950b30a50485e60
Requested by: @keelerm84

@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@kinyoklion kinyoklion marked this pull request as ready for review April 1, 2026 17:59
@kinyoklion kinyoklion requested a review from a team as a code owner April 1, 2026 17:59
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

run: >
gh release edit "$TAG_NAME"
--repo ${{ github.repository }}
--draft=false
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Server-otel draft release never gets published

High Severity

The global "draft": true setting causes release-please to create draft releases for all packages, and the create-tags job explicitly creates a tag for server-otel when released. However, there is no publish-release-server-otel job to un-draft the release. The server-otel release will remain permanently invisible as a draft on GitHub, which is a regression from pre-PR behavior where release-please published it directly.

Additional Locations (2)
Fix in Cursor Fix in Web

fi
shell: bash
- name: Attest build provenance
uses: actions/attest@v4
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Attest action unpinned unlike all other actions

Medium Severity

All 8 instances of actions/attest@v4 are referenced by a mutable major-version tag, while every other action in both workflows (actions/checkout, googleapis/release-please-action) is pinned to a specific commit SHA. A compromised or force-pushed v4 tag could inject malicious code into the release pipeline, which runs with contents: write, attestations: write, and id-token: write permissions.

Additional Locations (1)
Fix in Cursor Fix in Web

elif [ -n "${HASHES_MACOS}" ]; then
echo "${HASHES_MACOS}" | base64 -d > checksums.txt
fi
shell: bash
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

macOS base64 -d triggers debug, not decode

High Severity

On macOS, BSD base64 -d means --debug (verbose logging), not --decode (which requires uppercase -D). The sdk-release action already handles this asymmetry when encoding (base64 -w0 on Linux vs base64 -b 0 on macOS), but all new decoding steps use GNU-style base64 -d universally. On macOS runners, this produces incorrect checksums.txt content, causing the attestation step to fail. Since the matrix job then fails, publish-release-* jobs are blocked and releases remain permanently in draft.

Additional Locations (2)
Fix in Cursor Fix in Web

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant