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 }}* 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 }}" 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 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. | 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 |