From 5f7da31a4ecf1aae5df16b9b9d09280cf746b288 Mon Sep 17 00:00:00 2001 From: Levi Durfee Date: Fri, 16 Oct 2026 00:45:54 +0000 Subject: [PATCH 1/3] Run CI on development and document the branch workflow development now collects work for the next release, as in the rypt module: CI runs on pushes to it, CONTRIBUTING.md describes the main/development/feature-branch flow and the branch protection, and docs/releasing.md starts a release with a pull request into main. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NTEjHbdKp83qgQgmqR1BJ6 --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 26 ++++++++++++++++++++++++++ docs/releasing.md | 17 +++++++++++------ 3 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 043df80..ea145fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, development] pull_request: permissions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3daa2ba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +## Branches + +- `main` is what has been released. It changes only through pull requests, + and every release is a tag on it. +- `development` collects the work for the next release. +- Work happens on a short-lived branch from `development`, named for what it + does: `feature/...`, `fix/...`, `docs/...`, `ci/...`. Open a pull request + into `development`, and delete the branch once it is merged. +- A release is a pull request from `development` into `main`, followed by a + tag on `main`; see [docs/releasing.md](docs/releasing.md). If `main` then + has commits `development` lacks, merge `main` back into `development`. +- An urgent fix branches from `main` as `hotfix/...`, merges into `main` and + is released as a patch, and is then merged into `development`. + +Both `main` and `development` are protected: a change needs a pull request +whose `test` and `build` checks pass, and nobody, admins included, can push +to them directly or force-push. + +Keep commits focused, with messages in the imperative. Rebase a branch onto +`development` rather than merging `development` into it. + +## Tests + +See [Development](README.md#development) in the README. diff --git a/docs/releasing.md b/docs/releasing.md index 8fe4523..f11db2f 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,5 +1,15 @@ # Releasing rypt +A release is a pull request from `development` into `main` (see +[CONTRIBUTING.md](../CONTRIBUTING.md)). Once it is merged, tag the merge +commit on `main`: + +```sh +git switch main && git pull +git tag -a v1.0.0 -m v1.0.0 +git push origin v1.0.0 +``` + Pushing a tag that starts with `v` runs [.github/workflows/release.yml](../.github/workflows/release.yml). It runs `make vet test race`, builds the six binaries with GoReleaser @@ -8,11 +18,6 @@ binaries, publishes a GitHub release with the binaries, `SHA256SUMS`, and a changelog of the commits since the previous tag, and then updates the Homebrew cask in [x6c-co/homebrew-tap](https://github.com/x6c-co/homebrew-tap). -```sh -git tag -a v1.0.0 -m v1.0.0 -git push origin v1.0.0 -``` - A tag with a pre-release suffix, such as `v1.1.0-rc.1`, is published as a pre-release and leaves the Homebrew cask alone. `rypt version` prints the tag. @@ -21,7 +26,7 @@ output: `rypt-linux-amd64`, `rypt-darwin-arm64`, `rypt-windows-amd64.exe`, and so on. [.github/workflows/ci.yml](../.github/workflows/ci.yml) runs the same tests on -every push to `main` and every pull request, and builds all six targets with +every push to `main` and `development` and every pull request, and builds all six targets with the release configuration, unsigned. To build locally with the release configuration, unsigned, install From 517503cb291846f0ec53d39359efb3bea182e1d7 Mon Sep 17 00:00:00 2001 From: Levi Durfee Date: Fri, 16 Oct 2026 00:55:36 +0000 Subject: [PATCH 2/3] Ship an SBOM with each binary and check for vulnerabilities GoReleaser now makes a CycloneDX SBOM for each binary with Syft, from the binary's build information, named like the binary and listed in SHA256SUMS. CI installs Syft, so the snapshot build checks this on every pull request. A govulncheck job fails CI when the code calls a function with a known vulnerability. Its first run found eight standard-library vulnerabilities in what Go 1.27.0 builds, which v0.1.0 and v0.1.1 were built with; Go 1.27.2 fixes them. CI and releases now build with the newest Go release, and go.mod asks for at least 1.27.2, so that "go install" users get the fixes too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NTEjHbdKp83qgQgmqR1BJ6 --- .github/workflows/ci.yml | 22 ++++++++++++++++++++-- .github/workflows/release.yml | 8 +++++++- .goreleaser.yaml | 11 +++++++++++ docs/releasing.md | 7 +++++++ go.mod | 2 +- 5 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 043df80..f97242a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,11 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The newest Go release, so that binaries get its security fixes. - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version-file: go.mod + go-version: stable + check-latest: true - run: make vet test race # Builds every release target with the release config, without signing. @@ -27,10 +29,26 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # The newest Go release, so that binaries get its security fixes. - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version-file: go.mod + go-version: stable + check-latest: true + # GoReleaser makes each binary's SBOM with Syft. + - uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + syft-version: v1.54.1 - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: version: v2.18.3 args: release --snapshot --clean + + # Fails when the code calls a function with a known vulnerability, in a + # module or in the Go standard library. + vulncheck: + runs-on: ubuntu-latest + steps: + - uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0 + with: + go-version-input: stable + check-latest: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 91c8204..e8f52b6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,12 +54,18 @@ jobs: with: fetch-depth: 0 # the changelog needs the previous tag persist-credentials: false + # The newest Go release, so that binaries get its security fixes. - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version-file: go.mod + go-version: stable + check-latest: true cache: false # build release binaries from a clean module cache - run: make vet test race + # GoReleaser makes each binary's SBOM with Syft. + - uses: anchore/sbom-action/download-syft@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3 + with: + syft-version: v1.54.1 - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3 with: version: v2.18.3 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index db4a763..6e6aaa8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -24,6 +24,17 @@ archives: - formats: [binary] name_template: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" +# A CycloneDX SBOM for each binary, named like it: rypt-linux-amd64.cdx.json +# and so on. Syft reads the Go version and the modules from the binary's +# build information, so the SBOM lists exactly what was compiled in. +sboms: + - id: binaries + artifacts: binary + cmd: syft + args: ["$artifact", "--output", "cyclonedx-json=$document", "--quiet"] + documents: + - "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}.cdx.json" + checksum: name_template: SHA256SUMS algorithm: sha256 diff --git a/docs/releasing.md b/docs/releasing.md index 8fe4523..72d6130 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -20,6 +20,13 @@ The release binaries are named and checksummed the same way as `make dist` output: `rypt-linux-amd64`, `rypt-darwin-arm64`, `rypt-windows-amd64.exe`, and so on. +Each binary has a CycloneDX SBOM beside it, named like it +(`rypt-linux-amd64.cdx.json`), which GoReleaser makes with Syft from the +binary's own build information: the Go version and every module compiled in. +The SBOMs are listed in `SHA256SUMS` too. The release workflow builds with +the newest Go release, whatever go.mod's `go` line says, so that binaries +get the standard library's security fixes. + [.github/workflows/ci.yml](../.github/workflows/ci.yml) runs the same tests on every push to `main` and every pull request, and builds all six targets with the release configuration, unsigned. diff --git a/go.mod b/go.mod index a5172f8..4b89f14 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/x6c-co/rypt-cli -go 1.27.0 +go 1.27.2 require ( github.com/x6c-co/rypt v0.1.0 From 30bdde40bcf4b826c7c4b24a9b2f4e1c21da5f3a Mon Sep 17 00:00:00 2001 From: Levi Durfee Date: Fri, 16 Oct 2026 01:00:59 +0000 Subject: [PATCH 3/3] Attest the build provenance of every release file The release workflow's last step signs a SLSA build provenance attestation, with actions/attest, for every file in SHA256SUMS: each binary and its SBOM. It shows that this workflow built the file from the tagged commit, which a checksum published beside the file cannot. The README shows how to check one with "gh attestation verify". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NTEjHbdKp83qgQgmqR1BJ6 --- .github/workflows/release.yml | 12 ++++++++++++ README.md | 18 ++++++++++++++++++ docs/releasing.md | 5 ++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8f52b6..6982e65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write # to sign the attestation with Sigstore + attestations: write # to store the attestation with GitHub steps: # GoReleaser skips signing when MACOS_SIGN_P12 is empty, and a missing # secret arrives as an empty string. Fail rather than publish unsigned @@ -78,3 +80,13 @@ jobs: MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + + # Sign a SLSA build provenance attestation for every file in + # SHA256SUMS, each binary and its SBOM: it records that this workflow + # built the file from the tagged commit. "gh attestation verify" + # checks it; see the README. The release is already published by now, + # so if this step fails, delete the GitHub release (not the tag) and + # re-run the workflow. + - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-checksums: dist/SHA256SUMS diff --git a/README.md b/README.md index c439e05..eb467b8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,24 @@ that `go install` records. To check a release or `make dist` output, run (`shasum -a 256 -c --ignore-missing SHA256SUMS` on macOS) in the directory that holds it. +### Verify a release + +From v0.2.0 on, every release file has a signed build provenance +attestation, which shows that this repository's release workflow built it +from the tagged commit. A checksum only shows that a file matches +`SHA256SUMS`, which is published beside it; the attestation shows where both +came from. With the [GitHub CLI](https://cli.github.com): + +```sh +gh attestation verify rypt-linux-amd64 \ + --repo x6c-co/rypt-cli \ + --signer-workflow x6c-co/rypt-cli/.github/workflows/release.yml +``` + +The same command checks the `rypt` that Homebrew installed: give it +`"$(command -v rypt)"` instead of a file name. Each binary's SBOM +(`rypt-linux-amd64.cdx.json` and so on) is attested the same way. + ## Usage ``` diff --git a/docs/releasing.md b/docs/releasing.md index ad1aa24..d132d69 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -28,7 +28,10 @@ so on. Each binary has a CycloneDX SBOM beside it, named like it (`rypt-linux-amd64.cdx.json`), which GoReleaser makes with Syft from the binary's own build information: the Go version and every module compiled in. -The SBOMs are listed in `SHA256SUMS` too. The release workflow builds with +The SBOMs are listed in `SHA256SUMS` too, and the workflow's last step signs a +build provenance attestation for every file in `SHA256SUMS` with +[actions/attest](https://github.com/actions/attest); the README shows how to +verify one. The release workflow builds with the newest Go release, whatever go.mod's `go` line says, so that binaries get the standard library's security fixes.