From c2c6b0e5a042860177beafe3b2f507814d9454eb Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 21:14:16 +0000 Subject: [PATCH 01/11] ci: use draft releases to support immutable GitHub releases --- .github/workflows/publish.yml | 26 ++++++++-------- .github/workflows/release-please.yml | 44 +++++++++++++++++++++++++++- release-please-config.json | 3 +- 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9b164d9..5fa2251 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -30,6 +30,7 @@ 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 steps: - uses: actions/checkout@v4 @@ -71,16 +72,15 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} dry_run: ${{ inputs.dry_run }} - provenance: - permissions: - actions: read - id-token: write - contents: write - if: ${{ inputs.dry_run == 'false' }} - needs: ['publish'] - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 - with: - base64-subjects: "${{ needs.publish.outputs.hashes }}" - upload-assets: true - upload-tag-name: ${{ inputs.tag }} - provenance-name: ${{ format('LaunchDarkly.Logging-{0}_provenance.intoto.jsonl', inputs.tag) }} + - name: Generate checksums file + if: ${{ inputs.dry_run == 'false' }} + env: + HASHES: ${{ steps.publish.outputs.hashes }} + run: | + echo "$HASHES" | base64 -d > checksums.txt + + - name: Attest build provenance + if: ${{ inputs.dry_run == 'false' }} + uses: actions/attest@v4 + with: + subject-checksums: checksums.txt diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index da32471..62fc727 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -23,14 +23,56 @@ jobs: token: ${{secrets.GITHUB_TOKEN}} default-branch: main + create-tag: + needs: ['release-please'] + if: ${{ needs.release-please.outputs.releases_created == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Create release tag + env: + TAG_NAME: ${{ needs.release-please.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 + ci: needs: ['release-please'] if: ${{ needs.release-please.outputs.releases_created == 'true' }} uses: ./.github/workflows/ci.yml publish: - needs: ['release-please', 'ci'] + needs: ['release-please', 'ci', 'create-tag'] if: ${{ needs.release-please.outputs.releases_created == 'true' }} uses: ./.github/workflows/publish.yml with: dry_run: false tag: ${{ needs.release-please.outputs.tag_name }} + + publish-release: + needs: ['release-please', 'publish'] + if: ${{ needs.release-please.outputs.releases_created == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Publish release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG_NAME: ${{ needs.release-please.outputs.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 e89297f..a02552a 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,7 +4,8 @@ "release-type": "simple", "bootstrap-sha": "b4600d1c993afecd737dad1c85da34908c7d7e50", "include-v-in-tag": false, - "include-component-in-tag": false + "include-component-in-tag": false, + "draft": true } } } From 15c6de353acd3895739f439eb828e420176c8fe7 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 21:41:57 +0000 Subject: [PATCH 02/11] ci: add force-tag-creation and publish_release option --- .github/workflows/publish.yml | 28 +++++++++++++++++++++++++++- release-please-config.json | 3 ++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5fa2251..ddb0758 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,9 +8,14 @@ on: type: boolean required: true tag: - description: 'Tag for provenance. For a dry run the value does not matter.' + description: 'Tag of an existing draft release. For a dry run the value does not matter.' type: string required: true + publish_release: + description: 'Publish (un-draft) the release after all artifacts are uploaded?' + type: boolean + required: false + default: true workflow_call: inputs: @@ -22,6 +27,11 @@ on: description: 'Tag for provenance' type: string required: true + publish_release: + description: 'Publish (un-draft) the release after all artifacts are uploaded?' + type: boolean + required: false + default: false jobs: publish: @@ -84,3 +94,19 @@ jobs: uses: actions/attest@v4 with: subject-checksums: checksums.txt + + publish-release: + needs: ['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 a02552a..678daf8 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,7 +5,8 @@ "bootstrap-sha": "b4600d1c993afecd737dad1c85da34908c7d7e50", "include-v-in-tag": false, "include-component-in-tag": false, - "draft": true + "draft": true, + "force-tag-creation": true } } } From a873dccf6cba84d8b19eb5f1e625645c7d98d6bd Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:06:51 +0000 Subject: [PATCH 03/11] 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/publish.yml | 29 +----------------- .github/workflows/release-please.yml | 44 +--------------------------- release-please-config.json | 1 - 3 files changed, 2 insertions(+), 72 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ddb0758..32d1f4b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,14 +8,9 @@ on: type: boolean required: true tag: - description: 'Tag of an existing draft release. For a dry run the value does not matter.' + description: 'Tag for provenance. For a dry run the value does not matter.' type: string required: true - publish_release: - description: 'Publish (un-draft) the release after all artifacts are uploaded?' - type: boolean - required: false - default: true workflow_call: inputs: @@ -27,11 +22,6 @@ on: description: 'Tag for provenance' type: string required: true - publish_release: - description: 'Publish (un-draft) the release after all artifacts are uploaded?' - type: boolean - required: false - default: false jobs: publish: @@ -57,7 +47,6 @@ jobs: /production/common/releasing/nuget/api_key = NUGET_API_KEY' s3_path_pairs: 'launchdarkly-releaser/dotnet/LaunchDarkly.Logging.snk = LaunchDarkly.Logging.snk' - - name: Build Release uses: ./.github/actions/release-build @@ -94,19 +83,3 @@ jobs: uses: actions/attest@v4 with: subject-checksums: checksums.txt - - publish-release: - needs: ['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 62fc727..da32471 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -23,56 +23,14 @@ jobs: token: ${{secrets.GITHUB_TOKEN}} default-branch: main - create-tag: - needs: ['release-please'] - if: ${{ needs.release-please.outputs.releases_created == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Create release tag - env: - TAG_NAME: ${{ needs.release-please.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 - ci: needs: ['release-please'] if: ${{ needs.release-please.outputs.releases_created == 'true' }} uses: ./.github/workflows/ci.yml publish: - needs: ['release-please', 'ci', 'create-tag'] + needs: ['release-please', 'ci'] if: ${{ needs.release-please.outputs.releases_created == 'true' }} uses: ./.github/workflows/publish.yml with: dry_run: false tag: ${{ needs.release-please.outputs.tag_name }} - - publish-release: - needs: ['release-please', 'publish'] - if: ${{ needs.release-please.outputs.releases_created == 'true' }} - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Publish release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAG_NAME: ${{ needs.release-please.outputs.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 678daf8..b6fedc1 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,7 +5,6 @@ "bootstrap-sha": "b4600d1c993afecd737dad1c85da34908c7d7e50", "include-v-in-tag": false, "include-component-in-tag": false, - "draft": true, "force-tag-creation": true } } From 0701773851b07be0c40c544e21e4325fbe007aa5 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:33:35 +0000 Subject: [PATCH 04/11] 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 | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index b6fedc1..e89297f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,8 +4,7 @@ "release-type": "simple", "bootstrap-sha": "b4600d1c993afecd737dad1c85da34908c7d7e50", "include-v-in-tag": false, - "include-component-in-tag": false, - "force-tag-creation": true + "include-component-in-tag": false } } } From 0df2aa2a681a9c5ddac632f01bb6fcfd2d2edbb5 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 23:12:36 +0000 Subject: [PATCH 05/11] ci: switch from subject-checksums to subject-path for attestation --- .github/actions/publish/action.yml | 12 ------------ .github/workflows/publish.yml | 9 +-------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/.github/actions/publish/action.yml b/.github/actions/publish/action.yml index 0780cf2..6ae0445 100644 --- a/.github/actions/publish/action.yml +++ b/.github/actions/publish/action.yml @@ -4,11 +4,6 @@ inputs: dry_run: description: 'Is this a dry run. If so no package will be published.' required: true -outputs: - hashes: - description: sha256sum hashes of built artifacts - value: ${{ steps.hash.outputs.hashes }} - runs: using: composite steps: @@ -33,13 +28,6 @@ runs: echo "published ${pkg}" done - - name: Hash nuget packages - id: hash - if: ${{ inputs.dry_run == 'false' }} - shell: bash - run: | - echo "hashes=$(sha256sum ./nupkgs/*.nupkg ./nupkgs/*.snupkg | base64 -w0)" >> "$GITHUB_OUTPUT" - - name: Dry Run Publish if: ${{ inputs.dry_run == 'true' }} shell: bash diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 32d1f4b..53be012 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -71,15 +71,8 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} dry_run: ${{ inputs.dry_run }} - - name: Generate checksums file - if: ${{ inputs.dry_run == 'false' }} - env: - HASHES: ${{ steps.publish.outputs.hashes }} - run: | - echo "$HASHES" | base64 -d > checksums.txt - - name: Attest build provenance if: ${{ inputs.dry_run == 'false' }} uses: actions/attest@v4 with: - subject-checksums: checksums.txt + subject-path: 'nupkgs/*' From b22672b7b6f0fb87e2a09762f9f5fceadb05d20b Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 18:34:11 +0000 Subject: [PATCH 06/11] ci: remove unused tag input --- .github/workflows/publish.yml | 8 -------- .github/workflows/release-please.yml | 1 - 2 files changed, 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 53be012..1a386ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,10 +7,6 @@ on: description: 'Is this a dry run. If so no package will be published.' type: boolean required: true - tag: - description: 'Tag for provenance. For a dry run the value does not matter.' - type: string - required: true workflow_call: inputs: @@ -18,10 +14,6 @@ on: description: 'Is this a dry run. If so no package will be published.' type: boolean required: true - tag: - description: 'Tag for provenance' - type: string - required: true jobs: publish: diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index da32471..94eec03 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -33,4 +33,3 @@ jobs: uses: ./.github/workflows/publish.yml with: dry_run: false - tag: ${{ needs.release-please.outputs.tag_name }} From 133baa7b4b4f975057f1aa4f606b2ee00d128867 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 19:07:49 +0000 Subject: [PATCH 07/11] ci: use format() for dry_run conditions to handle both string and boolean inputs --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1a386ef..c8cc9ef 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,7 +64,7 @@ jobs: dry_run: ${{ inputs.dry_run }} - name: Attest build provenance - if: ${{ inputs.dry_run == 'false' }} + if: ${{ format('{0}', inputs.dry_run) == 'false' }} uses: actions/attest@v4 with: subject-path: 'nupkgs/*' From 64fb4986760e3ad9c5bfe158d55af5e90db01a23 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 20:36:38 +0000 Subject: [PATCH 08/11] ci: remove dead tag_name outputs and inputs --- .github/workflows/release-please.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 94eec03..e24992b 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -9,7 +9,6 @@ jobs: release-please: outputs: releases_created: ${{ steps.release.outputs.releases_created }} - tag_name: ${{ steps.release.outputs.tag_name }} runs-on: ubuntu-latest permissions: id-token: write # Needed if using OIDC to get release secrets. From b41e479b1ce5bd03c31a054eed4f4b16f21c1370 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 22:07:11 +0000 Subject: [PATCH 09/11] docs: add PROVENANCE.md for GitHub artifact attestations --- PROVENANCE.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 PROVENANCE.md diff --git a/PROVENANCE.md b/PROVENANCE.md new file mode 100644 index 0000000..ad6d235 --- /dev/null +++ b/PROVENANCE.md @@ -0,0 +1,49 @@ +## Verifying SDK build provenance with GitHub artifact attestations + +LaunchDarkly uses [GitHub artifact attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. + +LaunchDarkly publishes provenance about our SDK package builds using [GitHub's `actions/attest` action](https://github.com/actions/attest). These attestations are stored in GitHub's attestation API and can be verified using the [GitHub CLI](https://cli.github.com/). + +To verify build provenance attestations, we recommend using the [GitHub CLI `attestation verify` command](https://cli.github.com/manual/gh_attestation_verify). Example usage for verifying SDK packages is included below: + + +``` +# Set the version of the SDK to verify +SDK_VERSION=2.0.0 +``` + + +``` +# Download the nupkg from NuGet +$ nuget install LaunchDarkly.Logging -Version $SDK_VERSION -OutputDirectory ./packages + +# Verify provenance using the GitHub CLI +$ gh attestation verify ./packages/LaunchDarkly.Logging.${SDK_VERSION}/LaunchDarkly.Logging.${SDK_VERSION}.nupkg --owner launchdarkly +``` + +Below is a sample of expected output. + +``` +Loaded digest sha256:... for file://LaunchDarkly.Logging.2.0.0.nupkg +Loaded 1 attestation from GitHub API + +The following policy criteria will be enforced: +- Predicate type must match:................ https://slsa.dev/provenance/v1 +- Source Repository Owner URI must match:... https://github.com/launchdarkly +- Subject Alternative Name must match regex: (?i)^https://github.com/launchdarkly/ +- OIDC Issuer must match:................... https://token.actions.githubusercontent.com + +✓ Verification succeeded! + +The following 1 attestation matched the policy criteria + +- Attestation #1 + - Build repo:..... launchdarkly/dotnet-logging + - Build workflow:. .github/workflows/release-please.yml + - Signer repo:.... launchdarkly/dotnet-logging + - Signer workflow: .github/workflows/release-please.yml +``` + +For more information, see [GitHub's documentation on verifying artifact attestations](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds#verifying-artifact-attestations-with-the-github-cli). + +**Note:** These instructions do not apply when building our SDKs from source. From a4d82351151b75dec71518374bc46fb9a3e372ba Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 22:12:29 +0000 Subject: [PATCH 10/11] docs: add SLSA provenance section to README.md --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 668c91c..510c634 100644 --- a/README.md +++ b/README.md @@ -37,13 +37,17 @@ a8a17f69f1bef56e253fc9166096c907514ab74b812d041faa04712e2bcb243d Public Key Token: d9182e4b0afd33e7 ``` +## Verifying build provenance with the SLSA framework + +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published packages. To learn more, see the [provenance guide](PROVENANCE.md). + ## About LaunchDarkly * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. + * Grant access to certain features based on user attributes, like payment plan (eg: users on the 'gold' plan get access to more features than users in the 'silver' plan). Disable parts of your application to facilitate maintenance, without taking everything offline. * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list. * Explore LaunchDarkly * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information From 604be692da7656679956d47ef4acc9aed38f62f8 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 22:54:36 +0000 Subject: [PATCH 11/11] ci: register PROVENANCE.md in release-please extra-files --- release-please-config.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/release-please-config.json b/release-please-config.json index e89297f..2b29245 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,7 +4,10 @@ "release-type": "simple", "bootstrap-sha": "b4600d1c993afecd737dad1c85da34908c7d7e50", "include-v-in-tag": false, - "include-component-in-tag": false + "include-component-in-tag": false, + "extra-files": [ + "PROVENANCE.md" + ] } } }