diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4232c1a..ebcc021 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: with: go-version-file: go.mod cache-dependency-path: go.sum - - uses: goreleaser/goreleaser-action@v7 + - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 with: distribution: goreleaser version: '~> v2' diff --git a/docs/superpowers/plans/2026-07-20-pin-goreleaser-action.md b/docs/superpowers/plans/2026-07-20-pin-goreleaser-action.md new file mode 100644 index 0000000..5375aa8 --- /dev/null +++ b/docs/superpowers/plans/2026-07-20-pin-goreleaser-action.md @@ -0,0 +1,325 @@ +# Pin GoReleaser Action Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Close CodeQL alert 1 by replacing the mutable GoReleaser Action `v7` reference with the exact full commit SHA currently selected by that tag. + +**Architecture:** Keep the release workflow behavior unchanged and modify only the GoReleaser Action reference. Preserve an inline `# v7` comment so the pinned version stays readable and Dependabot can maintain the version documentation. + +**Tech Stack:** GitHub Actions YAML, CodeQL default setup, Dependabot, Ruby YAML parser, Go toolchain, GitHub CLI + +--- + +### Task 1: Prove the Existing Reference Violates the Invariant + +**Files:** +- Inspect: `.github/workflows/release.yml:22` +- Reference: `docs/superpowers/specs/2026-07-20-pin-goreleaser-action-design.md` + +- [ ] **Step 1: Confirm the open alert and upstream tag target** + +Run: + +```bash +gh api repos/hmmhmmhm/tmux-attach-browser/code-scanning/alerts \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + -f state=open \ + --method GET \ + --jq '.[] | select(.number == 1) | [.rule.id, .rule.security_severity_level, .most_recent_instance.location.path, .most_recent_instance.location.start_line] | join("|")' + +gh api repos/goreleaser/goreleaser-action/git/ref/tags/v7 \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + --jq '[.object.type, .object.sha] | join("|")' +``` + +Expected: + +```text +actions/unpinned-tag|medium|.github/workflows/release.yml|22 +commit|f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 +``` + +- [ ] **Step 2: Run the full-SHA assertion and verify that it fails** + +Run: + +```bash +/usr/bin/ruby -e ' +line = File.readlines(".github/workflows/release.yml").find { |candidate| candidate.include?("goreleaser/goreleaser-action@") } +abort "GoReleaser Action reference missing" unless line +ref = line[/goreleaser\/goreleaser-action@(\S+)/, 1] +abort "expected full SHA, got #{ref}" unless ref&.match?(/\A[0-9a-f]{40}\z/) +' +``` + +Expected: exit 1 with `expected full SHA, got v7`. + +### Task 2: Pin the GoReleaser Action + +**Files:** +- Modify: `.github/workflows/release.yml:22` + +- [ ] **Step 1: Replace the mutable tag with the verified full SHA** + +Apply this exact change: + +```diff +- - uses: goreleaser/goreleaser-action@v7 ++ - uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 +``` + +- [ ] **Step 2: Run the security invariant and exact-value assertions** + +Run: + +```bash +/usr/bin/ruby -e ' +line = File.readlines(".github/workflows/release.yml").find { |candidate| candidate.include?("goreleaser/goreleaser-action@") } +abort "GoReleaser Action reference missing" unless line +ref = line[/goreleaser\/goreleaser-action@(\S+)/, 1] +abort "expected full SHA, got #{ref}" unless ref&.match?(/\A[0-9a-f]{40}\z/) +abort "unexpected SHA #{ref}" unless ref == "f06c13b6b1a9625abc9e6e439d9c05a8f2190e94" +abort "missing version comment" unless line.match?(/# v7\s*\z/) +puts "action_pin=ok" +' +``` + +Expected: `action_pin=ok`. + +- [ ] **Step 3: Verify the upstream ref has not moved** + +Run: + +```bash +test "$(gh api repos/goreleaser/goreleaser-action/git/ref/tags/v7 \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + --jq '.object.type + " " + .object.sha')" \ + = 'commit f06c13b6b1a9625abc9e6e439d9c05a8f2190e94' +``` + +Expected: exit 0. If it fails, stop and resolve the current official `v7` ref before continuing. + +- [ ] **Step 4: Verify workflow syntax and repository health** + +Run: + +```bash +/usr/bin/ruby -e 'require "yaml"; YAML.load_file(".github/workflows/release.yml"); puts "yaml=ok"' +git diff --check +go test ./... +go vet ./... +``` + +Expected: YAML prints `yaml=ok`; diff check, all Go tests, and vet exit 0. + +- [ ] **Step 5: Verify the implementation diff is exactly one workflow line** + +Run: + +```bash +git diff -- .github/workflows/release.yml +git diff --name-only +``` + +Expected: the workflow diff contains one removed line and one added line; the only uncommitted file is `.github/workflows/release.yml`. + +- [ ] **Step 6: Commit the implementation** + +Run: + +```bash +git add .github/workflows/release.yml +git commit -m "ci: pin GoReleaser action" +``` + +Expected: one commit containing only `.github/workflows/release.yml`. + +### Task 3: Deliver and Review the Pull Request + +**Files:** +- Include: `.github/workflows/release.yml` +- Include: `docs/superpowers/specs/2026-07-20-pin-goreleaser-action-design.md` +- Include: `docs/superpowers/plans/2026-07-20-pin-goreleaser-action.md` + +- [ ] **Step 1: Verify the complete branch before pushing** + +Run: + +```bash +git diff --check origin/main...HEAD +go test ./... +go vet ./... +git status --short --branch +git diff --name-only origin/main...HEAD +``` + +Expected: all verification commands exit 0, the worktree is clean, and the branch contains exactly the workflow, spec, and plan files listed above. + +- [ ] **Step 2: Push and create a draft pull request** + +Run: + +```bash +git push -u origin fix/pin-goreleaser-action + +gh pr create \ + --repo hmmhmmhm/tmux-attach-browser \ + --base main \ + --head fix/pin-goreleaser-action \ + --draft \ + --title "ci: pin GoReleaser action" \ + --body "## Summary + +- pin the third-party GoReleaser Action to the full SHA currently selected by v7 +- retain the v7 comment for readability and Dependabot updates +- document and verify closure criteria for CodeQL alert 1 + +## Verification + +- Ruby workflow YAML parse +- full-SHA and upstream-ref assertions +- git diff --check +- go test ./... +- go vet ./..." +``` + +Expected: a new draft pull request URL. + +- [ ] **Step 3: Wait for all pull request checks** + +Run: + +```bash +gh pr checks --repo hmmhmmhm/tmux-attach-browser --watch --interval 10 +``` + +Expected: CI, Dependency Review, and CodeQL checks finish successfully. + +- [ ] **Step 4: Request independent review** + +Use `superpowers:requesting-code-review` against the range `origin/main...HEAD`. + +Expected: no Critical or Important findings remain. Resolve any such finding before continuing. + +### Task 4: Merge, Verify Alert Closure, and Clean Up + +**Files:** +- Update outside repository: `/Users/hm/Documents/personal-agent/projects/tmux-attach-browser/PROJECT.md` + +- [ ] **Step 1: Mark the pull request ready and squash merge it** + +Run: + +```bash +pr_number=$(gh pr view \ + --repo hmmhmmhm/tmux-attach-browser \ + fix/pin-goreleaser-action \ + --json number \ + --jq '.number') + +gh pr ready "$pr_number" --repo hmmhmmhm/tmux-attach-browser +gh pr merge "$pr_number" \ + --repo hmmhmmhm/tmux-attach-browser \ + --squash \ + --subject "ci: pin GoReleaser action" \ + --body "" +``` + +Expected: pull request state is `MERGED`. + +- [ ] **Step 2: Fast-forward the primary worktree and rerun local verification** + +Run from `/Users/hm/Documents/personal-agent/workspaces/tmux-attach-browser`: + +```bash +git fetch origin main +git merge --ff-only origin/main +/usr/bin/ruby -e 'require "yaml"; YAML.load_file(".github/workflows/release.yml"); puts "yaml=ok"' +go test ./... +go vet ./... +``` + +Expected: `main` fast-forwards and every verification command exits 0. + +- [ ] **Step 3: Wait for successful CodeQL analysis on `main`** + +Run: + +```bash +pr_number=$(gh pr view \ + --repo hmmhmmhm/tmux-attach-browser \ + fix/pin-goreleaser-action \ + --json number \ + --jq '.number') +merge_sha=$(gh pr view "$pr_number" \ + --repo hmmhmmhm/tmux-attach-browser \ + --json mergeCommit \ + --jq '.mergeCommit.oid') + +codeql_run_id='' +for attempt in {1..24}; do + codeql_run_id=$(gh run list \ + --repo hmmhmmhm/tmux-attach-browser \ + --workflow CodeQL \ + --branch main \ + --limit 10 \ + --json databaseId,headSha \ + --jq ".[] | select(.headSha == \"$merge_sha\") | .databaseId" | head -n 1) + test -n "$codeql_run_id" && break + sleep 5 +done +test -n "$codeql_run_id" + +gh run watch "$codeql_run_id" \ + --repo hmmhmmhm/tmux-attach-browser \ + --interval 10 \ + --exit-status +``` + +Expected: the latest post-merge CodeQL run completes successfully. + +- [ ] **Step 4: Prove CodeQL alert 1 is no longer open** + +Run: + +```bash +test "$(gh api repos/hmmhmmhm/tmux-attach-browser/code-scanning/alerts \ + -H 'X-GitHub-Api-Version: 2026-03-10' \ + -f state=open \ + --method GET \ + --jq 'map(select(.number == 1)) | length')" -eq 0 +``` + +Expected: exit 0. + +- [ ] **Step 5: Update the project action log and security fact** + +Use `apply_patch` to add the merge commit, alert closure, and verification result to the top of the Facts and recent action log in `/Users/hm/Documents/personal-agent/projects/tmux-attach-browser/PROJECT.md`. + +Expected: the new entry follows the existing `YYYY-MM-DD HH:MM KST · summary · link` format and contains no U+2014 character. + +- [ ] **Step 6: Remove the worktree and feature branches** + +Run from `/Users/hm/Documents/personal-agent/workspaces/tmux-attach-browser` after confirming both worktrees are clean: + +```bash +git worktree remove /Users/hm/Documents/personal-agent/workspaces/tmux-attach-browser/.worktrees/fix/pin-goreleaser-action +git push origin --delete fix/pin-goreleaser-action +git branch -D fix/pin-goreleaser-action +``` + +Expected: the implementation worktree and both feature branch refs are removed. + +- [ ] **Step 7: Perform final completion audit** + +Run: + +```bash +test "$(git for-each-ref --format='%(refname:short)' refs/heads | sort)" = 'main' +test "$(git ls-remote --heads origin | awk '{sub("refs/heads/", "", $2); print $2}' | sort)" = 'main' +test "$(git worktree list --porcelain | awk '/^worktree /{count++} END{print count+0}')" -eq 1 +test "$(git tag --list | sort)" = 'v0.1.0' +test -z "$(git status --porcelain)" +``` + +Expected: only `main` remains locally and remotely, one worktree remains, `v0.1.0` is preserved, and the primary worktree is clean. diff --git a/docs/superpowers/specs/2026-07-20-pin-goreleaser-action-design.md b/docs/superpowers/specs/2026-07-20-pin-goreleaser-action-design.md new file mode 100644 index 0000000..4fecd48 --- /dev/null +++ b/docs/superpowers/specs/2026-07-20-pin-goreleaser-action-design.md @@ -0,0 +1,77 @@ +# Pin GoReleaser Action Design + +## Context + +CodeQL default setup reports one open medium-severity alert on +`.github/workflows/release.yml`. The release job references +`goreleaser/goreleaser-action@v7`, and the `v7` tag is mutable. + +The alert is: + +- Rule: `actions/unpinned-tag` +- Alert: +- Location: `.github/workflows/release.yml:22` + +The official `goreleaser/goreleaser-action` repository currently resolves the +`v7` tag to the verified commit +`f06c13b6b1a9625abc9e6e439d9c05a8f2190e94`. + +## Decision + +Replace the moving `v7` reference with its full 40-character commit SHA and +retain `# v7` on the same line: + +```yaml +- uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 +``` + +The full SHA makes the executed third-party code immutable. The inline version +comment keeps the workflow readable and lets Dependabot update the documented +version when it updates the pinned SHA. + +## Scope + +The implementation changes only the GoReleaser `uses` line in +`.github/workflows/release.yml`. + +It does not change: + +- `actions/checkout` or `actions/setup-go` +- workflow triggers or permissions +- GoReleaser distribution, version constraint, or arguments +- release artifacts, archive names, or checksums +- Go source code, installers, documentation, tags, or existing releases + +## Verification + +Verification will prove both the security property and the absence of unrelated +behavior changes: + +1. A pre-change assertion must fail because the GoReleaser reference is not a + full SHA. +2. A post-change assertion must require the exact 40-character SHA and the + `# v7` comment. +3. The current upstream `v7` ref must still resolve to the pinned SHA before the + branch is pushed. +4. The workflow YAML must parse successfully. +5. `git diff --check`, `go test ./...`, and `go vet ./...` must pass. +6. Pull request CI, Dependency Review, and CodeQL checks must pass. +7. After squash merge, CodeQL must analyze `main` successfully and alert 1 must + no longer be open. + +## Delivery and Cleanup + +Work will be delivered through a draft pull request. After independent review +and all checks pass, the pull request will be marked ready and squash merged. +The local `main` branch will then be fast-forwarded, the implementation +worktree will be removed, and the local and remote feature branches will be +deleted. The `v0.1.0` tag and existing release remain untouched. + +## Failure and Rollback + +If upstream moves `v7` before the branch is pushed, the implementation will +stop and re-resolve the official ref instead of pinning stale evidence. If CI or +CodeQL fails, the pull request will not be merged until the root cause is +understood. The functional change is one line and can be rolled back by +restoring `goreleaser/goreleaser-action@v7`, although that rollback would reopen +the security alert.