diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ad664071 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,115 @@ +name: release + +on: + push: + tags: + - "v*" + +# Minimal permissions required by this workflow +permissions: + contents: write # create releases and upload assets + id-token: write # needed for keyless provenance/attestations + attestations: write # record build provenance in the repo's Attestations + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + environment: release + if: startsWith(github.ref, 'refs/tags/') + + runs-on: ubuntu-latest + steps: + - name: Check actor access + # Defence in depth: also require multiple approvals from: + # Settings -> Environments -> release -> Deployment protection rules -> Required reviewers + # + if: ${{ !contains( fromJson('["frenchi"]'), github.actor ) }} + # !contains( fromJson('["findleyr", "jba",...]'), github.actor ) }} + run: exit 1 + + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # needed to access and verify tags + + # TODO(frenchi): Verify tag is signed (GPG/SSH) + # Possible simpler alternative: repo rulesets + + # - name: Verify tag is signed (GPG/SSH) + # env: + # GH_TOKEN: ${{ github.token }} + # TAG: ${{ github.ref_name }} + # REPO: ${{ github.repository }} + # run: | + # set -euo pipefail + # obj_type="$(gh api repos/${REPO}/git/ref/tags/${TAG} --jq '.object.type')" + # obj_sha="$(gh api repos/${REPO}/git/ref/tags/${TAG} --jq '.object.sha')" + # echo "Tag ${TAG} type=${obj_type} sha=${obj_sha}" + # if [ "${obj_type}" = "tag" ]; then + # verified="$(gh api repos/${REPO}/git/tags/${obj_sha} --jq '.verification.verified')" + # reason="$(gh api repos/${REPO}/git/tags/${obj_sha} --jq '.verification.reason')" + # else + # verified="$(gh api repos/${REPO}/git/commits/${obj_sha} --jq '.verification.verified')" + # reason="$(gh api repos/${REPO}/git/commits/${obj_sha} --jq '.verification.reason')" + # fi + # echo "verification.verified=${verified} reason=${reason}" + # if [ "${verified}" != "true" ]; then + # echo "Tag ${TAG} is not signed/verified. Aborting release." >&2 + # exit 1 + # fi + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "^1.23" + + - name: Download Go modules (for SBOM resolution) + run: go mod download + + # Optional: sigstore/cosign. required for keyless signing at the cost of a (relatively common) dependency on cosign. + # - name: Install Cosign + # uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 #v3.10.0 + # Optional: anchore/syft. generate SBOMs, at the cost of a (relatively common) dependency on anchore/sbom-action + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + path: . + format: github-json + output-file: sbom.json + upload-artifact: true + # Zero dep alternative for SBOMs: + # + # Without anchore/sbom-action, we could use actions/go-dependency-submission@v2 to submit + # to the dependency graph and then curl the github API to download the SBOM: + # + # curl -L \ + # -H "Accept: application/vnd.github+json" \ + # -H "Authorization: Bearer " \ + # -H "X-GitHub-Api-Version: 2022-11-28" \ + # https://api.github.com/repos/OWNER/REPO/dependency-graph/sboms/SHA + # + # from: https://docs.github.com/en/rest/dependency-graph/sboms?apiVersion=2022-11-28#export-a-software-bill-of-materials-sbom-for-a-repository + + - name: Generate SLSA build provenance + uses: actions/attest-build-provenance@v1 + with: + subject-path: sbom.json + - name: Create GitHub Release and upload SBOMs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + TAG="${{ github.ref_name }}" + TITLE="Release ${TAG}" + gh release create "${TAG}" \ + --title "${TITLE}" \ + --generate-notes \ + sbom.json + + - name: Post-release summary + run: | + echo "Release ${{ github.ref_name }} created with SBOMs and provenance attestations." + echo "Environment approvals (if configured) gated this job."