Skip to content

Commit 9373dad

Browse files
keelerm84kinyoklionbeekld
authored
ci: use draft releases to support immutable GitHub releases (#516)
## 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.yml` — `publish_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)](launchdarkly/ld-relay@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 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes release publishing order and provenance model; mis-timed publish jobs could finalize releases before all artifacts upload, and otel publish runs without waiting on artifact jobs. > > **Overview** > Supports **immutable GitHub releases** by having release-please create **draft** releases (`draft` + `force-tag-creation` in `release-please-config.json`), uploading artifacts while still draft, then **`gh release edit --draft=false`** only after all build jobs finish (`publish-release-*` in `release-please.yml`, optional `publish_release` on the manual workflow). > > **Provenance** moves from separate **SLSA** jobs to per-platform **`actions/attest`** steps that decode base64 artifact hashes into `checksums.txt` (with macOS vs GNU `base64` handling). Build jobs gain `attestations` / `id-token` permissions; matrix jobs no longer export hash outputs for downstream SLSA. > > **release-please** is bumped to **v5.0.0** (single run; tags for drafts via `force-tag-creation`). **server-sdk-dynamodb** is wired through automated release (outputs, matrix + arm64 jobs, publish job) and the manual artifact workflow target list. > > **sdk-release** sets `CURL_ROOT` / `CMAKE_PREFIX_PATH` on Linux/macOS non-CURL builds so **aws-sdk-cpp** (dynamodb) can satisfy `find_package(CURL)` during CMake configure. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 722ee53. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Co-authored-by: Bee Klimt <bklimt@launchdarkly.com>
1 parent 18028aa commit 9373dad

4 files changed

Lines changed: 355 additions & 131 deletions

File tree

.github/actions/sdk-release/action.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ runs:
5959
WORKSPACE: ${{ inputs.sdk_path }}
6060
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
6161
OPENSSL_ROOT_DIR: ${{ steps.install-openssl.outputs.OPENSSL_ROOT_DIR }}
62+
# aws-sdk-cpp (dynamodb-source) calls find_package(CURL) on Linux/macOS, regardless of LD_CURL_NETWORKING.
63+
CURL_ROOT: ${{ steps.install-curl.outputs.CURL_ROOT }}
64+
CMAKE_PREFIX_PATH: ${{ steps.install-curl.outputs.CURL_ROOT }}
6265

6366
- name: Build Linux Artifacts (CURL)
6467
if: runner.os == 'Linux'
@@ -253,6 +256,9 @@ runs:
253256
WORKSPACE: ${{ inputs.sdk_path }}
254257
BOOST_ROOT: ${{ steps.install-boost.outputs.BOOST_ROOT }}
255258
OPENSSL_ROOT_DIR: ${{ steps.install-openssl.outputs.OPENSSL_ROOT_DIR }}
259+
# aws-sdk-cpp (dynamodb-source) calls find_package(CURL) on Linux/macOS, regardless of LD_CURL_NETWORKING.
260+
CURL_ROOT: ${{ steps.install-curl.outputs.CURL_ROOT }}
261+
CMAKE_PREFIX_PATH: ${{ steps.install-curl.outputs.CURL_ROOT }}
256262

257263
- name: Build Mac Artifacts (CURL)
258264
if: runner.os == 'macOS'

.github/workflows/manual-sdk-release-artifacts.yml

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ on:
1717
- libs/server-sdk:launchdarkly-cpp-server
1818
- libs/server-sdk-redis-source:launchdarkly-cpp-server-redis-source
1919
- libs/server-sdk-dynamodb-source:launchdarkly-cpp-server-dynamodb-source
20+
publish_release:
21+
description: 'Publish (un-draft) the release after all artifacts are uploaded?'
22+
type: boolean
23+
required: false
24+
default: true
2025

2126
name: Publish SDK Artifacts
2227

@@ -41,10 +46,10 @@ jobs:
4146
# Each of the platforms for which release-artifacts need generated.
4247
os: [ ubuntu-22.04, windows-2022, macos-15-large ]
4348
runs-on: ${{ matrix.os }}
44-
outputs:
45-
hashes-linux: ${{ steps.release-sdk.outputs.hashes-linux }}
46-
hashes-windows: ${{ steps.release-sdk.outputs.hashes-windows }}
47-
hashes-macos: ${{ steps.release-sdk.outputs.hashes-macos }}
49+
permissions:
50+
contents: write
51+
attestations: write
52+
id-token: write
4853
steps:
4954
# https://github.com/actions/checkout/releases/tag/v4.3.0
5055
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
@@ -59,12 +64,34 @@ jobs:
5964
github_token: ${{secrets.GITHUB_TOKEN}}
6065
sdk_path: ${{ needs.split-input.outputs.sdk_path}}
6166
sdk_cmake_target: ${{ needs.split-input.outputs.sdk_cmake_target}}
62-
67+
- name: Generate checksums file
68+
env:
69+
HASHES_LINUX: ${{ steps.release-sdk.outputs.hashes-linux }}
70+
HASHES_WINDOWS: ${{ steps.release-sdk.outputs.hashes-windows }}
71+
HASHES_MACOS: ${{ steps.release-sdk.outputs.hashes-macos }}
72+
run: |
73+
# BSD base64 (macOS) uses -D to decode; GNU base64 (Linux/Windows) uses -d.
74+
if [[ "$OSTYPE" == darwin* ]]; then B64_DECODE="base64 -D"; else B64_DECODE="base64 -d"; fi
75+
if [ -n "${HASHES_LINUX}" ]; then
76+
echo "${HASHES_LINUX}" | $B64_DECODE > checksums.txt
77+
elif [ -n "${HASHES_WINDOWS}" ]; then
78+
echo "${HASHES_WINDOWS}" | $B64_DECODE > checksums.txt
79+
elif [ -n "${HASHES_MACOS}" ]; then
80+
echo "${HASHES_MACOS}" | $B64_DECODE > checksums.txt
81+
fi
82+
shell: bash
83+
# https://github.com/actions/attest/releases/tag/v4.1.0
84+
- name: Attest build provenance
85+
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
86+
with:
87+
subject-checksums: checksums.txt
6388
release-sdk-mac-arm64:
6489
needs: split-input
6590
runs-on: macos-15
66-
outputs:
67-
hashes-macos-arm64: ${{ steps.release-sdk.outputs.hashes-macos }}
91+
permissions:
92+
contents: write
93+
attestations: write
94+
id-token: write
6895
steps:
6996
# https://github.com/actions/checkout/releases/tag/v4.3.0
7097
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
@@ -79,33 +106,31 @@ jobs:
79106
sdk_path: ${{ needs.split-input.outputs.sdk_path}}
80107
sdk_cmake_target: ${{ needs.split-input.outputs.sdk_cmake_target}}
81108
mac_artifact_arch: 'arm64'
109+
- name: Generate checksums file
110+
env:
111+
HASHES: ${{ steps.release-sdk.outputs.hashes-macos }}
112+
run: |
113+
# This job always runs on macOS, so use -D (BSD base64 decode).
114+
echo "${HASHES}" | base64 -D > checksums.txt
115+
shell: bash
116+
# https://github.com/actions/attest/releases/tag/v4.1.0
117+
- name: Attest build provenance
118+
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
119+
with:
120+
subject-checksums: checksums.txt
82121

83-
release-sdk-provenance:
84-
needs: [ 'release-sdk' ]
85-
strategy:
86-
matrix:
87-
# Generates a combined attestation for each platform
88-
os: [ linux, windows, macos ]
89-
permissions:
90-
actions: read
91-
id-token: write
92-
contents: write
93-
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
94-
with:
95-
base64-subjects: "${{ needs.release-sdk.outputs[format('hashes-{0}', matrix.os)] }}"
96-
upload-assets: true
97-
upload-tag-name: ${{ inputs.tag }}
98-
provenance-name: ${{ format('{0}-multiple-provenance.intoto.jsonl', matrix.os) }}
99-
100-
release-sdk-mac-arm64-provenance:
101-
needs: [ 'release-sdk-mac-arm64' ]
122+
publish-release:
123+
needs: ['release-sdk', 'release-sdk-mac-arm64']
124+
if: ${{ format('{0}', inputs.publish_release) == 'true' }}
125+
runs-on: ubuntu-latest
102126
permissions:
103-
actions: read
104-
id-token: write
105127
contents: write
106-
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
107-
with:
108-
base64-subjects: "${{ needs.release-sdk-mac-arm64.outputs.hashes-macos-arm64 }}"
109-
upload-assets: true
110-
upload-tag-name: ${{ inputs.tag }}
111-
provenance-name: 'macos-arm64-multiple-provenance.intoto.jsonl'
128+
steps:
129+
- name: Publish release
130+
env:
131+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
132+
TAG_NAME: ${{ inputs.tag }}
133+
run: >
134+
gh release edit "$TAG_NAME"
135+
--repo ${{ github.repository }}
136+
--draft=false

0 commit comments

Comments
 (0)