From a4580b91f58074b4d2f00215485e86dbc2ca0f2e Mon Sep 17 00:00:00 2001 From: Justin Beckwith Date: Tue, 18 Aug 2026 15:25:19 -0700 Subject: [PATCH] fix: verify GoReleaser before privileged release execution --- .github/actions/setup-goreleaser/action.yml | 11 +++ .github/workflows/ci.yml | 15 ++- .github/workflows/publish-release.yml | 10 +- .goreleaser-checksums | 4 + docs/releasing.md | 56 +++++++++++ scripts/install-goreleaser | 61 ++++++++++++ scripts/test-goreleaser-installer | 102 ++++++++++++++++++++ 7 files changed, 244 insertions(+), 15 deletions(-) create mode 100644 .github/actions/setup-goreleaser/action.yml create mode 100644 .goreleaser-checksums create mode 100644 docs/releasing.md create mode 100755 scripts/install-goreleaser create mode 100755 scripts/test-goreleaser-installer diff --git a/.github/actions/setup-goreleaser/action.yml b/.github/actions/setup-goreleaser/action.yml new file mode 100644 index 00000000..3af4b840 --- /dev/null +++ b/.github/actions/setup-goreleaser/action.yml @@ -0,0 +1,11 @@ +name: Setup verified GoReleaser +description: Install GoReleaser only after verifying a repository-reviewed archive digest +runs: + using: composite + steps: + - name: Install verified GoReleaser + shell: bash + run: | + installation_directory="$RUNNER_TEMP/openai-cli-goreleaser" + ./scripts/install-goreleaser "$installation_directory" + printf '%s\n' "$installation_directory" >> "$GITHUB_PATH" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57b0764a..f5da69bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,15 +45,14 @@ jobs: - name: Bootstrap run: ./scripts/bootstrap - - name: Read GoReleaser version - id: goreleaser-version - run: echo "version=$(cat .goreleaser-version)" >> "$GITHUB_OUTPUT" + - name: Test verified GoReleaser installer + run: ./scripts/test-goreleaser-installer - - name: Run goreleaser - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 - with: - version: ${{ steps.goreleaser-version.outputs.version }} - args: release --snapshot --clean --skip=publish + - name: Set up verified GoReleaser + uses: ./.github/actions/setup-goreleaser + + - name: Run GoReleaser + run: goreleaser release --snapshot --clean --skip=publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 63ee2418..d7e5b307 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -26,6 +26,8 @@ jobs: with: fetch-depth: 0 persist-credentials: false + - name: Set up verified GoReleaser + uses: ./.github/actions/setup-goreleaser - name: Ensure release tag is on main env: GITHUB_TOKEN: ${{ github.token }} @@ -74,14 +76,8 @@ jobs: repositories: homebrew-tools permission-contents: write permission-pull-requests: write - - name: Read GoReleaser version - id: goreleaser-version - run: echo "version=$(cat .goreleaser-version)" >> "$GITHUB_OUTPUT" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0 - with: - version: ${{ steps.goreleaser-version.outputs.version }} - args: release --clean + run: goreleaser release --clean env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.tap-token.outputs.token }} diff --git a/.goreleaser-checksums b/.goreleaser-checksums new file mode 100644 index 00000000..4e802cf5 --- /dev/null +++ b/.goreleaser-checksums @@ -0,0 +1,4 @@ +d9cd0978d64686eb54e53e9f08b400edc8b1f9034f98fca43ec7a3f641327081 v2.15.4/goreleaser_Darwin_arm64.tar.gz +f9096d896139581cb7aeb4abcee1e7280357a3cd525a22440127be5ea52cf901 v2.15.4/goreleaser_Darwin_x86_64.tar.gz +de01ca1497571e9b348413cd2e7f74be49b8d57696ae386f7eedd06176544a88 v2.15.4/goreleaser_Linux_arm64.tar.gz +aae00c71a4a6d55e08cce9273a1516bdce33c1e07cffb7e502fa6fec4377dede v2.15.4/goreleaser_Linux_x86_64.tar.gz diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..5f3758d1 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,56 @@ +# Release tooling maintenance + +CI and release publishing install GoReleaser with `scripts/install-goreleaser`. +The installer downloads only the version in `.goreleaser-version`, requires a +matching, repository-reviewed SHA-256 digest in `.goreleaser-checksums`, and +verifies the archive before extracting or executing anything from it. Release +write tokens and signing/notarization secrets are not requested until this +verification succeeds. + +The reviewed checksums cover Linux and macOS hosts on x86_64 and arm64. The +host architecture does not limit the Linux, macOS, and Windows release targets +configured in `.goreleaser.yml`. + +The publish workflow installs GoReleaser from its trusted workflow checkout +before switching to the requested release tag. This allows existing release tags +that predate the installer to be republished; the workflow checkout, not the +historical tag, determines the reviewed GoReleaser version. + +## Update GoReleaser + +1. Choose and review the upstream release, then set its version without the `v` + prefix in `.goreleaser-version`. +2. Download its checksum manifest and Sigstore bundle: + + ```sh + version="$(cat .goreleaser-version)" + gh release download "v$version" \ + --repo goreleaser/goreleaser \ + --pattern checksums.txt \ + --pattern checksums.txt.sigstore.json + ``` + +3. Verify that the manifest was signed by GoReleaser's upstream release workflow + for the exact tag, using an independently trusted `cosign` installation: + + ```sh + cosign verify-blob \ + --certificate-identity "https://github.com/goreleaser/goreleaser/.github/workflows/release.yml@refs/tags/v$version" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + --bundle checksums.txt.sigstore.json \ + checksums.txt + ``` + +4. Replace the reviewed manifest with the four supported host-archive digests: + + ```sh + awk -v version="$version" \ + '$2 ~ /^goreleaser_(Darwin|Linux)_(arm64|x86_64)\.tar\.gz$/ { + print $1 " v" version "/" $2 + }' checksums.txt > .goreleaser-checksums + test "$(wc -l < .goreleaser-checksums | tr -d ' ')" -eq 4 + ``` + +5. Run `./scripts/test-goreleaser-installer` and review the version and all four + digest changes together. The installer intentionally fails before downloading + if a version or host platform does not have exactly one reviewed digest. diff --git a/scripts/install-goreleaser b/scripts/install-goreleaser new file mode 100755 index 00000000..d865b68c --- /dev/null +++ b/scripts/install-goreleaser @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 INSTALLATION_DIRECTORY" >&2 + exit 1 +fi + +repository_root="$(cd "$(dirname "$0")/.." && pwd)" +installation_directory="$1" +# Update the version and reviewed digests together; see docs/releasing.md. +version="$(cat "$repository_root/.goreleaser-version")" + +if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid GoReleaser version: $version" >&2 + exit 1 +fi + +case "$(uname -s)" in + Darwin) platform=Darwin ;; + Linux) platform=Linux ;; + *) + echo "Unsupported GoReleaser host operating system" >&2 + exit 1 + ;; +esac + +case "$(uname -m)" in + amd64 | x86_64) architecture=x86_64 ;; + aarch64 | arm64) architecture=arm64 ;; + *) + echo "Unsupported GoReleaser host architecture" >&2 + exit 1 + ;; +esac + +archive_name="goreleaser_${platform}_${architecture}.tar.gz" +release_asset="v${version}/${archive_name}" +expected_checksum="$(awk -v asset="$release_asset" '$2 == asset { print $1 }' "$repository_root/.goreleaser-checksums")" + +if [[ ! "$expected_checksum" =~ ^[[:xdigit:]]{64}$ ]]; then + echo "Expected exactly one reviewed SHA-256 digest for $release_asset" >&2 + exit 1 +fi + +temporary_directory="$(mktemp -d)" +trap 'rm -rf "$temporary_directory"' EXIT +archive_path="$temporary_directory/$archive_name" + +curl --fail --silent --show-error --location --proto '=https' --tlsv1.2 \ + --output "$archive_path" \ + "https://github.com/goreleaser/goreleaser/releases/download/$release_asset" + +printf '%s %s\n' "$expected_checksum" "$archive_path" | shasum -a 256 --check - + +tar -xzf "$archive_path" -C "$temporary_directory" goreleaser +mkdir -p "$installation_directory" +install -m 0755 "$temporary_directory/goreleaser" "$installation_directory/goreleaser" + +echo "Installed verified GoReleaser $version for $platform/$architecture" diff --git a/scripts/test-goreleaser-installer b/scripts/test-goreleaser-installer new file mode 100755 index 00000000..bd6c62ec --- /dev/null +++ b/scripts/test-goreleaser-installer @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository_root="$(cd "$(dirname "$0")/.." && pwd)" +temporary_directory="$(mktemp -d)" +trap 'rm -rf "$temporary_directory"' EXIT + +fixture_repository="$temporary_directory/repository" +fixture_archive_directory="$temporary_directory/archive" +fixture_tools="$temporary_directory/tools" +curl_log="$temporary_directory/curl.log" +execution_log="$temporary_directory/executed.log" +reviewed_version="$(cat "$repository_root/.goreleaser-version")" + +mkdir -p "$fixture_repository/scripts" "$fixture_archive_directory" "$fixture_tools" +cp "$repository_root/scripts/install-goreleaser" "$fixture_repository/scripts/install-goreleaser" +printf '%s\n' "$reviewed_version" > "$fixture_repository/.goreleaser-version" + +cat > "$fixture_archive_directory/goreleaser" <<'EOF' +#!/usr/bin/env bash +printf 'executed\n' >> "$TEST_EXECUTION_LOG" +EOF +chmod +x "$fixture_archive_directory/goreleaser" +tar -czf "$temporary_directory/verified.tar.gz" -C "$fixture_archive_directory" goreleaser + +cat > "$fixture_tools/curl" <<'EOF' +#!/usr/bin/env bash + +set -euo pipefail + +while [[ $# -gt 0 ]]; do + case "$1" in + --output) + output="$2" + shift 2 + ;; + *) shift ;; + esac +done + +printf 'downloaded\n' >> "$TEST_CURL_LOG" +cp "$TEST_ARCHIVE" "$output" +EOF +chmod +x "$fixture_tools/curl" + +case "$(uname -s)" in + Darwin) platform=Darwin ;; + Linux) platform=Linux ;; + *) echo "Unsupported test host operating system" >&2; exit 1 ;; +esac + +case "$(uname -m)" in + amd64 | x86_64) architecture=x86_64 ;; + aarch64 | arm64) architecture=arm64 ;; + *) echo "Unsupported test host architecture" >&2; exit 1 ;; +esac + +archive_name="goreleaser_${platform}_${architecture}.tar.gz" +checksum="$(shasum -a 256 "$temporary_directory/verified.tar.gz" | awk '{print $1}')" +printf '%s v%s/%s\n' "$checksum" "$reviewed_version" "$archive_name" > "$fixture_repository/.goreleaser-checksums" + +export PATH="$fixture_tools:$PATH" +export TEST_ARCHIVE="$temporary_directory/verified.tar.gz" +export TEST_CURL_LOG="$curl_log" +export TEST_EXECUTION_LOG="$execution_log" + +"$fixture_repository/scripts/install-goreleaser" "$temporary_directory/installed" +[[ -x "$temporary_directory/installed/goreleaser" ]] +[[ ! -e "$execution_log" ]] + +printf 'tampered archive\n' > "$temporary_directory/tampered.tar.gz" +TEST_ARCHIVE="$temporary_directory/tampered.tar.gz" +export TEST_ARCHIVE + +if "$fixture_repository/scripts/install-goreleaser" "$temporary_directory/tampered-installation"; then + echo "Installer accepted an archive with the wrong digest" >&2 + exit 1 +fi +[[ ! -e "$temporary_directory/tampered-installation" ]] + +printf '%s v%s/%s\n' "$checksum" "$reviewed_version" "$archive_name" >> "$fixture_repository/.goreleaser-checksums" +: > "$curl_log" + +if "$fixture_repository/scripts/install-goreleaser" "$temporary_directory/ambiguous-installation"; then + echo "Installer accepted multiple reviewed digests for one archive" >&2 + exit 1 +fi +[[ ! -s "$curl_log" ]] +[[ ! -e "$temporary_directory/ambiguous-installation" ]] + +printf '0.0.0\n' > "$fixture_repository/.goreleaser-version" +: > "$curl_log" + +if "$fixture_repository/scripts/install-goreleaser" "$temporary_directory/unreviewed-installation"; then + echo "Installer accepted a version without a reviewed digest" >&2 + exit 1 +fi +[[ ! -s "$curl_log" ]] +[[ ! -e "$temporary_directory/unreviewed-installation" ]] + +echo "GoReleaser installer verification tests passed"