From 34bd586bbd6635128e04a94d67ebc029e8e980d3 Mon Sep 17 00:00:00 2001 From: jferrl Date: Mon, 2 Nov 2026 18:10:49 +0100 Subject: [PATCH] ci: build releases with current Go, attest them, run govulncheck Release binaries were built with go.mod's go 1.26.0, because setup-go reads go-version-file literally. Every standard-library fix since then shipped unpatched: govulncheck finds 45 reachable vulnerabilities in v1.12.0's binary, in net/http, crypto/tls and crypto/x509 among others. The release job now sets up the newest stable Go; go.mod stays the library's minimum. Every release archive and every gh extension binary now gets a build provenance attestation, so gh attestation verify shows a file came from this workflow and a tag. checksums.txt came from the same job it vouched for, so it could not show that. Archive attestation is its own job, which "Re-run failed jobs" can retry, since a re-run of goreleaser fails on the assets it already uploaded; the extension job is split into unpack, attest and publish steps. govulncheck runs on every push and pull request to main and weekly, with the Go the release builds with. --- .github/workflows/govulncheck.yml | 28 +++++++++++++ .github/workflows/release.yml | 66 +++++++++++++++++++++++++------ CHANGELOG.md | 17 ++++++++ README.md | 8 ++++ 4 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/govulncheck.yml diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml new file mode 100644 index 0000000..755888c --- /dev/null +++ b/.github/workflows/govulncheck.yml @@ -0,0 +1,28 @@ +name: govulncheck + +on: + push: + branches: + - main + pull_request: + branches: + - main + # New vulnerabilities are published without any change here, so the code is + # checked again every week. + schedule: + - cron: "17 6 * * 1" + +permissions: + contents: read + +jobs: + govulncheck: + runs-on: ubuntu-latest + steps: + # The same Go the release builds with, so a standard-library finding is + # one the shipped binaries have. + - uses: golang/govulncheck-action@032d45514ae346b1db93c04b0c90b841c370344f # v1.1.0 + with: + go-version-input: stable + check-latest: true + go-package: ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69eff89..a83afc4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,14 @@ jobs: # checkout would produce an empty one. fetch-depth: 0 + # The newest Go, not go.mod's minimum: go.mod's go line is the oldest Go + # the library supports, and building the binaries with exactly that one + # shipped every standard-library fix since, net/http and crypto/tls + # included, unpatched. govulncheck checks the same version. - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version-file: go.mod + go-version: stable + check-latest: true # A missing or empty tap token skips the cask instead of failing the # release, which is the right trade but is otherwise invisible. Say so in @@ -54,6 +59,31 @@ jobs: # still publishes; only the cask is skipped. HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + attest: + # Provenance for every archive in checksums.txt, so anyone can check one + # was built by this workflow from this tag: + # gh attestation verify githubauth___.tar.gz -R jferrl/go-githubauth + # Its own job for the gh-extension job's reason: a re-run of goreleaser + # fails on the assets it already uploaded, so only a separate job can be + # retried with "Re-run failed jobs". + needs: goreleaser + runs-on: ubuntu-latest + permissions: + contents: read + # attest-build-provenance signs with the job's OIDC identity and stores + # the attestation on the repository. + id-token: write + attestations: write + steps: + - name: Download the release's checksums + env: + TAG: ${{ github.ref_name }} + GH_TOKEN: ${{ github.token }} + run: gh release download "$TAG" -R "$GITHUB_REPOSITORY" -p checksums.txt + - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-checksums: checksums.txt + gh-extension: # gh installs a binary extension from the latest release of a gh-* repo, # picking the asset whose name ends in os-arch (.exe on Windows), and takes @@ -70,23 +100,18 @@ jobs: cancel-in-progress: false permissions: contents: read + # The bare binaries are attested too: they are what gh installs, and + # their digests differ from the archives they come out of. + id-token: write + attestations: write steps: - - name: Publish the gh extension + - name: Unpack the release's binaries shell: bash env: TAG: ${{ github.ref_name }} - REPO: jferrl/gh-githubauth # Downloads the public release just made. GH_TOKEN: ${{ github.token }} - # The tap token, extended to jferrl/gh-githubauth: the job's own - # token cannot write to another repository. - PUBLISH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} run: | - if [ -z "$PUBLISH_TOKEN" ]; then - echo "::warning::HOMEBREW_TAP_GITHUB_TOKEN is unset or empty. The gh extension is not published." - exit 0 - fi - gh release download "$TAG" -R "$GITHUB_REPOSITORY" -p 'githubauth_*' -p checksums.txt -D dl (cd dl && sha256sum -c checksums.txt) @@ -111,7 +136,24 @@ jobs: exit 1 fi - export GH_TOKEN="$PUBLISH_TOKEN" + - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: ext/* + + - name: Publish the gh extension + shell: bash + env: + TAG: ${{ github.ref_name }} + REPO: jferrl/gh-githubauth + # The tap token, extended to jferrl/gh-githubauth: the job's own + # token cannot write to another repository. + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} + run: | + if [ -z "$GH_TOKEN" ]; then + echo "::warning::HOMEBREW_TAP_GITHUB_TOKEN is unset or empty. The gh extension is not published." + exit 0 + fi + # gh installs from the latest release, so it is built as a draft and # published only once every binary is on it. A published one is never # touched again; a draft left by a failed run is picked up. diff --git a/CHANGELOG.md b/CHANGELOG.md index f046b74..344a99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 > That `v2.0.2` tag is the only thing carrying the retraction — the `v2/` directory no > longer exists on `main` — so the tag must not be deleted or the retraction is lost. +## [Unreleased] + +### Security + +- Release binaries were built with Go 1.26.0, the oldest Go `go.mod` supports, so every + standard-library fix since shipped unpatched: v1.12.0's binary has 45 known + vulnerabilities that govulncheck finds reachable, in `net/http`, `crypto/tls` and + `crypto/x509` among others. Releases are now built with the newest stable Go. The library + is unaffected: your own Go toolchain builds it + +### Added + +- Release archives and the gh extension's binaries carry a build provenance attestation: + `gh attestation verify -R jferrl/go-githubauth` shows it was built by the release + workflow from a tag +- govulncheck runs on every change and weekly, with the Go the release builds with + ## [v1.12.0] - 2026-10-31 `githubauth token --exec` now revokes the token when the command exits, and the CLI can diff --git a/README.md b/README.md index c98ce1d..ed06a23 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,14 @@ From v1.12.0 it is also a [GitHub CLI](https://cli.github.com) extension, run as gh extension install jferrl/gh-githubauth ``` +Releases after v1.12.0 carry a build provenance attestation for every archive and every +extension binary, which shows a file was built by this repository's release workflow from +a tagged commit. Check one before you run it: + +```bash +gh attestation verify githubauth__darwin_arm64.tar.gz -R jferrl/go-githubauth +``` + ```bash githubauth token --client-id Iv1.abc --key app.pem --installation 12345 ```