From 7db70a43ffaeb1551bc8945592e9b5b8e2ece493 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 21:14:28 +0000 Subject: [PATCH 01/15] ci: use draft releases to support immutable GitHub releases --- .github/workflows/manual-publish.yml | 24 ++++++------- .github/workflows/release-please.yml | 52 ++++++++++++++++++++++------ release-please-config.json | 3 +- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 9b35bb2..857c07f 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: - package-hashes: ${{ steps.build.outputs.package-hashes}} + attestations: write steps: - uses: actions/checkout@v4 @@ -41,13 +40,14 @@ jobs: with: password: ${{env.PYPI_AUTH_TOKEN}} - 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.package-hashes }}" - upload-assets: ${{ !inputs.dry_run }} + - name: Generate checksums file + env: + HASHES: ${{ steps.build.outputs.package-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 a7fc2f1..9bcb9b1 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -11,10 +11,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 }} - package-hashes: ${{ steps.build.outputs.package-hashes}} steps: - uses: googleapis/release-please-action@v4 id: release @@ -24,6 +24,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: actions/setup-python@v5 if: ${{ steps.release.outputs.releases_created == 'true' }} with: @@ -53,15 +69,31 @@ jobs: with: password: ${{env.PYPI_AUTH_TOKEN}} - release-provenance: - needs: [ 'release-package' ] + - name: Generate checksums file + if: ${{ steps.release.outputs.releases_created == 'true' }} + env: + HASHES: ${{ steps.build.outputs.package-hashes }} + 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.package-hashes }}" - 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 c458d9f..038ca1b 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -5,7 +5,8 @@ "versioning": "default", "include-v-in-tag": false, "extra-files": ["ldotel/__init__.py", "PROVENANCE.md"], - "include-component-in-tag": false + "include-component-in-tag": false, + "draft": true } } } From 28febc2fcafba92f4acc11b8fc62d5c6c51db24c Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 21:42:14 +0000 Subject: [PATCH 02/15] ci: add force-tag-creation and publish_release option --- .github/workflows/manual-publish.yml | 25 +++++++++++++++++++++++++ release-please-config.json | 8 ++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 857c07f..15f87d5 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: @@ -51,3 +60,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 038ca1b..60a1423 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,9 +4,13 @@ "release-type": "python", "versioning": "default", "include-v-in-tag": false, - "extra-files": ["ldotel/__init__.py", "PROVENANCE.md"], + "extra-files": [ + "ldotel/__init__.py", + "PROVENANCE.md" + ], "include-component-in-tag": false, - "draft": true + "draft": true, + "force-tag-creation": true } } } From 72e292d90a60e370ef0436d56dd7b6802639c3e6 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:06:51 +0000 Subject: [PATCH 03/15] 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 15f87d5..70f9cc0 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: @@ -60,19 +55,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 9bcb9b1..dac2b3e 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -24,22 +24,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: actions/setup-python@v5 if: ${{ steps.release.outputs.releases_created == 'true' }} with: @@ -81,19 +65,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 60a1423..4b54bef 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -9,7 +9,6 @@ "PROVENANCE.md" ], "include-component-in-tag": false, - "draft": true, "force-tag-creation": true } } From ceeccd292859e55fa7eeec88ee0d80d08a21c0b5 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:33:41 +0000 Subject: [PATCH 04/15] 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 4b54bef..e694848 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,8 +8,7 @@ "ldotel/__init__.py", "PROVENANCE.md" ], - "include-component-in-tag": false, - "force-tag-creation": true + "include-component-in-tag": false } } } From 4cb48d6e6ccb8c6a5a9354032b36ad297169e29d Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 22:55:05 +0000 Subject: [PATCH 05/15] ci: add dry_run guard to checksums generation in manual-publish workflow The attest step was already guarded, but the checksums file generation was not. Now both steps are skipped during dry runs. --- .github/workflows/manual-publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 70f9cc0..86c6981 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -45,6 +45,7 @@ jobs: password: ${{env.PYPI_AUTH_TOKEN}} - name: Generate checksums file + if: ${{ inputs.dry_run == false }} env: HASHES: ${{ steps.build.outputs.package-hashes }} run: | From d80fdf1fff47550f5dcc87ccec07f6546f0c6c7d Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Tue, 31 Mar 2026 23:12:13 +0000 Subject: [PATCH 06/15] ci: switch from subject-checksums to subject-path for attestation --- .github/actions/build/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/build/action.yml b/.github/actions/build/action.yml index 6761d0d..5cf127c 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -1,9 +1,5 @@ name: Build distribution files description: 'Build distribution files' -outputs: - package-hashes: - description: "base64-encoded sha256 hashes of distribution files" - value: ${{ steps.package-hashes.outputs.package-hashes }} runs: using: composite @@ -11,9 +7,3 @@ runs: - name: Build distribution files shell: bash run: poetry build - - name: Hash build files for provenance - id: package-hashes - shell: bash - working-directory: ./dist - run: | - echo "package-hashes=$(sha256sum * | base64 -w0)" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 86c6981..f37aec0 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -44,15 +44,8 @@ jobs: with: password: ${{env.PYPI_AUTH_TOKEN}} - - name: Generate checksums file - if: ${{ inputs.dry_run == false }} - env: - HASHES: ${{ steps.build.outputs.package-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: 'dist/*' diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index dac2b3e..864f6b6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -53,15 +53,8 @@ jobs: with: password: ${{env.PYPI_AUTH_TOKEN}} - - name: Generate checksums file - if: ${{ steps.release.outputs.releases_created == 'true' }} - env: - HASHES: ${{ steps.build.outputs.package-hashes }} - 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: 'dist/*' From 8972d68d2d8fb18c48488933f25e99ec4300b69f Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 16:34:24 +0000 Subject: [PATCH 07/15] 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 f37aec0..d20e7ff 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 864f6b6..ec59681 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -12,9 +12,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@v4 id: release From b6b56f3a27690f7958c084a298353d8a63d2a5f9 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 19:08:03 +0000 Subject: [PATCH 08/15] ci: use format() for dry_run conditions to handle both string and boolean inputs --- .github/workflows/manual-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index d20e7ff..eecbf6b 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -35,13 +35,13 @@ jobs: id: build - name: Publish package distributions to PyPI - if: ${{ inputs.dry_run == false }} + if: ${{ format('{0}', inputs.dry_run) == 'false' }} uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{env.PYPI_AUTH_TOKEN}} - name: Attest build provenance - if: ${{ inputs.dry_run == false }} + if: ${{ format('{0}', inputs.dry_run) == 'false' }} uses: actions/attest@v4 with: subject-path: 'dist/*' From 8af62bd9a895219a7cbbb54534c0fcead94b9906 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 21:48:59 +0000 Subject: [PATCH 09/15] docs: update PROVENANCE.md and README.md for GitHub artifact attestations --- PROVENANCE.md | 47 ++++++++++++++++++++++++++--------------------- README.md | 4 ++-- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/PROVENANCE.md b/PROVENANCE.md index 06a8c55..064f8f4 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -1,10 +1,10 @@ -## Verifying SDK build provenance with the SLSA framework +## Verifying SDK build provenance with GitHub artifact attestations -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 SDK packages. +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. -As part of [SLSA requirements for level 3 compliance](https://slsa.dev/spec/v1.0/requirements), LaunchDarkly publishes provenance about our SDK package builds using [GitHub's generic SLSA3 provenance generator](https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md#generation-of-slsa3-provenance-for-arbitrary-projects) for distribution alongside our packages. These attestations are available for download from the GitHub release page for the release version under Assets > `multiple.intoto.jsonl`. +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 SLSA provenance attestations, we recommend using [slsa-verifier](https://github.com/slsa-framework/slsa-verifier). Example usage for verifying a package is included below: +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: ``` @@ -13,32 +13,37 @@ VERSION=1.2.0 ``` - ``` -# Download package from PyPi +# Download package from PyPI $ pip download --only-binary=:all: launchdarkly-server-sdk-otel==${VERSION} -# Download provenance from Github release into same directory -$ curl --location -O \ - https://github.com/launchdarkly/python-server-sdk-otel/releases/download/${VERSION}/multiple.intoto.jsonl - -# Run slsa-verifier to verify provenance against package artifacts -$ slsa-verifier verify-artifact \ ---provenance-path multiple.intoto.jsonl \ ---source-uri github.com/launchdarkly/python-server-sdk-otel \ -launchdarkly_server_sdk_otel-${VERSION}-py3-none-any.whl +# Verify provenance using the GitHub CLI +$ gh attestation verify launchdarkly_server_sdk_otel-${VERSION}-py3-none-any.whl --owner launchdarkly ``` Below is a sample of expected output. ``` -Verified signature against tlog entry index 89939519 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77abb8d2f681b007c76a4fe9f89cd9574918683ac8bc87cd6834c5baa479ae5cb98 -Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v1.10.0" at commit 984fc268df29918b03f51f2507146f66d8668d03 -Verifying artifact launchdarkly_server_sdk_otel-1.0.0-py3-none-any.whl: PASSED +Loaded digest sha256:... for file://launchdarkly_server_sdk_otel-1.2.0-py3-none-any.whl +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 -PASSED: Verified SLSA provenance +- Attestation #1 + - Build repo:..... launchdarkly/python-server-sdk-otel + - Build workflow:. .github/workflows/release-please.yml + - Signer repo:.... launchdarkly/python-server-sdk-otel + - Signer workflow: .github/workflows/release-please.yml ``` -Alternatively, to verify the provenance manually, the SLSA framework specifies [recommendations for verifying build artifacts](https://slsa.dev/spec/v1.0/verifying-artifacts) in their documentation. +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 libraries from source. +**Note:** These instructions do not apply when building our libraries from source. diff --git a/README.md b/README.md index 785a63f..cbb77bc 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,10 @@ Contributing We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this library. -Verifying library build provenance with the SLSA framework +Verifying library build provenance with GitHub artifact attestations ------------ -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 library packages. To learn more, see the [provenance guide](PROVENANCE.md). +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 library packages. To learn more, see the [provenance guide](PROVENANCE.md). About LaunchDarkly ----------- From 9f48de6f03681f9dde033f5b6f3ee57a2850f0b6 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Wed, 1 Apr 2026 22:12:05 +0000 Subject: [PATCH 10/15] docs: restore original SLSA framework text in README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbb77bc..785a63f 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,10 @@ Contributing We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this library. -Verifying library build provenance with GitHub artifact attestations +Verifying library build provenance with the SLSA framework ------------ -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 library packages. To learn more, see the [provenance guide](PROVENANCE.md). +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 library packages. To learn more, see the [provenance guide](PROVENANCE.md). About LaunchDarkly ----------- From a1239203dd5a3d9fd3016979f94fe7eed8668359 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Mon, 6 Apr 2026 16:34:45 +0000 Subject: [PATCH 11/15] ci: apply split release-please pattern for immutable releases --- .github/workflows/release-please.yml | 54 +++++++++++++++++++++------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index ec59681..5f9d7c6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -5,33 +5,67 @@ on: branches: [ main ] jobs: + release-please: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + outputs: + releases_created: ${{ steps.release.outputs.releases_created }} + steps: + # Create any releases first, then create tags, and then optionally create any new PRs. + - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 + id: release + with: + skip-github-pull-request: true + + # Need the repository content to be able to create and push a tag. + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.release_created == 'true' }} + + - name: Create release tag + if: ${{ steps.release.outputs.release_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: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 + if: ${{ steps.release.outputs.release_created != 'true' }} + id: release-prs + with: + skip-github-release: true + release-package: + needs: release-please + if: ${{ needs.release-please.outputs.releases_created == 'true' }} runs-on: ubuntu-latest permissions: 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: googleapis/release-please-action@v4 - id: release - - uses: actions/checkout@v4 - if: ${{ steps.release.outputs.releases_created == 'true' }} with: fetch-depth: 0 # Full history is required for proper changelog generation - uses: actions/setup-python@v5 - if: ${{ steps.release.outputs.releases_created == 'true' }} with: python-version: 3.9 - name: Install poetry - if: ${{ steps.release.outputs.releases_created == 'true' }} uses: abatilo/actions-poetry@7b6d33e44b4f08d7021a1dee3c044e9c253d6439 - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 - if: ${{ steps.release.outputs.releases_created == 'true' }} name: 'Get PyPI token' with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} @@ -39,19 +73,15 @@ jobs: - uses: ./.github/actions/build id: build - if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/build-docs - if: ${{ steps.release.outputs.releases_created == 'true' }} - name: Publish package distributions to PyPI - if: ${{ steps.release.outputs.releases_created == 'true' }} uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{env.PYPI_AUTH_TOKEN}} - name: Attest build provenance - if: ${{ steps.release.outputs.releases_created == 'true' }} uses: actions/attest@v4 with: subject-path: 'dist/*' From b064f64e94d98a8e8e8c7ff3dcd24653845289c9 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Mon, 6 Apr 2026 17:14:56 +0000 Subject: [PATCH 12/15] ci: use release_created consistently in single-package repo --- .github/workflows/release-please.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 5f9d7c6..fb39429 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -11,7 +11,7 @@ jobs: contents: write pull-requests: write outputs: - releases_created: ${{ steps.release.outputs.releases_created }} + release_created: ${{ steps.release.outputs.release_created }} steps: # Create any releases first, then create tags, and then optionally create any new PRs. - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 @@ -47,7 +47,7 @@ jobs: release-package: needs: release-please - if: ${{ needs.release-please.outputs.releases_created == 'true' }} + if: ${{ needs.release-please.outputs.release_created == 'true' }} runs-on: ubuntu-latest permissions: id-token: write # Needed if using OIDC to get release secrets. From 35bd72a75d1c3510161bc1a090306df18cce7acf Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Mon, 6 Apr 2026 17:30:32 +0000 Subject: [PATCH 13/15] Revert "ci: use release_created consistently in single-package repo" This reverts commit b064f64e94d98a8e8e8c7ff3dcd24653845289c9. --- .github/workflows/release-please.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index fb39429..5f9d7c6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -11,7 +11,7 @@ jobs: contents: write pull-requests: write outputs: - release_created: ${{ steps.release.outputs.release_created }} + releases_created: ${{ steps.release.outputs.releases_created }} steps: # Create any releases first, then create tags, and then optionally create any new PRs. - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 @@ -47,7 +47,7 @@ jobs: release-package: needs: release-please - if: ${{ needs.release-please.outputs.release_created == 'true' }} + if: ${{ needs.release-please.outputs.releases_created == 'true' }} runs-on: ubuntu-latest permissions: id-token: write # Needed if using OIDC to get release secrets. From 9bfece1590fc42ddbd629cc20fdb4ddbfba61759 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Mon, 6 Apr 2026 17:30:32 +0000 Subject: [PATCH 14/15] Revert "ci: apply split release-please pattern for immutable releases" This reverts commit a1239203dd5a3d9fd3016979f94fe7eed8668359. --- .github/workflows/release-please.yml | 54 +++++++--------------------- 1 file changed, 12 insertions(+), 42 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 5f9d7c6..ec59681 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -5,67 +5,33 @@ on: branches: [ main ] jobs: - release-please: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - outputs: - releases_created: ${{ steps.release.outputs.releases_created }} - steps: - # Create any releases first, then create tags, and then optionally create any new PRs. - - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 - id: release - with: - skip-github-pull-request: true - - # Need the repository content to be able to create and push a tag. - - uses: actions/checkout@v4 - if: ${{ steps.release.outputs.release_created == 'true' }} - - - name: Create release tag - if: ${{ steps.release.outputs.release_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: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 - if: ${{ steps.release.outputs.release_created != 'true' }} - id: release-prs - with: - skip-github-release: true - release-package: - needs: release-please - if: ${{ needs.release-please.outputs.releases_created == 'true' }} runs-on: ubuntu-latest permissions: 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: googleapis/release-please-action@v4 + id: release + - uses: actions/checkout@v4 + if: ${{ steps.release.outputs.releases_created == 'true' }} with: fetch-depth: 0 # Full history is required for proper changelog generation - uses: actions/setup-python@v5 + if: ${{ steps.release.outputs.releases_created == 'true' }} with: python-version: 3.9 - name: Install poetry + if: ${{ steps.release.outputs.releases_created == 'true' }} uses: abatilo/actions-poetry@7b6d33e44b4f08d7021a1dee3c044e9c253d6439 - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0 + if: ${{ steps.release.outputs.releases_created == 'true' }} name: 'Get PyPI token' with: aws_assume_role: ${{ vars.AWS_ROLE_ARN }} @@ -73,15 +39,19 @@ jobs: - uses: ./.github/actions/build id: build + if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/build-docs + if: ${{ steps.release.outputs.releases_created == 'true' }} - name: Publish package distributions to PyPI + if: ${{ steps.release.outputs.releases_created == 'true' }} uses: pypa/gh-action-pypi-publish@release/v1 with: password: ${{env.PYPI_AUTH_TOKEN}} - name: Attest build provenance + if: ${{ steps.release.outputs.releases_created == 'true' }} uses: actions/attest@v4 with: subject-path: 'dist/*' From 1cba2d58fa1835d6877cf60f98bdf33a1966b620 Mon Sep 17 00:00:00 2001 From: "mkeeler@launchdarkly.com" Date: Mon, 6 Apr 2026 19:43:49 +0000 Subject: [PATCH 15/15] ci: remove unused step IDs and fix stale permission comments --- .github/workflows/manual-publish.yml | 1 - .github/workflows/release-please.yml | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index eecbf6b..44d78c1 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -32,7 +32,6 @@ jobs: ssm_parameter_pairs: '/production/common/releasing/pypi/token = PYPI_AUTH_TOKEN' - uses: ./.github/actions/build - id: build - name: Publish package distributions to PyPI if: ${{ format('{0}', inputs.dry_run) == 'false' }} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index ec59681..e3fb8fe 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest permissions: id-token: write # Needed if using OIDC to get release secrets. - contents: write # Contents and pull-requests are for release-please to make releases. + contents: write # Needed for release-please to create releases. pull-requests: write attestations: write steps: @@ -38,7 +38,6 @@ jobs: ssm_parameter_pairs: '/production/common/releasing/pypi/token = PYPI_AUTH_TOKEN' - uses: ./.github/actions/build - id: build if: ${{ steps.release.outputs.releases_created == 'true' }} - uses: ./.github/actions/build-docs