diff --git a/.github/actions/publish/action.yml b/.github/actions/publish/action.yml index 1a2b8654..b68689e1 100644 --- a/.github/actions/publish/action.yml +++ b/.github/actions/publish/action.yml @@ -24,9 +24,12 @@ inputs: required: true outputs: - hashes: - description: sha256sum hashes of built artifacts - value: ${{ steps.hash.outputs.hashes }} + checksum_file: + description: path to the sha256 checksums file generated by goreleaser + value: ${{ steps.binary.outputs.checksum_file }} + images_and_digests: + description: built docker image names and digests in JSON format + value: ${{ steps.image.outputs.images_and_digests }} runs: using: composite @@ -76,8 +79,27 @@ runs: with: name: ldcli path: dist/* - - name: Hash build artifacts for provenance - id: hash + - name: Generate binary checksum file path + id: binary shell: bash run: | - echo "hashes=$(sha256sum dist/*.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT" + # Extract path to checksums file generated by goreleaser from dist/artifacts.json + set -euo pipefail + + checksum_file=$(jq -r '.[] | select (.type=="Checksum") | .path' dist/artifacts.json) + echo "checksum_file=$checksum_file" >> "$GITHUB_OUTPUT" + - name: Output image and digest + id: image + shell: bash + run: | + # Extract image names and digests from goreleaser's dist/artifacts.json + set -euo pipefail + + echo "images_and_digests=$(jq -c '. | map(select (.type=="Docker Manifest") | .image=(.path | split(":")[0]) | .digest=(.extra | .Digest) | {image, digest})' dist/artifacts.json)" >> "$GITHUB_OUTPUT" + - name: Upload Release Artifacts + if: ${{ inputs.dry-run != 'true' }} + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.token }} + run: | + gh release upload "${{ inputs.tag }}" ./dist/*.tar.gz ./dist/*.zip ./dist/*.txt --clobber diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index 1ed396a5..0d860c74 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -2,7 +2,7 @@ name: Manually Publish Images and Artifacts on: workflow_dispatch: inputs: - dry-run: + dry_run: default: true description: 'Skip publishing to DockerHub and Homebrew' required: false @@ -13,20 +13,26 @@ on: required: false type: boolean tag: - description: 'Tag to upload binary artifacts to' + description: 'Tag of an existing draft release to upload binary artifacts to.' required: true type: string + publish_release: + description: 'Publish (un-draft) the release after all artifacts are uploaded?' + type: boolean + required: false + default: true jobs: release-ldcli: permissions: - id-token: write # Needed to obtain Docker tokens + id-token: write # Needed to obtain Docker tokens and to sign attestations contents: write # Needed to upload release artifacts packages: read # Needed to load goreleaser-cross image + attestations: write # Needed for artifact attestations runs-on: ubuntu-latest outputs: - hashes: ${{ steps.publish.outputs.hashes }} + images_and_digests: ${{ steps.publish.outputs.images_and_digests }} steps: - uses: actions/checkout@v4 name: Checkout @@ -44,16 +50,39 @@ jobs: - uses: ./.github/actions/publish id: publish with: - dry-run: ${{ inputs.dry-run }} + dry-run: ${{ inputs.dry_run }} token: ${{ secrets.GITHUB_TOKEN }} - homebrew-gh-secret: ${{secrets.HOMEBREW_DEPLOY_KEY}} + homebrew-gh-secret: ${{ secrets.HOMEBREW_DEPLOY_KEY }} tag: ${{ inputs.tag }} ghcr_token: "${{ secrets.GITHUB_TOKEN }}" + - name: Attest binary artifacts + if: ${{ !inputs.dry_run }} + uses: actions/attest@v4 + with: + subject-checksums: ${{ steps.publish.outputs.checksum_file }} + + attest-image-provenance: + needs: [release-ldcli] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + strategy: + matrix: + images_and_digests: ${{ fromJson(needs.release-ldcli.outputs.images_and_digests) }} + steps: + - name: Attest container image + uses: actions/attest@v4 + with: + subject-name: ${{ matrix.images_and_digests.image }} + subject-digest: ${{ matrix.images_and_digests.digest }} + release-ldcli-npm: runs-on: ubuntu-latest if: ${{ inputs.dry-run-npm == false }} - needs: ['release-ldcli'] + needs: [release-ldcli] permissions: actions: read id-token: write @@ -76,18 +105,23 @@ jobs: name: Publish NPM Package uses: ./.github/actions/publish-npm with: - dry-run: ${{ inputs.dry-run }} - prerelease: ${{ inputs.prerelease }} + dry-run: ${{ inputs.dry_run }} + prerelease: 'false' - release-ldcli-provenance: - needs: ['release-ldcli'] + publish-release: + needs: [release-ldcli, attest-image-provenance, release-ldcli-npm] + # !cancelled() && !failure() lets this job run when release-ldcli-npm was + # skipped (dry-run-npm: true) but still blocks if any needed job failed. + if: ${{ !cancelled() && !failure() && !inputs.dry_run && inputs.publish_release }} + 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-ldcli.outputs.hashes }}" - upload-assets: true - upload-tag-name: ${{ inputs.tag }} - provenance-name: ${{ format('ldcli_{0}_multiple_provenance.intoto.jsonl', inputs.tag) }} + 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 c9d8c55d..963c3704 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -1,7 +1,6 @@ name: Run Release Please on: - pull_request: push: branches: - main @@ -9,7 +8,6 @@ on: jobs: release-please: runs-on: ubuntu-latest - if: github.event_name == 'push' permissions: contents: write pull-requests: write @@ -17,21 +15,51 @@ jobs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} steps: - - uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1 + # Create any releases in release, then create tags, and then optionally create any new PRs. + - uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 id: release with: - token: ${{secrets.GITHUB_TOKEN}} + token: ${{ secrets.GITHUB_TOKEN }} + 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: + token: ${{ secrets.GITHUB_TOKEN }} + skip-github-release: true release-ldcli: permissions: - id-token: write # Needed to obtain Docker tokens + id-token: write # Needed to obtain Docker tokens and to sign attestations contents: write # Needed to upload release artifacts packages: read # Needed to load goreleaser-cross image - needs: [ release-please ] - if: needs.release-please.outputs.release_created == 'true' || github.event_name == 'pull_request' + attestations: write # Needed for artifact attestations + needs: [release-please] + if: needs.release-please.outputs.release_created == 'true' runs-on: ubuntu-22.04-8core-32gb outputs: - hashes: ${{ steps.publish.outputs.hashes }} + images_and_digests: ${{ steps.publish.outputs.images_and_digests }} steps: - uses: actions/checkout@v4 name: Checkout @@ -49,16 +77,36 @@ jobs: - uses: ./.github/actions/publish id: publish with: - dry-run: ${{ github.event_name == 'pull_request' }} - snapshot: ${{ github.event_name == 'pull_request' }} + dry-run: 'false' token: ${{ secrets.GITHUB_TOKEN }} - homebrew-gh-secret: ${{secrets.HOMEBREW_DEPLOY_KEY}} + homebrew-gh-secret: ${{ secrets.HOMEBREW_DEPLOY_KEY }} tag: ${{ needs.release-please.outputs.tag_name }} ghcr_token: "${{ secrets.GITHUB_TOKEN }}" + - name: Attest binary artifacts + uses: actions/attest@v4 + with: + subject-checksums: ${{ steps.publish.outputs.checksum_file }} + + attest-image-provenance: + needs: [release-ldcli] + runs-on: ubuntu-latest + permissions: + id-token: write + attestations: write + strategy: + matrix: + images_and_digests: ${{ fromJson(needs.release-ldcli.outputs.images_and_digests) }} + steps: + - name: Attest container image + uses: actions/attest@v4 + with: + subject-name: ${{ matrix.images_and_digests.image }} + subject-digest: ${{ matrix.images_and_digests.digest }} + release-ldcli-npm: runs-on: ubuntu-latest - needs: ['release-please', 'release-ldcli'] + needs: [release-please, release-ldcli] permissions: id-token: write contents: write @@ -78,18 +126,21 @@ jobs: name: Publish NPM Package uses: ./.github/actions/publish-npm with: - dry-run: ${{ github.event_name == 'pull_request' }} + dry-run: 'false' prerelease: 'false' - release-ldcli-provenance: - needs: ['release-please', 'release-ldcli'] + publish-release: + needs: [release-please, release-ldcli, attest-image-provenance, release-ldcli-npm] + if: needs.release-please.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-ldcli.outputs.hashes }}" - upload-assets: true - upload-tag-name: ${{ needs.release-please.outputs.tag_name }} - provenance-name: ${{ format('ldcli_{0}_multiple_provenance.intoto.jsonl', needs.release-please.outputs.tag_name) }} + 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/.goreleaser.yaml b/.goreleaser.yaml index 71e8f085..cda90a02 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,6 +2,13 @@ version: 2 project_name: ldcli +release: + # release-please owns the GitHub release lifecycle (creates the draft, and + # publish-release un-drafts at the end). Goreleaser only builds binaries and + # pushes images; the workflow's `gh release upload` step attaches artifacts + # to the draft. See .github/workflows/release-please.yml for the full flow. + disable: true + env: - GO111MODULE=on # Ensure we aren't using anything in GOPATH when building - CGO_ENABLED=1 # Needed for SQLite support diff --git a/PROVENANCE.md b/PROVENANCE.md index b93993ab..1a92eb50 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -1,56 +1,56 @@ -## Verifying build provenance with the SLSA framework +## Verifying 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 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 packages. -As part of [SLSA requirements for level 3 compliance](https://slsa.dev/spec/v1.0/requirements), LaunchDarkly publishes provenance about our 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. +LaunchDarkly publishes provenance about our 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/). - -These attestations are available for download from the GitHub release page for the release version under Assets > `ldcli_3.0.0_multiple_provenance.intoto.jsonl`. - - -To verify SLSA provenance attestations, we recommend using [slsa-verifier](https://github.com/slsa-framework/slsa-verifier). Example usage for verifying packages for Linux 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 packages for Linux is included below: ``` -# Set the version of the PACKAGE to verify +# Set the version of the package to verify PACKAGE_VERSION=3.0.0 ``` ``` -# Ensure provenance file is downloaded along with packages for your OS -# Run slsa-verifier to verify provenance against package artifacts -$ slsa-verifier verify-artifact \ ---provenance-path ldcli_${PACKAGE_VERSION}_multiple_provenance.intoto.jsonl \ ---source-uri github.com/launchdarkly/ldcli \ -ldcli_${PACKAGE_VERSION}_*.tar.gz +# Download the release archive from GitHub +$ curl --location -O \ + https://github.com/launchdarkly/ldcli/releases/download/${PACKAGE_VERSION}/ldcli_${PACKAGE_VERSION}_linux_amd64.tar.gz + +# Verify provenance using the GitHub CLI +$ gh attestation verify ldcli_${PACKAGE_VERSION}_linux_amd64.tar.gz --owner launchdarkly ``` -Below is a sample of expected output: +You can also verify the provenance of the published container images: + +``` +$ gh attestation verify oci://launchdarkly/ldcli:${PACKAGE_VERSION} --owner launchdarkly ``` -Verified signature against tlog entry index 84971628 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a9053fbc27f7e695f7bcf705e69e3596a48e4759b9f9429725d4fec327c9d09bf -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 50b064100a9a142a6da6539e520deef1df6a4ddf -Verifying artifact ldcli_0.6.0_darwin_amd64.tar.gz: PASSED -Verified signature against tlog entry index 84971628 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a9053fbc27f7e695f7bcf705e69e3596a48e4759b9f9429725d4fec327c9d09bf -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 50b064100a9a142a6da6539e520deef1df6a4ddf -Verifying artifact ldcli_0.6.0_darwin_arm64.tar.gz: PASSED +Below is a sample of expected output. + +``` +Loaded digest sha256:... for file://ldcli_3.0.0_linux_amd64.tar.gz +Loaded 1 attestation from GitHub API -Verified signature against tlog entry index 84971628 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a9053fbc27f7e695f7bcf705e69e3596a48e4759b9f9429725d4fec327c9d09bf -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 50b064100a9a142a6da6539e520deef1df6a4ddf -Verifying artifact ldcli_0.6.0_linux_386.tar.gz: PASSED +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 -Verified signature against tlog entry index 84971628 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a9053fbc27f7e695f7bcf705e69e3596a48e4759b9f9429725d4fec327c9d09bf -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 50b064100a9a142a6da6539e520deef1df6a4ddf -Verifying artifact ldcli_0.6.0_linux_amd64.tar.gz: PASSED +✓ Verification succeeded! -Verified signature against tlog entry index 84971628 at URL: https://rekor.sigstore.dev/api/v1/log/entries/24296fb24b8ad77a9053fbc27f7e695f7bcf705e69e3596a48e4759b9f9429725d4fec327c9d09bf -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 50b064100a9a142a6da6539e520deef1df6a4ddf -Verifying artifact ldcli_0.6.0_linux_arm64.tar.gz: PASSED +The following 1 attestation matched the policy criteria -PASSED: Verified SLSA provenance +- Attestation #1 + - Build repo:..... launchdarkly/ldcli + - Build workflow:. .github/workflows/release-please.yml + - Signer repo:.... launchdarkly/ldcli + - 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 CLI from source. +**Note:** These instructions do not apply when building our CLI from source. diff --git a/release-please-config.json b/release-please-config.json index 5eaf0714..c1b3726f 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -3,6 +3,7 @@ ".": { "release-type": "go", "versioning": "default", + "draft": true, "extra-files": [ "PROVENANCE.md", {