diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb0b860..5fda793 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,13 +73,11 @@ jobs: permission-pull-requests: write - name: Mint release token - # Used as GoReleaser's GITHUB_TOKEN to publish the release to this repo. - # The default GITHUB_TOKEN cannot be used here: GitHub does not start new - # workflow runs from events triggered by the default token, so a release - # published with it would never fire the `release: published` trigger on - # "Release lifecycle sync" (notify-landing-yank.yml) — and the - # landing-page docs sync (repository_dispatch) would silently never run. - # An app token is attributed to the app, so the publish event cascades. + # Used as GoReleaser's GITHUB_TOKEN to create the draft release on this + # repo and upload its assets (archives, packages, checksums, signature). + # The release stays a draft here (see .goreleaser.yaml) — actual + # publishing happens in the `publish` job below, after the SLSA + # provenance file is also attached. uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 id: release-token with: @@ -118,7 +116,45 @@ jobs: # shorter tag — slsa-verifier resolves the builder identity from this exact ref, # and SHA-pinning breaks that resolution. See .github/pinact.yaml, which excludes # this line from automated re-pinning. + # + # Uploads to the still-draft release created above — draft releases are + # exempt from GitHub's immutable-releases restriction, which otherwise + # rejects new assets on an already-published release. uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: base64-subjects: ${{ needs.release.outputs.hashes }} upload-assets: ${{ startsWith(github.ref, 'refs/tags/') }} + + publish: + needs: [release, provenance] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Mint release token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + id: release-token + with: + client-id: ${{ vars.RELEASE_BOT_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} + owner: bomly-dev + repositories: bomly-cli + permission-contents: write + + - name: Publish release + # Finalizes the draft into a published release now that every asset — + # GoReleaser's archives/packages/checksums/signature, plus the SLSA + # provenance file from the `provenance` job — is attached. Must run + # last: immutable releases reject new assets once published. + # + # Uses the release-bot app token, not the default GITHUB_TOKEN: GitHub + # does not start new workflow runs from events triggered by the + # default token, so a release published with it would never fire the + # `release: published` trigger on "Release lifecycle sync" + # (notify-landing-yank.yml) — and the landing-page docs sync would + # silently never run. An app token is attributed to the app, so the + # publish event cascades. + env: + GH_TOKEN: ${{ steps.release-token.outputs.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: gh release edit "$RELEASE_TAG" --draft=false --repo bomly-dev/bomly-cli diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 17f9e48..283abc5 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -107,7 +107,12 @@ release: github: owner: bomly-dev name: bomly-cli - draft: false + # Stays a draft until the `publish` job in release.yml finalizes it. GitHub's + # immutable releases feature forbids adding assets once a release is + # published, and the SLSA provenance file is attached by a later job — so + # publishing has to be the last step, after every asset (including + # provenance) is in place. See dev-docs/CI.md. + draft: true prerelease: auto footer: | --- diff --git a/dev-docs/CI.md b/dev-docs/CI.md index 3ac858e..241ff68 100644 --- a/dev-docs/CI.md +++ b/dev-docs/CI.md @@ -35,7 +35,7 @@ Bomly dogfoods its own domain by tracking the project's [OpenSSF Scorecard](http - **Token-Permissions** — every workflow declares a top-level `permissions:` block scoped to `contents: read`. Any write scope (release publishing, the Guard PR comment, the smoke-goldens PR) is granted at the **job** level only, never at the top level. - **Pinned-Dependencies** — all GitHub Actions are pinned by full commit SHA with a trailing `# vX.Y.Z` comment (for example `actions/checkout@ # v7.0.0`). Dependabot's `github-actions` updater understands this form and bumps both the SHA and the comment, so pinning does not freeze us on stale actions. When adding a new `uses:`, pin it the same way — `pinact run` (suzuki-shunsuke/pinact) rewrites the whole tree, or resolve a single tag with `gh api repos///commits/ --jq .sha`. The `Smoke` and `Update Smoke Goldens` workflows install `pip`/`pipenv`/`poetry` from `.github/requirements-ci-tools.txt`, a hash-locked, fully-resolved requirements file (`pip install --require-hashes`) instead of an unpinned inline `pip install`. Regenerate it after bumping `.github/requirements-ci-tools.in` with `pip-compile --allow-unsafe --generate-hashes --output-file=requirements-ci-tools.txt requirements-ci-tools.in` run from `.github/` under the same Python version the workflows use (3.12), so the resolved hash set covers the right wheel tags. - **SAST** — CodeQL runs on every push, PR, and weekly. -- **Signed-Releases** — the `release` job signs `SHA256SUMS` keylessly with [cosign](https://docs.sigstore.dev/cosign/signing/overview/) (`SHA256SUMS.sigstore.json`, GitHub OIDC identity, no managed keys), and a separate `provenance` job calls the [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) generic builder to produce a single `multiple.intoto.jsonl` SLSA Build Level 3 provenance file over every release artifact's hash, uploaded to the same GitHub release. Verification commands for end users are in [docs/INSTALLATION.md](../docs/INSTALLATION.md#verify-release-checksums). The generator's `uses:` line is pinned to the `v2.1.0` tag, not a commit SHA — SHA-pinning it breaks `slsa-verifier`'s builder-identity check — and `.github/pinact.yaml` excludes that line from automated re-pinning so it doesn't regress. +- **Signed-Releases** — the `release` job signs `SHA256SUMS` keylessly with [cosign](https://docs.sigstore.dev/cosign/signing/overview/) (`SHA256SUMS.sigstore.json`, GitHub OIDC identity, no managed keys), and a separate `provenance` job calls the [slsa-github-generator](https://github.com/slsa-framework/slsa-github-generator) generic builder to produce a single `multiple.intoto.jsonl` SLSA Build Level 3 provenance file over every release artifact's hash, uploaded to the same GitHub release. Verification commands for end users are in [docs/INSTALLATION.md](../docs/INSTALLATION.md#verify-release-checksums). The generator's `uses:` line is pinned to the `v2.1.0` tag, not a commit SHA — SHA-pinning it breaks `slsa-verifier`'s builder-identity check — and `.github/pinact.yaml` excludes that line from automated re-pinning so it doesn't regress. Because the provenance file is attached by a job downstream of `release`, and GitHub's immutable releases feature blocks adding assets after a release is published, `.goreleaser.yaml` keeps the release as a draft and a final `publish` job flips it to published only after `provenance` succeeds — see [Release Process](#release-process). A few Scorecard checks require maintainer action **outside** the repository and are not code changes: @@ -176,8 +176,8 @@ go build -tags "bomly_external_syft,bomly_external_grype" -o bin/bomly-lite ./cm Release packaging is driven by `.goreleaser.yaml`. The release workflow uses GoReleaser to create: -- A published GitHub Release with archives for `bomly` and `bomly-lite`. -- `SHA256SUMS`. +- A GitHub Release with archives for `bomly` and `bomly-lite`. +- `SHA256SUMS`, keylessly signed with cosign (`SHA256SUMS.sigstore.json`). - Linux `.deb`, `.rpm`, `.apk`, and Arch Linux package artifacts for the full `bomly` binary. - Homebrew cask, Scoop, and WinGet manifest pull requests. @@ -186,7 +186,7 @@ Release packaging is driven by `.goreleaser.yaml`. The release workflow uses GoR 1. Merge to `main`. 2. When ready to publish, a maintainer runs the `Auto Version` workflow from `main` and chooses a `patch`, `minor`, or `major` bump. 3. The `Auto Version` workflow updates `cmd/bomly/main.go`, commits the bump, creates a tag such as `v0.2.0`, and starts the `Release` workflow. -4. The `Release` workflow reruns validation and runs GoReleaser. +4. The `Release` workflow reruns validation, then the `release` job runs GoReleaser. 5. GoReleaser cross-compiles `bomly` and `bomly-lite`, packages archives for: - `linux/amd64` - `linux/arm64` @@ -194,12 +194,13 @@ Release packaging is driven by `.goreleaser.yaml`. The release workflow uses GoR - `darwin/arm64` - `windows/amd64` - `windows/arm64` -6. GoReleaser generates `SHA256SUMS` and Linux packages. -7. GoReleaser publishes the GitHub Release, using the configured GoReleaser header plus GitHub-native generated release notes, and uploads archives, packages, and checksums. -8. GoReleaser opens or updates package-manager manifest PRs for Homebrew, Scoop, and WinGet. -9. After the release is published, the `Release lifecycle sync` workflow dispatches the landing-page docs and changelog sync with the published timestamp. +6. GoReleaser generates `SHA256SUMS`, Linux packages, and the cosign signature, then creates the GitHub Release **as a draft** and uploads everything to it. +7. GoReleaser opens or updates package-manager manifest PRs for Homebrew, Scoop, and WinGet (these reference the release's download URLs, which aren't publicly fetchable until the release is published in step 9 — a brief window, typically under a minute). +8. The `provenance` job calls `slsa-github-generator` to attach SLSA provenance (`multiple.intoto.jsonl`) to the same draft release. +9. The `publish` job flips the release from draft to published, using the configured GoReleaser header plus GitHub-native generated release notes. +10. After the release is published, the `Release lifecycle sync` workflow dispatches the landing-page docs and changelog sync with the published timestamp. -The manual approval point for a release is the `Auto Version` workflow that creates the release tag. The GitHub Release is intentionally published automatically after validation so package-manager manifest PRs can reference public release assets and checksums. +The manual approval point for a release is the `Auto Version` workflow that creates the release tag. The GitHub Release stays a draft until every asset — including SLSA provenance — is attached, then a final job publishes it. This is required by GitHub's [immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases) feature: once a release is published, no further assets can be added by anyone, so the provenance file (generated by a separate downstream job, by design — see [Supply-Chain Hardening](#supply-chain-hardening-openssf-scorecard)) must land before publish, not after. ## Yanking Releases