From d439f974d11e4986a4a24b1576b43b613546b81c Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 21:16:31 +0000 Subject: [PATCH 1/7] ci: use draft releases to support immutable GitHub releases --- .github/workflows/manual-publish.yml | 23 ++++++------ .github/workflows/release-please.yml | 52 ++++++++++++++++++++++------ release-please-config.json | 1 + 3 files changed, 54 insertions(+), 22 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 8be05d3..af5647c 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -14,8 +14,7 @@ jobs: permissions: id-token: write contents: read - outputs: - gem-hash: ${{ steps.publish.outputs.gem-hash}} + attestations: write steps: - uses: actions/checkout@v4 @@ -37,13 +36,13 @@ jobs: with: dry_run: ${{ inputs.dry_run }} - release-provenance: - needs: [ 'build-publish' ] - permissions: - actions: read - id-token: write - contents: write - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 - with: - base64-subjects: "${{ needs.build-publish.outputs.gem-hash }}" - upload-assets: ${{ !inputs.dry_run }} + - name: Generate checksums file + env: + HASHES: ${{ steps.publish.outputs.gem-hash }} + run: | + echo "$HASHES" | base64 -d > checksums.txt + + - name: Attest build provenance + uses: actions/attest@v4 + with: + subject-checksums: checksums.txt diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index f865d71..664c0f4 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -12,10 +12,10 @@ jobs: id-token: write # Needed if using OIDC to get release secrets. contents: write # Contents and pull-requests are for release-please to make releases. pull-requests: write + attestations: write outputs: release-created: ${{ steps.release.outputs.release_created }} upload-tag-name: ${{ steps.release.outputs.tag_name }} - gem-hash: ${{ steps.publish.outputs.gem-hash}} steps: - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 id: release @@ -25,6 +25,22 @@ jobs: with: fetch-depth: 0 # Full history is required for proper changelog generation + - name: Create release tag + if: ${{ steps.release.outputs.releases_created == 'true' }} + env: + TAG_NAME: ${{ steps.release.outputs.tag_name }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh api "repos/${{ github.repository }}/git/ref/tags/${TAG_NAME}" >/dev/null 2>&1; then + echo "Tag ${TAG_NAME} already exists, skipping creation." + else + echo "Creating tag ${TAG_NAME}." + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git tag "${TAG_NAME}" + git push origin "${TAG_NAME}" + fi + - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 if: ${{ steps.release.outputs.releases_created == 'true' }} name: 'Get rubygems API key' @@ -51,15 +67,31 @@ jobs: with: token: ${{secrets.GITHUB_TOKEN}} - release-provenance: - needs: [ 'release-package' ] + - name: Generate checksums file + if: ${{ steps.release.outputs.releases_created == 'true' }} + env: + HASHES: ${{ steps.publish.outputs.gem-hash }} + run: | + echo "$HASHES" | base64 -d > checksums.txt + + - name: Attest build provenance + if: ${{ steps.release.outputs.releases_created == 'true' }} + uses: actions/attest@v4 + with: + subject-checksums: checksums.txt + + publish-release: + needs: ['release-package'] if: ${{ needs.release-package.outputs.release-created == 'true' }} + runs-on: ubuntu-latest permissions: - actions: read - id-token: write contents: write - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 - with: - base64-subjects: "${{ needs.release-package.outputs.gem-hash }}" - upload-assets: true - upload-tag-name: ${{ needs.release-package.outputs.upload-tag-name }} + steps: + - name: Publish release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ needs.release-package.outputs.upload-tag-name }} + run: > + gh release edit "$TAG_NAME" + --repo ${{ github.repository }} + --draft=false diff --git a/release-please-config.json b/release-please-config.json index 5010ac9..4c8a3f4 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,6 +1,7 @@ { "packages": { ".": { + "draft": true, "release-type": "ruby", "bump-minor-pre-major": true, "versioning": "default", From 466907601bea769d15c27ecef281f826e5e793be Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 21:42:27 +0000 Subject: [PATCH 2/7] ci: add force-tag-creation and publish_release option --- .github/workflows/manual-publish.yml | 25 +++++++++++++++++++++++++ release-please-config.json | 6 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index af5647c..12f017b 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -6,6 +6,15 @@ on: description: 'Is this a dry run. If so no package will be published.' type: boolean required: true + tag: + description: 'Tag of an existing draft release to upload artifacts to.' + type: string + required: false + publish_release: + description: 'Publish (un-draft) the release after all artifacts are uploaded?' + type: boolean + required: false + default: true jobs: build-publish: @@ -46,3 +55,19 @@ jobs: uses: actions/attest@v4 with: subject-checksums: checksums.txt + + publish-release: + needs: ['build-publish'] + if: ${{ !inputs.dry_run && inputs.publish_release }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Publish release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ inputs.tag }} + run: > + gh release edit "$TAG_NAME" + --repo ${{ github.repository }} + --draft=false diff --git a/release-please-config.json b/release-please-config.json index 4c8a3f4..49c267f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -2,12 +2,16 @@ "packages": { ".": { "draft": true, + "force-tag-creation": true, "release-type": "ruby", "bump-minor-pre-major": true, "versioning": "default", "include-component-in-tag": false, "include-v-in-tag": false, - "extra-files": ["PROVENANCE.md", "lib/ldclient-openfeature/version.rb"] + "extra-files": [ + "PROVENANCE.md", + "lib/ldclient-openfeature/version.rb" + ] } } } From a9aa4c0ac6bc2441ee8eb9937227769bba538888 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:06:51 +0000 Subject: [PATCH 3/7] ci: simplify for attestation-only releases (no draft needed) Since actions/attest@v4 stores attestations via GitHub's attestation API (not as release assets), repos that only use attestation don't need draft releases. Release-please can publish the release directly. Changes: - Remove draft:true from release-please-config.json - Remove create-tag job/steps (force-tag-creation handles this) - Remove publish-release job (release is published directly) - Remove publish_release input from manual workflows --- .github/workflows/manual-publish.yml | 21 ------------------ .github/workflows/release-please.yml | 32 ---------------------------- release-please-config.json | 1 - 3 files changed, 54 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 12f017b..5f184d2 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -10,11 +10,6 @@ on: description: 'Tag of an existing draft release to upload artifacts to.' type: string required: false - publish_release: - description: 'Publish (un-draft) the release after all artifacts are uploaded?' - type: boolean - required: false - default: true jobs: build-publish: @@ -55,19 +50,3 @@ jobs: uses: actions/attest@v4 with: subject-checksums: checksums.txt - - publish-release: - needs: ['build-publish'] - if: ${{ !inputs.dry_run && inputs.publish_release }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Publish release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ inputs.tag }} - run: > - gh release edit "$TAG_NAME" - --repo ${{ github.repository }} - --draft=false diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 664c0f4..ee8e8c0 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -25,22 +25,6 @@ jobs: with: fetch-depth: 0 # Full history is required for proper changelog generation - - name: Create release tag - if: ${{ steps.release.outputs.releases_created == 'true' }} - env: - TAG_NAME: ${{ steps.release.outputs.tag_name }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - if gh api "repos/${{ github.repository }}/git/ref/tags/${TAG_NAME}" >/dev/null 2>&1; then - echo "Tag ${TAG_NAME} already exists, skipping creation." - else - echo "Creating tag ${TAG_NAME}." - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag "${TAG_NAME}" - git push origin "${TAG_NAME}" - fi - - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 if: ${{ steps.release.outputs.releases_created == 'true' }} name: 'Get rubygems API key' @@ -79,19 +63,3 @@ jobs: uses: actions/attest@v4 with: subject-checksums: checksums.txt - - publish-release: - needs: ['release-package'] - if: ${{ needs.release-package.outputs.release-created == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Publish release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ needs.release-package.outputs.upload-tag-name }} - run: > - gh release edit "$TAG_NAME" - --repo ${{ github.repository }} - --draft=false diff --git a/release-please-config.json b/release-please-config.json index 49c267f..e037494 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,7 +1,6 @@ { "packages": { ".": { - "draft": true, "force-tag-creation": true, "release-type": "ruby", "bump-minor-pre-major": true, From a6a49f5757660410f2f1706185f835bafc301586 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:33:46 +0000 Subject: [PATCH 4/7] ci: remove force-tag-creation from attestation-only repo force-tag-creation only operates in conjunction with draft releases. Since this repo does not use draft releases (attestation-only, no artifact uploads to the release), force-tag-creation is not needed. --- release-please-config.json | 1 - 1 file changed, 1 deletion(-) diff --git a/release-please-config.json b/release-please-config.json index e037494..780e615 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,7 +1,6 @@ { "packages": { ".": { - "force-tag-creation": true, "release-type": "ruby", "bump-minor-pre-major": true, "versioning": "default", From 246a4382e922e5b5dd46b641c9eec40ad461087e Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:54:54 +0000 Subject: [PATCH 5/7] ci: add dry_run guard to attestation steps in manual-publish workflow Ensures that checksums generation and attestation are skipped during dry runs, preventing unnecessary attestation of unpublished artifacts. --- .github/workflows/manual-publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 5f184d2..d5bd145 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -41,12 +41,14 @@ jobs: dry_run: ${{ inputs.dry_run }} - name: Generate checksums file + if: ${{ !inputs.dry_run }} env: HASHES: ${{ steps.publish.outputs.gem-hash }} run: | echo "$HASHES" | base64 -d > checksums.txt - name: Attest build provenance + if: ${{ !inputs.dry_run }} uses: actions/attest@v4 with: subject-checksums: checksums.txt From af85ad261d2f9d67599e6b6753d94a62963bb80a Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 23:12:32 +0000 Subject: [PATCH 6/7] ci: switch from subject-checksums to subject-path for attestation --- .github/actions/publish/action.yml | 10 ---------- .github/workflows/manual-publish.yml | 9 +-------- .github/workflows/release-please.yml | 9 +-------- 3 files changed, 2 insertions(+), 26 deletions(-) diff --git a/.github/actions/publish/action.yml b/.github/actions/publish/action.yml index e9c3f66..7d70537 100644 --- a/.github/actions/publish/action.yml +++ b/.github/actions/publish/action.yml @@ -4,10 +4,6 @@ inputs: dry_run: description: 'Is this a dry run. If so no package will be published.' required: true -outputs: - gem-hash: - description: "base64-encoded sha256 hashes of distribution files" - value: ${{ steps.gem-hash.outputs.gem-hash }} runs: using: composite @@ -16,12 +12,6 @@ runs: shell: bash run: gem build launchdarkly-openfeature-server-sdk.gemspec - - name: Hash gem for provenance - id: gem-hash - shell: bash - run: | - echo "gem-hash=$(sha256sum launchdarkly-openfeature-server-sdk-*.gem | base64 -w0)" >> "$GITHUB_OUTPUT" - - name: Publish Library shell: bash if: ${{ inputs.dry_run == 'false' }} diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index d5bd145..1299c3a 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -40,15 +40,8 @@ jobs: with: dry_run: ${{ inputs.dry_run }} - - name: Generate checksums file - if: ${{ !inputs.dry_run }} - env: - HASHES: ${{ steps.publish.outputs.gem-hash }} - run: | - echo "$HASHES" | base64 -d > checksums.txt - - name: Attest build provenance if: ${{ !inputs.dry_run }} uses: actions/attest@v4 with: - subject-checksums: checksums.txt + subject-path: 'launchdarkly-openfeature-server-sdk-*.gem' diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index ee8e8c0..eea84eb 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -51,15 +51,8 @@ jobs: with: token: ${{secrets.GITHUB_TOKEN}} - - name: Generate checksums file - if: ${{ steps.release.outputs.releases_created == 'true' }} - env: - HASHES: ${{ steps.publish.outputs.gem-hash }} - run: | - echo "$HASHES" | base64 -d > checksums.txt - - name: Attest build provenance if: ${{ steps.release.outputs.releases_created == 'true' }} uses: actions/attest@v4 with: - subject-checksums: checksums.txt + subject-path: 'launchdarkly-openfeature-server-sdk-*.gem' From 3da68de6bf01c05dc7dc222bf9ceb7780feb4f75 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 16:34:10 +0000 Subject: [PATCH 7/7] ci: remove unused tag input and orphaned job outputs --- .github/workflows/manual-publish.yml | 4 ---- .github/workflows/release-please.yml | 3 --- 2 files changed, 7 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 1299c3a..0ffa125 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -6,10 +6,6 @@ on: description: 'Is this a dry run. If so no package will be published.' type: boolean required: true - tag: - description: 'Tag of an existing draft release to upload artifacts to.' - type: string - required: false jobs: build-publish: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index eea84eb..79e45e6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -13,9 +13,6 @@ jobs: contents: write # Contents and pull-requests are for release-please to make releases. pull-requests: write attestations: write - outputs: - release-created: ${{ steps.release.outputs.release_created }} - upload-tag-name: ${{ steps.release.outputs.tag_name }} steps: - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 id: release