From f6513447aec72bf2f0301166709542f62cbb3b92 Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 14:07:52 -0500 Subject: [PATCH 1/8] docs: add PR artifacts v2 design plan Design for replacing manual go build matrix with GoReleaser snapshot builds to produce universal macOS binaries, add PR comments with artifact links, and chain builds after CI with concurrency. Co-Authored-By: Claude Opus 4.6 --- .../2026-02-27-pr-artifacts-v2.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 docs/design-plans/2026-02-27-pr-artifacts-v2.md diff --git a/docs/design-plans/2026-02-27-pr-artifacts-v2.md b/docs/design-plans/2026-02-27-pr-artifacts-v2.md new file mode 100644 index 0000000..9c38668 --- /dev/null +++ b/docs/design-plans/2026-02-27-pr-artifacts-v2.md @@ -0,0 +1,160 @@ +# PR Artifacts v2 Design + +## Summary + +Replace the manual `go build` matrix in the PR artifacts workflow with GoReleaser snapshot builds to produce universal macOS binaries matching the release pipeline, add a PR comment with artifact download links that updates on each push, and chain builds after CI passes with cancel-in-progress concurrency. + +## Definition of Done + +The PR artifacts workflow is replaced so that: + +1. **GoReleaser snapshot builds** produce the same artifact set as releases — including a universal macOS binary — by running `goreleaser release --snapshot --clean` with the existing `.goreleaser.yml` +2. **Snapshot versioning** uses the existing `version.txt` + commit-count scheme, producing tags like `v0.2.5-pr.42+abc1234` +3. **Build depends on CI** — snapshot builds only run after lint+test pass +4. **PR comment** is created (or updated on subsequent pushes) with the build version and a link to download artifacts +5. **Concurrency** — a cancel-in-progress group ensures only the latest push builds + +**Out of scope:** Changing action version pinning (tags → SHAs), adding change detection (paths-filter), or modifying the release workflow. + +## Acceptance Criteria + +### DoD 1: GoReleaser snapshot builds + +- **pr-artifacts-v2.AC1.1** (success): When a PR is opened against main and CI passes, the `build-snapshot` job runs `goreleaser release --snapshot --clean` and produces archives for linux/amd64, linux/arm64, darwin/universal, and windows/amd64 +- **pr-artifacts-v2.AC1.2** (success): The macOS artifact is a single universal binary (not separate amd64 + arm64), named `cpm_*_darwin_universal.tar.gz` +- **pr-artifacts-v2.AC1.3** (success): All archives are uploaded as GitHub Actions artifacts with 7-day retention +- **pr-artifacts-v2.AC1.4** (success): A checksums.txt file is uploaded alongside the archives +- **pr-artifacts-v2.AC1.5** (failure): If GoReleaser build fails, the job fails and no artifacts are uploaded + +### DoD 2: Snapshot versioning + +- **pr-artifacts-v2.AC2.1** (success): The snapshot version follows the pattern `{base}.{count}-pr.{pr_number}+{short_sha}` (e.g., `0.2.5-pr.42+abc1234`), derived from `version.txt` + commit count since last version.txt change +- **pr-artifacts-v2.AC2.2** (success): `GORELEASER_CURRENT_TAG` is set to the computed base version tag (e.g., `v0.2.5`) so GoReleaser resolves `{{ .Version }}` correctly +- **pr-artifacts-v2.AC2.3** (success): The `PR_NUMBER` environment variable is passed to GoReleaser so the snapshot template can interpolate it + +### DoD 3: Build depends on CI + +- **pr-artifacts-v2.AC3.1** (success): The `build-snapshot` job has `needs: [lint, test]` and only runs after both pass +- **pr-artifacts-v2.AC3.2** (success): The lint and test jobs are defined within `pr-artifacts.yml`, matching the existing `ci.yml` configuration +- **pr-artifacts-v2.AC3.3** (success): `ci.yml` is scoped to `push` only (the `pull_request` trigger is removed) since PR CI is now handled by `pr-artifacts.yml` +- **pr-artifacts-v2.AC3.4** (failure): If lint or test fails, the build-snapshot job is skipped entirely + +### DoD 4: PR comment + +- **pr-artifacts-v2.AC4.1** (success): On first successful build, a new comment is posted to the PR with the build version, platform list, and artifact download link +- **pr-artifacts-v2.AC4.2** (success): On subsequent pushes to the same PR, the existing comment is found (by `body-includes` marker) and updated in-place rather than creating a new comment +- **pr-artifacts-v2.AC4.3** (success): The comment includes a link to `https://github.com/{repo}/actions/runs/{run_id}` for artifact download +- **pr-artifacts-v2.AC4.4** (failure): If comment creation/update fails, the build still succeeds (comment is non-blocking) + +### DoD 5: Concurrency + +- **pr-artifacts-v2.AC5.1** (success): A concurrency group keyed on `workflow-pr_number` ensures only one build runs per PR at a time +- **pr-artifacts-v2.AC5.2** (success): `cancel-in-progress: true` cancels the running build when a new push arrives + +## Architecture + +### Workflow Structure + +The redesigned PR pipeline consolidates CI and artifact building into a single workflow file with three chained jobs: + +``` +pr-artifacts.yml +├── lint (runs golangci-lint via mise) +├── test (runs go test with race detector) +└── build-snapshot (needs: [lint, test]) + ├── Calculate version from version.txt + ├── GoReleaser snapshot build + ├── Upload artifacts (5 uploads: linux-amd64, linux-arm64, macos-universal, windows-amd64, checksums) + └── Create/update PR comment +``` + +### GoReleaser Snapshot Configuration + +A `snapshot` section is added to `.goreleaser.yml`: + +```yaml +snapshot: + version_template: "{{ .Version }}-pr.{{ .Env.PR_NUMBER }}+{{ .ShortCommit }}" +``` + +This template is only used when `--snapshot` is passed. The existing release builds are unaffected. GoReleaser computes `{{ .Version }}` from `GORELEASER_CURRENT_TAG` and `{{ .ShortCommit }}` from the git HEAD. + +### Version Computation + +The same shell logic from `auto-release.yml` is reused: + +```bash +BASE_VERSION=$(cat version.txt | tr -d '\n') # e.g., "0.2" +LAST_VERSION_COMMIT=$(git log -1 --format=%H version.txt) +COMMIT_COUNT=$(git rev-list --count ${LAST_VERSION_COMMIT}..HEAD) +TAG="v${BASE_VERSION}.${COMMIT_COUNT}" # e.g., "v0.2.5" +``` + +`GORELEASER_CURRENT_TAG` is set to `$TAG`, and the snapshot template appends the PR context. + +### PR Comment Strategy + +Uses two community actions in sequence: +1. `peter-evans/find-comment` — searches for an existing comment containing a known marker string (`## PR Build`) +2. `peter-evans/create-or-update-comment` — creates a new comment if none found, or updates the existing one via `edit-mode: replace` + +The marker string in the comment body acts as an identifier — only comments from this workflow contain it. + +## Existing Patterns + +| Pattern | Where | How We Follow It | +|---------|-------|-----------------| +| `version.txt` + commit count | `auto-release.yml` | Reuse the same version computation shell script | +| GoReleaser for builds | `release.yml` + `.goreleaser.yml` | Use GoReleaser snapshot mode with existing config | +| Tag-based action versions | All workflow files | Keep `@v4`/`@v6` style, don't switch to SHA pinning | +| mise for tool setup | `ci.yml`, current `pr-artifacts.yml` | Continue using `jdx/mise-action@v2` | +| Actions cache for Go modules | All workflows | Same cache key pattern | +| PR comment pattern | unquote `tui-pr.yml` | Adapt `peter-evans/find-comment` + `create-or-update-comment` | + +## Implementation Phases + +### Phase 1: GoReleaser snapshot config + +Add the `snapshot` section to `.goreleaser.yml`. This is a safe, isolated change that has zero effect on release builds (the template is only used with `--snapshot`). + +**Files:** `.goreleaser.yml` + +### Phase 2: Rewrite PR artifacts workflow + +Replace the contents of `.github/workflows/pr-artifacts.yml` with the new lint → test → build-snapshot pipeline. This includes: +- Concurrency group +- Lint and test jobs (mirroring current `ci.yml`) +- GoReleaser snapshot build with version computation +- Artifact uploads (linux-amd64, linux-arm64, macos-universal, windows-amd64, checksums) +- PR comment with find + create-or-update pattern + +**Files:** `.github/workflows/pr-artifacts.yml` + +### Phase 3: Scope ci.yml to push-only + +Remove the `pull_request` trigger from `ci.yml` so PR CI is handled exclusively by `pr-artifacts.yml`. This avoids redundant CI runs on PRs. + +**Files:** `.github/workflows/ci.yml` + +## Additional Considerations + +### GoReleaser `before.hooks` + +The existing `.goreleaser.yml` runs `go mod tidy` and `go test ./...` as pre-build hooks. In snapshot mode, `go test` will run again after the lint+test jobs already passed. This is redundant but harmless. Removing the test hook would speed up PR builds but also affects releases, which is out of scope for this design. + +### Archive naming with `+` in version + +The snapshot version contains a `+` character (semver build metadata separator). GoReleaser handles this correctly in archive filenames. The upload-artifact glob patterns (`dist/cpm_*_linux_amd64.tar.gz`) match regardless of the version string. + +### Permissions + +The workflow needs `contents: read` (checkout) and `pull-requests: write` (comment). These are the same permissions the current `pr-artifacts.yml` already declares. + +## Glossary + +| Term | Definition | +|------|-----------| +| **GoReleaser snapshot** | A GoReleaser build mode (`--snapshot`) that produces all artifacts without creating a GitHub release or pushing to Homebrew. Used for pre-release/preview builds. | +| **Universal binary** | A macOS Mach-O binary containing code for multiple architectures (amd64 + arm64). Users don't need to choose the right download for their Mac. | +| **`GORELEASER_CURRENT_TAG`** | Environment variable that tells GoReleaser what version tag to use when no actual git tag exists (e.g., in snapshot builds). | +| **Concurrency group** | GitHub Actions feature that groups workflow runs and optionally cancels older runs when a new one starts, keyed by a user-defined string. | From 3dcd678452a3c4ed3cab5965a65bc9051461b702 Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 14:43:31 -0500 Subject: [PATCH 2/8] build: add GoReleaser snapshot version template for PR builds --- .goreleaser.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.goreleaser.yml b/.goreleaser.yml index bcea992..fd2f936 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -101,3 +101,6 @@ release: name: cpm draft: false prerelease: auto + +snapshot: + version_template: "{{ .Version }}-pr.{{ .Env.PR_NUMBER }}+{{ .ShortCommit }}" From 3fe0165e2245bff188a459e7be8bd50c85a4a027 Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 14:46:34 -0500 Subject: [PATCH 3/8] ci: rewrite PR artifacts with GoReleaser snapshot builds Replace manual go build matrix with GoReleaser snapshot pipeline. Adds lint/test gates, per-platform artifact uploads, and PR comment with download links. Uses cancel-in-progress concurrency. --- .github/workflows/pr-artifacts.yml | 201 +++++++++++++++++++++-------- 1 file changed, 148 insertions(+), 53 deletions(-) diff --git a/.github/workflows/pr-artifacts.yml b/.github/workflows/pr-artifacts.yml index 2ece071..d4db175 100644 --- a/.github/workflows/pr-artifacts.yml +++ b/.github/workflows/pr-artifacts.yml @@ -4,30 +4,77 @@ on: pull_request: branches: [main] +concurrency: + group: pr-artifacts-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: contents: read pull-requests: write jobs: - build-artifacts: - name: Build ${{ matrix.goos }}-${{ matrix.goarch }} + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up mise + uses: jdx/mise-action@v2 + with: + install: true + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-${{ runner.os }}- + + - name: Run golangci-lint + run: mise run lint + + test: + name: Test runs-on: ubuntu-latest - strategy: - matrix: - include: - - goos: linux - goarch: amd64 - - goos: linux - goarch: arm64 - - goos: darwin - goarch: amd64 - - goos: darwin - goarch: arm64 - - goos: windows - goarch: amd64 + steps: + - uses: actions/checkout@v4 + + - name: Set up mise + uses: jdx/mise-action@v2 + with: + install: true + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + go-${{ runner.os }}- + + - name: Run tests + run: go test -v -race -coverprofile=coverage.out ./... + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + file: ./coverage.out + fail_ci_if_error: false + build-snapshot: + name: Build Snapshot + needs: [lint, test] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up mise uses: jdx/mise-action@v2 @@ -44,48 +91,96 @@ jobs: restore-keys: | go-${{ runner.os }}- - - name: Download dependencies for all platforms + - name: Compute snapshot version + id: version run: | - GOOS=linux GOARCH=amd64 go mod download - GOOS=linux GOARCH=arm64 go mod download - GOOS=darwin GOARCH=amd64 go mod download - GOOS=darwin GOARCH=arm64 go mod download - GOOS=windows GOARCH=amd64 go mod download + BASE_VERSION=$(cat version.txt | tr -d '\n') + LAST_VERSION_COMMIT=$(git log -1 --format=%H version.txt) + COMMIT_COUNT=$(git rev-list --count ${LAST_VERSION_COMMIT}..HEAD) + TAG="v${BASE_VERSION}.${COMMIT_COUNT}" + SHORT_SHA=$(git rev-parse --short HEAD) + SNAPSHOT="${BASE_VERSION}.${COMMIT_COUNT}-pr.${{ github.event.pull_request.number }}+${SHORT_SHA}" + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "snapshot=$SNAPSHOT" >> $GITHUB_OUTPUT + echo "Snapshot base tag: $TAG, full version: $SNAPSHOT" - - name: Build + - name: Run GoReleaser snapshot + uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --snapshot --clean env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: "0" + GORELEASER_CURRENT_TAG: ${{ steps.version.outputs.tag }} PR_NUMBER: ${{ github.event.pull_request.number }} - COMMIT_SHA: ${{ github.sha }} - BRANCH_NAME: ${{ github.head_ref }} - run: | - BINARY_NAME="cpm" - if [ "$GOOS" = "windows" ]; then - BINARY_NAME="cpm.exe" - fi - go build -ldflags="-s -w \ - -X github.com/open-cli-collective/cpm/internal/version.Version=pr-${PR_NUMBER} \ - -X github.com/open-cli-collective/cpm/internal/version.Commit=${COMMIT_SHA} \ - -X github.com/open-cli-collective/cpm/internal/version.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ - -X github.com/open-cli-collective/cpm/internal/version.Branch=${BRANCH_NAME}" \ - -o ${BINARY_NAME} ./cmd/cpm - - - name: Create archive - run: | - ARCHIVE_NAME="cpm_pr-${{ github.event.pull_request.number }}_${{ matrix.goos }}_${{ matrix.goarch }}" - if [ "${{ matrix.goos }}" = "windows" ]; then - zip "${ARCHIVE_NAME}.zip" cpm.exe README.md - else - tar -czvf "${ARCHIVE_NAME}.tar.gz" cpm README.md - fi - - - name: Upload artifact + + - name: Upload linux-amd64 uses: actions/upload-artifact@v4 with: - name: cpm_${{ matrix.goos }}_${{ matrix.goarch }} - path: | - *.tar.gz - *.zip + name: cpm-linux-amd64 + path: dist/cpm_*_linux_amd64.tar.gz + retention-days: 7 + if-no-files-found: error + + - name: Upload linux-arm64 + uses: actions/upload-artifact@v4 + with: + name: cpm-linux-arm64 + path: dist/cpm_*_linux_arm64.tar.gz + retention-days: 7 + if-no-files-found: error + + - name: Upload macos-universal + uses: actions/upload-artifact@v4 + with: + name: cpm-macos-universal + path: dist/cpm_*_darwin_universal.tar.gz + retention-days: 7 + if-no-files-found: error + + - name: Upload windows-amd64 + uses: actions/upload-artifact@v4 + with: + name: cpm-windows-amd64 + path: dist/cpm_*_windows_amd64.zip + retention-days: 7 + if-no-files-found: error + + - name: Upload checksums + uses: actions/upload-artifact@v4 + with: + name: checksums + path: dist/checksums.txt retention-days: 7 + if-no-files-found: error + + - name: Find existing PR comment + uses: peter-evans/find-comment@v4 + id: fc + continue-on-error: true + with: + issue-number: ${{ github.event.pull_request.number }} + comment-author: 'github-actions[bot]' + body-includes: '## PR Build' + + - name: Create or update PR comment + uses: peter-evans/create-or-update-comment@v5 + continue-on-error: true + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + edit-mode: replace + body: | + ## PR Build + + **Version:** `${{ steps.version.outputs.snapshot }}` + + | Platform | Archive | + |----------|---------| + | Linux (amd64) | `cpm-linux-amd64` | + | Linux (arm64) | `cpm-linux-arm64` | + | macOS (universal) | `cpm-macos-universal` | + | Windows (amd64) | `cpm-windows-amd64` | + + **[Download artifacts](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})** + + *Built from ${{ github.event.pull_request.head.sha }}* From eec2a1bbd3429a44e3664d6aaa539af920878eca Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 14:49:41 -0500 Subject: [PATCH 4/8] ci: scope ci.yml to push-only, PR CI handled by pr-artifacts --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b36d23..cb85b56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,6 @@ name: CI on: push: branches: [main] - pull_request: - branches: [main] permissions: contents: read From 772abf5fb7e79ed5e898dc84c3474182622a5b23 Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 14:53:17 -0500 Subject: [PATCH 5/8] docs: update CI workflow description for PR artifacts v2 The CI pipeline split so that ci.yml only runs on push to main, while pr-artifacts.yml handles PRs with lint, test, and GoReleaser snapshot builds. Updated CLAUDE.md to reflect this change. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e3b79e7..b08c7ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # cpm - Claude Plugin Manager -Last verified: 2026-02-14 +Last verified: 2026-02-27 A TUI application for managing Claude Code plugins with clear visibility into installation scopes. @@ -79,8 +79,8 @@ Example: `feat(tui): add plugin filtering with / key` ## CI & Release Workflow -1. **On every push/PR**: CI runs lint, test, build -2. **On PR**: Artifacts built for all platforms (downloadable for testing) +1. **On push to main**: CI runs lint, test, build +2. **On PR**: Lint, test, then GoReleaser snapshot builds for all platforms; posts a comment with download links 3. **On merge to main with `feat:` or `fix:` commit**: Auto-release creates tag and triggers release 4. **On tag push**: GoReleaser builds and publishes to GitHub Releases and Homebrew From b977307e71c35ab763055aa32c2364489590440a Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 14:59:13 -0500 Subject: [PATCH 6/8] docs: add test plan for PR artifacts v2 Co-Authored-By: Claude Opus 4.6 --- docs/test-plans/2026-02-27-pr-artifacts-v2.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 docs/test-plans/2026-02-27-pr-artifacts-v2.md diff --git a/docs/test-plans/2026-02-27-pr-artifacts-v2.md b/docs/test-plans/2026-02-27-pr-artifacts-v2.md new file mode 100644 index 0000000..835b7ec --- /dev/null +++ b/docs/test-plans/2026-02-27-pr-artifacts-v2.md @@ -0,0 +1,95 @@ +# Human Test Plan: PR Artifacts v2 + +**Implementation plan:** `docs/implementation-plans/2026-02-27-pr-artifacts-v2/` +**Generated:** 2026-02-27 + +## Prerequisites + +- Implementation branch pushed to `origin/brajkovic/pr-artifacts-v2` +- A PR opened against `main` +- Access to the GitHub Actions UI for the repository +- `gh` CLI authenticated with access to the repository + +## Phase 1: Workflow Trigger Verification (AC3.3) + +| Step | Action | Expected | +|------|--------|----------| +| 1.1 | Open the PR on GitHub. Navigate to the "Checks" tab. | A workflow named "PR Artifacts" appears. No workflow named "CI" appears. | +| 1.2 | Wait for "PR Artifacts" workflow to start. | The workflow run shows three jobs: "Lint", "Test", and "Build Snapshot". | +| 1.3 | After the PR is merged, push a separate commit to `main`. Navigate to the repository Actions tab and filter by "CI" workflow. | The "CI" workflow triggers on the push to `main`. It runs `lint`, `test`, and `build` jobs. | + +## Phase 2: Job Dependency Verification (AC3.1, AC3.2, AC3.4) + +| Step | Action | Expected | +|------|--------|----------| +| 2.1 | In the "PR Artifacts" workflow run, observe the job graph in the GitHub Actions UI. | "Build Snapshot" appears downstream of both "Lint" and "Test" with dependency arrows. It does not start until both predecessors complete. | +| 2.2 | Compare the `lint` job steps in `pr-artifacts.yml` against `ci.yml`. | Both use: `actions/checkout@v4`, `jdx/mise-action@v2`, `actions/cache@v4` (same cache key), and `mise run lint`. Steps are identical. | +| 2.3 | Compare the `test` job steps in `pr-artifacts.yml` against `ci.yml`. | Both use: `actions/checkout@v4`, `jdx/mise-action@v2`, `actions/cache@v4` (same cache key), `go test -v -race -coverprofile=coverage.out ./...`, and `codecov/codecov-action@v4`. Steps are identical. | +| 2.4 | (Optional) To test failure propagation: create a temporary commit with a lint error (e.g., add an unused import in a Go file), push to the PR branch. | "Lint" job fails. "Build Snapshot" shows status "Skipped" in the Actions UI. | +| 2.5 | (If 2.4 was done) Revert the lint failure commit and push. | "Lint" passes, "Test" passes, and "Build Snapshot" runs. | + +## Phase 3: Snapshot Build and Artifacts (AC1.1-AC1.5, AC2.1-AC2.3) + +| Step | Action | Expected | +|------|--------|----------| +| 3.1 | In the "Build Snapshot" job logs, expand the "Compute snapshot version" step. | The step outputs a `tag` value matching `v{base}.{count}` (e.g., `v0.2.5`) and a `snapshot` value matching `{base}.{count}-pr.{N}+{sha}` (e.g., `0.2.5-pr.42+abc1234`). | +| 3.2 | In the "Run GoReleaser snapshot" step logs, find where GoReleaser reports the version it is using. | GoReleaser reports the tag from step 3.1 as `GORELEASER_CURRENT_TAG`. The snapshot version in the output includes the correct PR number (confirming `PR_NUMBER` env var was interpolated). | +| 3.3 | In the GoReleaser output, find the list of build targets. | The output lists builds for: `linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`, and `windows/amd64`. Darwin builds are merged into a universal binary. | +| 3.4 | Navigate to the workflow run's "Artifacts" section (bottom of the run summary page). | Five artifacts are listed: `cpm-linux-amd64`, `cpm-linux-arm64`, `cpm-macos-universal`, `cpm-windows-amd64`, `checksums`. | +| 3.5 | Download the `cpm-macos-universal` artifact. Extract the archive inside. | The archive filename matches `cpm_*_darwin_universal.tar.gz`. It contains a single `cpm` binary (not separate amd64 and arm64 archives). | +| 3.6 | Download the `checksums` artifact. | It contains a `checksums.txt` file with SHA256 entries, one per archive file. | +| 3.7 | Verify retention via the GitHub API: run `gh api repos/{owner}/{repo}/actions/artifacts --jq '.artifacts[] \| select(.name \| startswith("cpm-")) \| {name, expires_at}'`. | Each artifact's `expires_at` is approximately 7 days from creation. | + +## Phase 4: PR Comment (AC4.1-AC4.4) + +| Step | Action | Expected | +|------|--------|----------| +| 4.1 | After the first successful "Build Snapshot" run, navigate to the PR conversation tab. | A comment from `github-actions[bot]` is present. | +| 4.2 | Read the comment content. | The comment contains: (1) a `## PR Build` header, (2) a `Version:` line showing the snapshot version from step 3.1, (3) a table listing four platforms (Linux amd64, Linux arm64, macOS universal, Windows amd64), (4) a "Download artifacts" link, (5) a line showing the commit SHA. | +| 4.3 | Click the "Download artifacts" link in the comment. | It navigates to the correct workflow run page. | +| 4.4 | Push a second commit to the PR branch. Wait for the new "Build Snapshot" run to complete. | The PR conversation still has exactly one `## PR Build` comment (not two). The version string and commit SHA in the comment are updated to reflect the new commit. | + +## Phase 5: Concurrency (AC5.1, AC5.2) + +| Step | Action | Expected | +|------|--------|----------| +| 5.1 | Open two separate PRs against `main` simultaneously. | Each PR triggers its own independent "PR Artifacts" workflow run. The runs do not interfere with each other. | +| 5.2 | On one PR, push two commits in rapid succession (before the first build completes). Observe the Actions tab. | The first workflow run is cancelled. Only the second run completes. | + +## End-to-End: Full PR Lifecycle + +1. Open a PR with a valid Go change against `main`. +2. Wait for "PR Artifacts" workflow to trigger. Confirm no "CI" workflow triggers. +3. Watch the job graph: "Lint" and "Test" run first, then "Build Snapshot" starts. +4. After "Build Snapshot" completes, verify 5 artifacts in the run summary. +5. Check the PR conversation for the `## PR Build` comment with correct version, platform table, and download link. +6. Click the download link; confirm it points to the correct run. +7. Download `cpm-linux-amd64` artifact. Extract the archive. Run `./cpm --version` and confirm the version includes the snapshot string. +8. Push a follow-up commit. Confirm the existing comment is updated in-place with the new version. +9. Merge the PR. Push a commit to `main`. Confirm the "CI" workflow triggers on push. + +## Traceability + +| Acceptance Criterion | Automated Test | Manual Step | +|----------------------|----------------|-------------| +| Phase 1 config validity | GoReleaser `check` (PASS) | -- | +| Phase 2 YAML syntax | Python YAML parse (PASS) | -- | +| Phase 3 YAML syntax | Python YAML parse (PASS) | -- | +| AC1.1 Platform archives | -- | 3.3, 3.4 | +| AC1.2 Universal macOS binary | -- | 3.5 | +| AC1.3 Artifacts with 7-day retention | -- | 3.4, 3.7 | +| AC1.4 checksums.txt uploaded | -- | 3.6 | +| AC1.5 Build failure propagation | `if-no-files-found: error` verified | 2.4 (optional) | +| AC2.1 Snapshot version format | -- | 3.1 | +| AC2.2 GORELEASER_CURRENT_TAG set | -- | 3.2 | +| AC2.3 PR_NUMBER env var | -- | 3.2 | +| AC3.1 build-snapshot depends on lint+test | `needs: [lint, test]` verified | 2.1 | +| AC3.2 Lint/test match ci.yml | -- | 2.2, 2.3 | +| AC3.3 ci.yml push-only | YAML parse confirms no `pull_request` key (PASS) | 1.1, 1.3 | +| AC3.4 Build skipped on failure | `needs` dependency verified | 2.4 (optional) | +| AC4.1 PR comment posted | -- | 4.1, 4.2 | +| AC4.2 Comment updated in-place | -- | 4.4 | +| AC4.3 Comment has run link | -- | 4.3 | +| AC4.4 Comment failure non-blocking | `continue-on-error: true` verified | -- | +| AC5.1 Concurrency group per PR | Group key verified | 5.1 | +| AC5.2 Cancel-in-progress | `cancel-in-progress: true` verified | 5.2 | From 26f8b4735dc0dd24aadc4b52b29b69ed71258d1a Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 15:38:26 -0500 Subject: [PATCH 7/8] ci: rename Build Snapshot job to Build for branch protection The main branch requires a "Build" status check. Rename the job display name so it satisfies the existing branch protection rule. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pr-artifacts.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-artifacts.yml b/.github/workflows/pr-artifacts.yml index d4db175..26097d9 100644 --- a/.github/workflows/pr-artifacts.yml +++ b/.github/workflows/pr-artifacts.yml @@ -68,7 +68,7 @@ jobs: fail_ci_if_error: false build-snapshot: - name: Build Snapshot + name: Build needs: [lint, test] runs-on: ubuntu-latest steps: From 33eadf7d4f6167c6cfce5e50f2790c3ffd2df1a3 Mon Sep 17 00:00:00 2001 From: Bojan Rajkovic Date: Fri, 27 Feb 2026 15:39:34 -0500 Subject: [PATCH 8/8] ci: restore pull_request trigger on ci.yml for branch protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the Phase 3 removal — ci.yml must keep the pull_request trigger so the "Build" required status check is satisfied on PRs. The redundant lint/test runs are acceptable overhead. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 2 ++ .github/workflows/pr-artifacts.yml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb85b56..2b36d23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,8 @@ name: CI on: push: branches: [main] + pull_request: + branches: [main] permissions: contents: read diff --git a/.github/workflows/pr-artifacts.yml b/.github/workflows/pr-artifacts.yml index 26097d9..d4db175 100644 --- a/.github/workflows/pr-artifacts.yml +++ b/.github/workflows/pr-artifacts.yml @@ -68,7 +68,7 @@ jobs: fail_ci_if_error: false build-snapshot: - name: Build + name: Build Snapshot needs: [lint, test] runs-on: ubuntu-latest steps: