From 20630147e3cb8c378c8b9c8ee9b6a371cd517581 Mon Sep 17 00:00:00 2001 From: Imani Manyara Date: Fri, 13 Nov 2026 09:41:38 -0500 Subject: [PATCH] Let Go release callers pick runner, environment and signing Adds a runs-on input (default ubuntu-latest) for a darwin build that needs cgo on macOS, an environment input (default empty) for a release run under a GitHub environment, and an optional SIGNPATH_API_TOKEN secret exported to GoReleaser. Both attestation steps now skip on a private repository, where GitHub Free refuses attestations. Each is documented in go-release/README.md and pinned by a test. --- .github/workflows/go-release.yml | 31 +++++++++++++++++++++++-- go-release/README.md | 14 +++++++++++ go-release/tests/test_go_release.py | 36 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go-release.yml b/.github/workflows/go-release.yml index cf52793..cf094d9 100644 --- a/.github/workflows/go-release.yml +++ b/.github/workflows/go-release.yml @@ -12,6 +12,11 @@ # with: # tag: ${{ inputs.tag || github.ref_name }} # +# Optional: runs-on (a macOS runner for a project whose darwin build needs cgo), environment (a +# GitHub environment the job runs under, such as `release`), and the SIGNPATH_API_TOKEN secret, +# passed explicitly (`secrets: SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}`, never +# `secrets: inherit`) for a GoReleaser config that signs through SignPath. +# # Steps: resolve and check the tag; refuse a GoReleaser config that publishes anywhere else; take # the release notes from the tag's CHANGELOG section (fail closed); push the module's subdirectory # alias tag (src/vX.Y.Z); run GoReleaser; read the published notes back; run the caller's optional @@ -53,13 +58,29 @@ on: required: false type: string default: '' + runs-on: + description: 'Runner label for the job, such as macos-latest for a darwin build that needs cgo' + required: false + type: string + default: ubuntu-latest + environment: + description: 'GitHub environment the job runs under, such as release; empty for none' + required: false + type: string + default: '' + secrets: + SIGNPATH_API_TOKEN: + description: 'SignPath API token, exported to GoReleaser for a config that signs through SignPath' + required: false permissions: {} jobs: release: name: Publish release - runs-on: ubuntu-latest + runs-on: ${{ inputs.runs-on }} + # An empty name runs the job under no environment. + environment: ${{ inputs.environment }} timeout-minutes: 40 permissions: contents: write # create the release, upload assets, push the module alias tag @@ -173,6 +194,8 @@ jobs: env: GITHUB_TOKEN: ${{ github.token }} GORELEASER_CURRENT_TAG: ${{ inputs.tag }} + # Empty unless the caller passes it; only a config that signs through SignPath reads it. + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} # Fail closed on an empty release body: read the published release back and require at least # the changelog section passed in. @@ -196,7 +219,11 @@ jobs: POST_RELEASE: ${{ inputs.post-release }} run: bash -euo pipefail -c "$POST_RELEASE" + # GitHub Free refuses artifact attestations on a private repository, so both attestation + # steps run on public repositories only. github.event.repository is the caller's repository, + # set on the push and workflow_dispatch events that start a release. - name: Attest build provenance + if: github.event.repository.private == false uses: actions/attest-build-provenance@v4 with: subject-checksums: ${{ env.DIST }}/checksums.txt @@ -207,7 +234,7 @@ jobs: if [ -s "$RUNNER_TEMP/extra-checksums.txt" ]; then echo "present=true" >> "$GITHUB_OUTPUT"; fi - name: Attest the extra assets - if: steps.extra.outputs.present == 'true' + if: steps.extra.outputs.present == 'true' && github.event.repository.private == false uses: actions/attest-build-provenance@v4 with: subject-checksums: ${{ runner.temp }}/extra-checksums.txt diff --git a/go-release/README.md b/go-release/README.md index f5fa17b..d5ce1bb 100644 --- a/go-release/README.md +++ b/go-release/README.md @@ -24,6 +24,20 @@ Push a version tag only for final code: tags are final, and a pushed tag publish | `module-alias-prefix` | `src/` | the module's subdirectory, for its `src/vX.Y.Z` alias tag; empty for none | | `changelog` | `CHANGELOG.md` | its `## [X.Y.Z]` section becomes the release notes | | `post-release` | empty | a command run after GoReleaser to attach extra assets; it may write `$RUNNER_TEMP/extra-checksums.txt` to have them attested | +| `runs-on` | `ubuntu-latest` | the runner label for the job; `macos-latest` for a project whose darwin build needs cgo | +| `environment` | empty | a GitHub environment the job runs under (for example `release`); empty for none | + +## Secrets + +| Secret | Required | What it is | +|---|---|---| +| `SIGNPATH_API_TOKEN` | no | exported to the GoReleaser step for a config that signs through SignPath. Pass it explicitly, `secrets: SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}`, never `secrets: inherit` | + +## Attestations + +Build provenance is attested for `checksums.txt` and any extra assets on a **public** repository +only: GitHub Free refuses attestations on a private one, so both steps skip when +`github.event.repository.private` is true. The release itself still publishes. ## What it refuses diff --git a/go-release/tests/test_go_release.py b/go-release/tests/test_go_release.py index 38dbf8c..a0d194d 100644 --- a/go-release/tests/test_go_release.py +++ b/go-release/tests/test_go_release.py @@ -48,6 +48,42 @@ def test_it_checks_out_the_tag_and_attests(self): self.assertIn("actions/attest-build-provenance", self.text) self.assertIn("GORELEASER_CURRENT_TAG: ${{ inputs.tag }}", self.text) + def input_block(self, name): + m = re.search(r"\n " + re.escape(name) + r":\n((?: .*\n)+)", self.text) + self.assertIsNotNone(m, f"no {name} input") + return m.group(1) + + def job(self): + return re.search(r"^jobs:\n((?:[ \t]+.*\n|\n)+)", self.text, re.M).group(1) + + def test_the_runner_is_an_input_defaulting_to_ubuntu(self): + block = self.input_block("runs-on") + self.assertIn("required: false", block) + self.assertRegex(block, r"default: ['\"]?ubuntu-latest['\"]?\n") + self.assertRegex(self.job(), r"\n runs-on: \$\{\{ inputs\.runs-on \}\}\n") + + def test_the_environment_is_an_optional_input(self): + block = self.input_block("environment") + self.assertIn("required: false", block) + self.assertRegex(block, r"default: ''\n") + self.assertRegex(self.job(), r"\n environment: \$\{\{ inputs\.environment \}\}\n") + + def test_the_signpath_token_is_an_optional_secret_for_goreleaser(self): + secrets = re.search(r"\n secrets:\n((?: .*\n)+)", self.text) + self.assertIsNotNone(secrets, "workflow_call declares no secrets") + m = re.search(r" SIGNPATH_API_TOKEN:\n((?: .*\n)+)", secrets.group(1)) + self.assertIsNotNone(m, "no SIGNPATH_API_TOKEN secret") + self.assertIn("required: false", m.group(1)) + step = re.search(r"- name: Run GoReleaser\n((?: .*\n)+)", self.text).group(1) + self.assertIn("SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}", step) + + def test_attestations_skip_a_private_repository(self): + steps = re.findall(r"\n((?: - name: .*\n)(?: .*\n)*)", self.text) + attest = [st for st in steps if "actions/attest-build-provenance" in st] + self.assertEqual(len(attest), 2, "expected two attestation steps") + for st in attest: + self.assertRegex(st, r"\n if: .*github\.event\.repository\.private == false", st) + class CallerTemplate(unittest.TestCase): def setUp(self):