From 8fa07c5dec3cc8ae111bd0bb2b1f2086da368e53 Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 21 Oct 2026 20:43:41 +0530 Subject: [PATCH 1/5] ci: replace SLSA generator with GitHub artifact attestations --- .github/workflows/main.yaml | 127 +++++++++++------------------------- 1 file changed, 37 insertions(+), 90 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 49bd31f1..5e7f5c76 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -148,9 +148,10 @@ jobs: contents: write packages: write # publish a new github release id-token: write # needed for keyless signing + attestations: write # needed for artifact attestations + artifact-metadata: write # needed for the image storage record outputs: digest: ${{ steps.image.outputs.digest }} - subjects-as-file: ${{ steps.hashes.outputs.handle }} steps: - name: Extract Version from Tag id: tag_name @@ -206,20 +207,6 @@ jobs: repository: openfga/cli short-description: ${{ github.event.repository.description }} - - name: Generate binary hashes - id: binary - env: - ARTIFACTS: "${{ steps.goreleaser.outputs.artifacts }}" - run: | - set -euo pipefail - hashes=$(echo $ARTIFACTS | jq --raw-output '[.[] | {name, "digest": (.extra.Digest // .extra.Checksum)}] | unique | .[] | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0) - echo $hashes > digests.txt - - - uses: slsa-framework/slsa-github-generator/actions/generator/generic/create-base64-subjects-from-file@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # pin@v2.1.0 - id: hashes - with: - path: digests.txt - - name: Image digest id: image env: @@ -239,98 +226,64 @@ jobs: digest=$(echo "$artifact" | jq -r '.extra.Digest') echo "digest=$digest" >> "$GITHUB_OUTPUT" - binary-provenance: - needs: [ goreleaser ] - permissions: - actions: read # To read the workflow path. - id-token: write # To sign the provenance. - # write required even with upload-assets:false due to slsa-framework/slsa-github-generator#2044 - contents: write - - # Note: this _must_ be referenced by tag. See: https://github.com/slsa-framework/slsa-verifier/issues/12 - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 - with: - base64-subjects-as-file: "${{ needs.goreleaser.outputs.subjects-as-file }}" - provenance-name: "fga.intoto.jsonl" - # Don't let the generator attach the provenance: - # - it uses softprops/action-gh-release@v2.2.1, whose paginated draft lookup - # creates a duplicate draft once a repo has >100 releases (2 pages) - # - the provenance is still published as a workflow artifact, so the - # release-provenance job attaches it via `gh release upload` (by tag) - upload-assets: false - - release-provenance: - needs: [ binary-provenance ] - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - contents: write # To add the provenance asset to the draft release. - steps: - - name: Download the provenance - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + - name: Attest release artifacts + id: attest + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - name: ${{ needs.binary-provenance.outputs.provenance-name }} + subject-checksums: dist/checksums.txt + # Scorecard's Signed-Releases check only detects provenance as a + # *.intoto.jsonl release asset. The bundle is a single JSON line. - name: Attach provenance to the draft release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PROVENANCE: ${{ needs.binary-provenance.outputs.provenance-name }} + BUNDLE: ${{ steps.attest.outputs.bundle-path }} run: | set -euo pipefail - gh release upload "$GITHUB_REF_NAME" "$PROVENANCE" \ + cp "$BUNDLE" fga.intoto.jsonl + gh release upload "$GITHUB_REF_NAME" fga.intoto.jsonl \ --repo "$GITHUB_REPOSITORY" --clobber - image-provenance: - needs: [ goreleaser ] - permissions: - actions: read - id-token: write - packages: write - - # Note: this _must_ be referenced by tag. See: https://github.com/slsa-framework/slsa-verifier/issues/12 - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 - with: - image: openfga/cli - digest: ${{ needs.goreleaser.outputs.digest }} - secrets: - registry-username: ${{ secrets.DOCKERHUB_USERNAME }} - registry-password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Attest image + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-name: docker.io/openfga/cli + subject-digest: ${{ steps.image.outputs.digest }} + push-to-registry: true undraft-release: - needs: [ release-provenance, image-provenance ] + needs: [ goreleaser ] permissions: contents: write uses: openfga/.github/.github/workflows/undraft-release.yml@835baf31562809ad9eb884c73efc5b79318f700f # pin@main - verification-with-slsa-verifier: - needs: [ undraft-release, binary-provenance ] + verify-binary-provenance: + needs: [ undraft-release ] runs-on: ubuntu-latest permissions: read-all steps: - - name: Install the verifier - uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6 # pin@v2.7.1 - - name: Download assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}" run: | set -euo pipefail gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" \ - -p "$PROVENANCE" -p "*.tar.gz" -p "*.tar.gz.sbom.json" + -p "*.tar.gz" -p "*.tar.gz.sbom.json" - name: Verify assets env: - PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}" + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - slsa-verifier verify-artifact \ - --provenance-path "$PROVENANCE" \ - --source-uri "github.com/$GITHUB_REPOSITORY" \ - --source-tag "$GITHUB_REF_NAME" \ - *.tar.gz *.tar.gz.sbom.json - - verification-with-cosign: - needs: [ goreleaser, image-provenance ] + set -euo pipefail + for artifact in *.tar.gz *.tar.gz.sbom.json; do + gh attestation verify "$artifact" \ + --repo "$GITHUB_REPOSITORY" \ + --cert-identity "https://github.com/$GITHUB_REPOSITORY/.github/workflows/main.yaml@$GITHUB_REF" \ + --source-ref "$GITHUB_REF" + done + + verify-image-provenance: + needs: [ goreleaser ] runs-on: ubuntu-latest permissions: read-all steps: @@ -340,18 +293,12 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Install Cosign - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - with: - cosign-release: "v2.6.1" - - name: Verify image env: - IMAGE: openfga/cli + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DIGEST: ${{ needs.goreleaser.outputs.digest }} run: | - cosign verify-attestation \ - --type slsaprovenance \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - --certificate-identity-regexp '^https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v[0-9]+.[0-9]+.[0-9]+$' \ - $IMAGE@$DIGEST + gh attestation verify "oci://docker.io/openfga/cli@$DIGEST" \ + --repo "$GITHUB_REPOSITORY" \ + --cert-identity "https://github.com/$GITHUB_REPOSITORY/.github/workflows/main.yaml@$GITHUB_REF" \ + --source-ref "$GITHUB_REF" From 0fb500040d028f7292486f9721abd86a235181a5 Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 21 Oct 2026 21:08:01 +0530 Subject: [PATCH 2/5] ci: move attestations to a separate job and gate undraft on verification --- .github/workflows/main.yaml | 72 ++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5e7f5c76..d0e5fb84 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -148,8 +148,6 @@ jobs: contents: write packages: write # publish a new github release id-token: write # needed for keyless signing - attestations: write # needed for artifact attestations - artifact-metadata: write # needed for the image storage record outputs: digest: ${{ steps.image.outputs.digest }} steps: @@ -226,11 +224,33 @@ jobs: digest=$(echo "$artifact" | jq -r '.extra.Digest') echo "digest=$digest" >> "$GITHUB_OUTPUT" + attest: + needs: [ goreleaser ] + runs-on: ubuntu-latest + permissions: + contents: write # to read the draft release and attach the provenance + id-token: write # needed for keyless signing + attestations: write # needed for artifact attestations + artifact-metadata: write # needed for the image storage record + steps: + - name: Login to Docker Hub + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Download checksums + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release download "$GITHUB_REF_NAME" -p checksums.txt \ + --repo "$GITHUB_REPOSITORY" + - name: Attest release artifacts id: attest uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: - subject-checksums: dist/checksums.txt + subject-checksums: checksums.txt # Scorecard's Signed-Releases check only detects provenance as a # *.intoto.jsonl release asset. The bundle is a single JSON line. @@ -248,19 +268,15 @@ jobs: uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 with: subject-name: docker.io/openfga/cli - subject-digest: ${{ steps.image.outputs.digest }} + subject-digest: ${{ needs.goreleaser.outputs.digest }} push-to-registry: true - undraft-release: - needs: [ goreleaser ] - permissions: - contents: write - uses: openfga/.github/.github/workflows/undraft-release.yml@835baf31562809ad9eb884c73efc5b79318f700f # pin@main - verify-binary-provenance: - needs: [ undraft-release ] + needs: [ attest ] runs-on: ubuntu-latest - permissions: read-all + permissions: + contents: write # to download from the draft release + attestations: read steps: - name: Download assets env: @@ -268,22 +284,27 @@ jobs: run: | set -euo pipefail gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" \ - -p "*.tar.gz" -p "*.tar.gz.sbom.json" + -p "*.tar.gz" -p "*.sbom.json" -p "*.apk" -p "*.deb" -p "*.rpm" -p "*.pkg.tar.zst" \ + -p fga.intoto.jsonl - name: Verify assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - for artifact in *.tar.gz *.tar.gz.sbom.json; do - gh attestation verify "$artifact" \ + verify() { + gh attestation verify "$@" \ --repo "$GITHUB_REPOSITORY" \ --cert-identity "https://github.com/$GITHUB_REPOSITORY/.github/workflows/main.yaml@$GITHUB_REF" \ --source-ref "$GITHUB_REF" + } + for artifact in *.tar.gz *.sbom.json *.apk *.deb *.rpm *.pkg.tar.zst; do + verify "$artifact" + verify "$artifact" --bundle fga.intoto.jsonl done verify-image-provenance: - needs: [ goreleaser ] + needs: [ goreleaser, attest ] runs-on: ubuntu-latest permissions: read-all steps: @@ -298,7 +319,18 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DIGEST: ${{ needs.goreleaser.outputs.digest }} run: | - gh attestation verify "oci://docker.io/openfga/cli@$DIGEST" \ - --repo "$GITHUB_REPOSITORY" \ - --cert-identity "https://github.com/$GITHUB_REPOSITORY/.github/workflows/main.yaml@$GITHUB_REF" \ - --source-ref "$GITHUB_REF" + set -euo pipefail + verify() { + gh attestation verify "oci://docker.io/openfga/cli@$DIGEST" "$@" \ + --repo "$GITHUB_REPOSITORY" \ + --cert-identity "https://github.com/$GITHUB_REPOSITORY/.github/workflows/main.yaml@$GITHUB_REF" \ + --source-ref "$GITHUB_REF" + } + verify + verify --bundle-from-oci + + undraft-release: + needs: [ verify-binary-provenance, verify-image-provenance ] + permissions: + contents: write + uses: openfga/.github/.github/workflows/undraft-release.yml@835baf31562809ad9eb884c73efc5b79318f700f # pin@main From d08d42c588a27809fa7ee4ae5f8a1b6fc99c7070 Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 21 Oct 2026 21:31:26 +0530 Subject: [PATCH 3/5] ci: drop persisted credentials and Go cache in the release job --- .github/workflows/main.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index d0e5fb84..fa4e71d7 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -161,13 +161,14 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: './go.mod' - cache-dependency-path: './go.sum' check-latest: true + cache: false # avoid cache poisoning in the release build - name: Set up QEMU uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0 From 445ca6ce7f0f3517b15f588a9f1c84c305848cf4 Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 21 Oct 2026 21:59:15 +0530 Subject: [PATCH 4/5] ci: don't move stable Docker tags or Homebrew formulas on prereleases --- .goreleaser.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 97da89e8..0f5a9097 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -28,10 +28,10 @@ dockers_v2: - images: - openfga/cli tags: - - latest + - "{{ if not .Prerelease }}latest{{ end }}" - "{{ .Tag }}" - - "v{{ .Major }}" - - "v{{ .Major }}.{{ .Minor }}" + - "{{ if not .Prerelease }}v{{ .Major }}{{ end }}" + - "{{ if not .Prerelease }}v{{ .Major }}.{{ .Minor }}{{ end }}" dockerfile: .goreleaser.Dockerfile platforms: - linux/amd64 @@ -82,6 +82,7 @@ docker_signs: brews: - name: "{{.ProjectName}}" + skip_upload: auto repository: owner: openfga name: homebrew-tap @@ -108,6 +109,7 @@ brews: test: | system "#{bin}/fga version" - name: "{{.ProjectName}}@{{.Version}}" + skip_upload: auto repository: owner: openfga name: homebrew-tap From 791a766ff1318c106a18b922e6e92e8518edc3bc Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 21 Oct 2026 22:36:23 +0530 Subject: [PATCH 5/5] ci: check the draft checksums.txt against the build output --- .github/workflows/main.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index fa4e71d7..2282fbb0 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -150,6 +150,7 @@ jobs: id-token: write # needed for keyless signing outputs: digest: ${{ steps.image.outputs.digest }} + checksums-digest: ${{ steps.checksums.outputs.digest }} steps: - name: Extract Version from Tag id: tag_name @@ -198,6 +199,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} + - name: Checksums digest + id: checksums + run: | + set -euo pipefail + echo "digest=$(sha256sum dist/checksums.txt | cut -d ' ' -f1)" >> "$GITHUB_OUTPUT" + - name: Update Docker Hub Description uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0 with: @@ -240,12 +247,16 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + # The draft is mutable, so check it against the file GoReleaser built. - name: Download checksums env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CHECKSUMS_DIGEST: ${{ needs.goreleaser.outputs.checksums-digest }} run: | + set -euo pipefail gh release download "$GITHUB_REF_NAME" -p checksums.txt \ --repo "$GITHUB_REPOSITORY" + echo "$CHECKSUMS_DIGEST checksums.txt" | sha256sum -c --strict - name: Attest release artifacts id: attest