The release workflow runs with publish-capable tokens while using mutable action tags and the latest GoReleaser binary.
Evidence
.github/workflows/release.yml:9 to .github/workflows/release.yml:10 grants write access to repository contents.
.github/workflows/release.yml:17 to .github/workflows/release.yml:39 uses version tags for actions and passes both release tokens to GoReleaser.
.github/workflows/release.yml:30 to .github/workflows/release.yml:33 requests the latest GoReleaser action tooling.
.github/workflows/ci.yml:25 to .github/workflows/ci.yml:72 also uses mutable action version tags in CI.
.goreleaser.yaml:62 to .goreleaser.yaml:70 publishes the Homebrew formula with the tap token.
Reproduction
Inspect the release workflow. Any unexpected upstream change in a referenced mutable action or the latest GoReleaser binary executes in a job that can publish GitHub release artifacts and update the Homebrew tap.
Why It Matters
The release job is the software supply-chain boundary for distributed binaries and the Homebrew formula. Mutable tooling in that token context increases the blast radius of compromised or unexpectedly changed upstream release tooling.
Suggested Fix
Pin actions and GoReleaser to immutable versions or SHAs, keep permissions minimal per job, and document a regular pin-refresh process.
## Finding
The release workflow runs with publish-capable tokens while using mutable action tags and the latest GoReleaser binary.
## Evidence
- `.github/workflows/release.yml:9` to `.github/workflows/release.yml:10` grants write access to repository contents.
- `.github/workflows/release.yml:17` to `.github/workflows/release.yml:39` uses version tags for actions and passes both release tokens to GoReleaser.
- `.github/workflows/release.yml:30` to `.github/workflows/release.yml:33` requests the latest GoReleaser action tooling.
- `.github/workflows/ci.yml:25` to `.github/workflows/ci.yml:72` also uses mutable action version tags in CI.
- `.goreleaser.yaml:62` to `.goreleaser.yaml:70` publishes the Homebrew formula with the tap token.
## Reproduction
Inspect the release workflow. Any unexpected upstream change in a referenced mutable action or the latest GoReleaser binary executes in a job that can publish GitHub release artifacts and update the Homebrew tap.
## Why It Matters
The release job is the software supply-chain boundary for distributed binaries and the Homebrew formula. Mutable tooling in that token context increases the blast radius of compromised or unexpectedly changed upstream release tooling.
## Suggested Fix
Pin actions and GoReleaser to immutable versions or SHAs, keep permissions minimal per job, and document a regular pin-refresh process.
Status: confirmed — every file:line citation in the body checks out against the tree at v0.2.1 (HEAD f4a111f); the release job runs publish-capable tokens alongside mutable action tags and version: latest GoReleaser.
Severity: medium (label is bug/github_actions, no severity label). This is a real supply-chain hardening gap, but it is latent: it requires a compromised/mutated upstream action or GoReleaser build to bite. No defect in shipped behavior. Medium is appropriate — not high, since there's no evidence of an active vector and the tokens are scoped (contents: write + a dedicated tap PAT), not org-admin.
Verified root cause (this is CI/config, not Go source — the render/detect boundary code is unrelated):
.github/workflows/release.yml:9-10 — workflow-level permissions: contents: write; the goreleaser job has no job-level permissions: override, so the write scope is the job's effective scope.
.github/workflows/release.yml:18,25,31 — actions/checkout@v6, actions/setup-go@v6, goreleaser/goreleaser-action@v7 (mutable major tags).
.github/workflows/release.yml:33 — version: latest pulls the newest GoReleaser binary at run time.
.github/workflows/release.yml:37,39 — GITHUB_TOKEN and HOMEBREW_TAP_TOKEN both injected into that same step.
.github/workflows/ci.yml:25,28,46,61,82,94,105 — mutable tags (checkout@v6, setup-go@v6, golangci-lint-action@v9, upload-artifact@v7) plus version: latest for golangci-lint at ci.yml:51.
.goreleaser.yaml:62-70 — brews: block pushes the formula to KofTwentyTwo/homebrew-tap using {{ .Env.HOMEBREW_TAP_TOKEN }} (line 70).
Suggested fix:
Pin all uses: to a commit SHA with the version in a trailing comment (e.g. actions/checkout@<sha> # v6.x), and let Dependabot (github_actions ecosystem) bump the SHAs — this preserves the current "dependabot bumps the tag" flow (cf. ce4edf7) while making each ref immutable.
Pin GoReleaser: replace version: latest at release.yml:33 with a concrete tag (e.g. version: "~> v2" or an exact vX.Y.Z). Mirror the already-good precedent in this repo — ci.yml:72 pins go-test-coverage to @v2.18.8. Apply the same discipline to golangci-lint's version: latest at ci.yml:51.
Scope the token blast radius: add a job-level permissions: { contents: write } block on the goreleaser job so the grant is explicit and local rather than inherited workflow-wide; the tap PAT is already a separate secret (good).
Document a pin-refresh cadence in RELEASING.md.
No precedent transfer / no boundary work: the v0.2.1 universal-shebang fix and its two-boundary rule (internal/render/render.go INSERT path + internal/detect/detect.go PreserveBoundary) plus the filetype table are about header placement and are not relevant here. This change is YAML-only.
100% gate: unaffected — no ./internal/... or ./cmd/... Go source changes, so .testcoverage.yml (total: 100) stays green with no new tests.
Corrections to the issue:
The body lumps ci.yml:72 into "mutable action version tags," but that line is the go-test-coverage gate invoked as go run github.com/vladopajic/go-test-coverage/v2@v2.18.8 — it is already pinned to an exact version, not mutable. The genuinely mutable CI refs are the uses: major tags and golangci-lint-action's version: latest (ci.yml:51), which the body's range (25-72) covers but does not call out.
Minor: release.yml:30-33 is the GoReleaser step block; the actual version: latest line is :33.
Open questions (maintainer must decide before implementing):
Pin style: exact SHAs (strongest, Dependabot-maintained) vs. exact semver tags (readable, weaker)? Repo currently relies on Dependabot tag bumps — moving to SHAs changes that workflow.
GoReleaser version policy: hard-pin an exact vX.Y.Z vs. a ~> v2 constraint (floating within a major)?
Should the job-level permissions tightening be in scope for this issue or tracked separately?
Overlaps: none identified among the issues reviewed. This is the only CI/supply-chain hardening item; it does not depend on the render/detect boundary issues.
Automated maintainer review.
**Status**: confirmed — every file:line citation in the body checks out against the tree at `v0.2.1` (HEAD `f4a111f`); the release job runs publish-capable tokens alongside mutable action tags and `version: latest` GoReleaser.
**Severity**: medium (label is `bug`/`github_actions`, no severity label). This is a real supply-chain hardening gap, but it is latent: it requires a compromised/mutated upstream action or GoReleaser build to bite. No defect in shipped behavior. Medium is appropriate — not high, since there's no evidence of an active vector and the tokens are scoped (`contents: write` + a dedicated tap PAT), not org-admin.
**Verified root cause** (this is CI/config, not Go source — the render/detect boundary code is unrelated):
- `.github/workflows/release.yml:9-10` — workflow-level `permissions: contents: write`; the `goreleaser` job has **no** job-level `permissions:` override, so the write scope is the job's effective scope.
- `.github/workflows/release.yml:18,25,31` — `actions/checkout@v6`, `actions/setup-go@v6`, `goreleaser/goreleaser-action@v7` (mutable major tags).
- `.github/workflows/release.yml:33` — `version: latest` pulls the newest GoReleaser binary at run time.
- `.github/workflows/release.yml:37,39` — `GITHUB_TOKEN` and `HOMEBREW_TAP_TOKEN` both injected into that same step.
- `.github/workflows/ci.yml:25,28,46,61,82,94,105` — mutable tags (`checkout@v6`, `setup-go@v6`, `golangci-lint-action@v9`, `upload-artifact@v7`) plus `version: latest` for golangci-lint at `ci.yml:51`.
- `.goreleaser.yaml:62-70` — `brews:` block pushes the formula to `KofTwentyTwo/homebrew-tap` using `{{ .Env.HOMEBREW_TAP_TOKEN }}` (line 70).
**Suggested fix**:
- Pin all `uses:` to a commit SHA with the version in a trailing comment (e.g. `actions/checkout@<sha> # v6.x`), and let Dependabot (`github_actions` ecosystem) bump the SHAs — this preserves the current "dependabot bumps the tag" flow (cf. `ce4edf7`) while making each ref immutable.
- Pin GoReleaser: replace `version: latest` at `release.yml:33` with a concrete tag (e.g. `version: "~> v2"` or an exact `vX.Y.Z`). Mirror the already-good precedent in this repo — `ci.yml:72` pins go-test-coverage to `@v2.18.8`. Apply the same discipline to `golangci-lint`'s `version: latest` at `ci.yml:51`.
- Scope the token blast radius: add a job-level `permissions: { contents: write }` block on the `goreleaser` job so the grant is explicit and local rather than inherited workflow-wide; the tap PAT is already a separate secret (good).
- Document a pin-refresh cadence in `RELEASING.md`.
- **No precedent transfer / no boundary work**: the v0.2.1 universal-shebang fix and its two-boundary rule (`internal/render/render.go` INSERT path + `internal/detect/detect.go PreserveBoundary`) plus the filetype table are about header placement and are **not** relevant here. This change is YAML-only.
- **100% gate**: unaffected — no `./internal/...` or `./cmd/...` Go source changes, so `.testcoverage.yml` (total: 100) stays green with no new tests.
**Corrections to the issue**:
- The body lumps `ci.yml:72` into "mutable action version tags," but that line is the go-test-coverage gate invoked as `go run github.com/vladopajic/go-test-coverage/v2@v2.18.8` — it is **already pinned** to an exact version, not mutable. The genuinely mutable CI refs are the `uses:` major tags and `golangci-lint-action`'s `version: latest` (`ci.yml:51`), which the body's range (25-72) covers but does not call out.
- Minor: `release.yml:30-33` is the GoReleaser step block; the actual `version: latest` line is `:33`.
**Open questions** (maintainer must decide before implementing):
- Pin style: exact SHAs (strongest, Dependabot-maintained) vs. exact semver tags (readable, weaker)? Repo currently relies on Dependabot tag bumps — moving to SHAs changes that workflow.
- GoReleaser version policy: hard-pin an exact `vX.Y.Z` vs. a `~> v2` constraint (floating within a major)?
- Should the job-level `permissions` tightening be in scope for this issue or tracked separately?
**Overlaps**: none identified among the issues reviewed. This is the only CI/supply-chain hardening item; it does not depend on the render/detect boundary issues.
_Automated maintainer review._
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Finding
The release workflow runs with publish-capable tokens while using mutable action tags and the latest GoReleaser binary.
Evidence
.github/workflows/release.yml:9to.github/workflows/release.yml:10grants write access to repository contents..github/workflows/release.yml:17to.github/workflows/release.yml:39uses version tags for actions and passes both release tokens to GoReleaser..github/workflows/release.yml:30to.github/workflows/release.yml:33requests the latest GoReleaser action tooling..github/workflows/ci.yml:25to.github/workflows/ci.yml:72also uses mutable action version tags in CI..goreleaser.yaml:62to.goreleaser.yaml:70publishes the Homebrew formula with the tap token.Reproduction
Inspect the release workflow. Any unexpected upstream change in a referenced mutable action or the latest GoReleaser binary executes in a job that can publish GitHub release artifacts and update the Homebrew tap.
Why It Matters
The release job is the software supply-chain boundary for distributed binaries and the Homebrew formula. Mutable tooling in that token context increases the blast radius of compromised or unexpectedly changed upstream release tooling.
Suggested Fix
Pin actions and GoReleaser to immutable versions or SHAs, keep permissions minimal per job, and document a regular pin-refresh process.
Status: confirmed — every file:line citation in the body checks out against the tree at
v0.2.1(HEADf4a111f); the release job runs publish-capable tokens alongside mutable action tags andversion: latestGoReleaser.Severity: medium (label is
bug/github_actions, no severity label). This is a real supply-chain hardening gap, but it is latent: it requires a compromised/mutated upstream action or GoReleaser build to bite. No defect in shipped behavior. Medium is appropriate — not high, since there's no evidence of an active vector and the tokens are scoped (contents: write+ a dedicated tap PAT), not org-admin.Verified root cause (this is CI/config, not Go source — the render/detect boundary code is unrelated):
.github/workflows/release.yml:9-10— workflow-levelpermissions: contents: write; thegoreleaserjob has no job-levelpermissions:override, so the write scope is the job's effective scope..github/workflows/release.yml:18,25,31—actions/checkout@v6,actions/setup-go@v6,goreleaser/goreleaser-action@v7(mutable major tags)..github/workflows/release.yml:33—version: latestpulls the newest GoReleaser binary at run time..github/workflows/release.yml:37,39—GITHUB_TOKENandHOMEBREW_TAP_TOKENboth injected into that same step..github/workflows/ci.yml:25,28,46,61,82,94,105— mutable tags (checkout@v6,setup-go@v6,golangci-lint-action@v9,upload-artifact@v7) plusversion: latestfor golangci-lint atci.yml:51..goreleaser.yaml:62-70—brews:block pushes the formula toKofTwentyTwo/homebrew-tapusing{{ .Env.HOMEBREW_TAP_TOKEN }}(line 70).Suggested fix:
uses:to a commit SHA with the version in a trailing comment (e.g.actions/checkout@<sha> # v6.x), and let Dependabot (github_actionsecosystem) bump the SHAs — this preserves the current "dependabot bumps the tag" flow (cf.ce4edf7) while making each ref immutable.version: latestatrelease.yml:33with a concrete tag (e.g.version: "~> v2"or an exactvX.Y.Z). Mirror the already-good precedent in this repo —ci.yml:72pins go-test-coverage to@v2.18.8. Apply the same discipline togolangci-lint'sversion: latestatci.yml:51.permissions: { contents: write }block on thegoreleaserjob so the grant is explicit and local rather than inherited workflow-wide; the tap PAT is already a separate secret (good).RELEASING.md.internal/render/render.goINSERT path +internal/detect/detect.go PreserveBoundary) plus the filetype table are about header placement and are not relevant here. This change is YAML-only../internal/...or./cmd/...Go source changes, so.testcoverage.yml(total: 100) stays green with no new tests.Corrections to the issue:
ci.yml:72into "mutable action version tags," but that line is the go-test-coverage gate invoked asgo run github.com/vladopajic/go-test-coverage/v2@v2.18.8— it is already pinned to an exact version, not mutable. The genuinely mutable CI refs are theuses:major tags andgolangci-lint-action'sversion: latest(ci.yml:51), which the body's range (25-72) covers but does not call out.release.yml:30-33is the GoReleaser step block; the actualversion: latestline is:33.Open questions (maintainer must decide before implementing):
vX.Y.Zvs. a~> v2constraint (floating within a major)?permissionstightening be in scope for this issue or tracked separately?Overlaps: none identified among the issues reviewed. This is the only CI/supply-chain hardening item; it does not depend on the render/detect boundary issues.
Automated maintainer review.
Resolved on
developin commit742c124e6ec2afa9312075db3112bc2def3c91ff.Validation:
Closing as fixed.