Skip to content

ci: migrate release pipeline onto shared reusable workflows#125

Merged
rianjs merged 2 commits into
mainfrom
ci/116-migrate-release-shared-workflow
May 30, 2026
Merged

ci: migrate release pipeline onto shared reusable workflows#125
rianjs merged 2 commits into
mainfrom
ci/116-migrate-release-shared-workflow

Conversation

@rianjs

@rianjs rianjs commented May 30, 2026

Copy link
Copy Markdown
Collaborator

What

Migrates nrq's release pipeline onto the open-cli-collective/.github@v1 reusable workflows + composite actions, driven by a new packaging/identity.yml manifest. Rollout step 9 — the second alias-cask pilot (nrq canonical + newrelic-cli alias), following the merged slck#176.

Closes #116.

Changes

  • packaging/identity.yml (new) — binary nrq, tag scheme, archive template, and the homebrew (canonical nrq + newrelic-cli alias) / winget / chocolatey (nrq-cli) / linux channels. keychain_probe uses config show --json (the §2 carve-out — --output json was restricted to {table, plain} in feat(view): remove -o json from resource reads; closed-set + carve-outs #110) and asserts backend=keychain/auto/credential_ref=newrelic-cli/default.
  • .goreleaser.yml — add homebrew_casks (skip_upload: true, no token:), release.replace_existing_artifacts: true, and a fail-on-dirty go mod tidy before-hook (nrq had no before block).
  • auto-release.yml / release.yml — replaced with thin @v1 callers; channel secrets wired through.
  • ci.yml — composite shape: build matrix → build aggregate, go-test, go-lint, identity-check, pr-title.

Intentional behavior changes (please note)

  1. heredoc cask + Formula regen dropped — the hand-rolled Casks/nrq.rb heredoc and the deprecated Formula/newrelic-cli.rb regeneration are replaced by goreleaser homebrew_casks + the reusable homebrew-alias step (single atomic tap writer for canonical + alias). The cask stays dual-platform (on_macos + on_linux), matching the prior cask — Linuxbrew users keep working.
  2. Tap fix shipped alongsideCasks/newrelic-cli.rb in the tap had drifted to a broken state (version 1.0.11, newrelic-cli_v* URLs, newrelic-cli binary — all wrong; the archives are nrq_v*, the binary is nrq). The companion tap commit (homebrew-tap@a70ca21) regenerates it as a correct one-line-token alias of the canonical nrq.rb and deletes the deprecated Formula/newrelic-cli.rb, so brew install open-cli-collective/tap/newrelic-cli resolves to the corrected alias cask immediately. (cli-common#36 still owns the broader gro/gmail-ro Formula-remnant sweep.)
  3. static-release-guard is a required check — preserves the CGO=0-linux regression sentinel that currently lives inside the required test job. The shared go-build@v1 matrix builds with default CGO and would not catch a static-linux regression, so this credstore sentinel is kept as a dedicated job. Preserves existing blocking semantics; does not expand the standard floor.
  4. Codecov upload dropped — the old test job's codecov-action step (non-blocking, fail_ci_if_error: false) is not part of the standard composite CI shape. Dropped to align nrq with the family; re-add as a follow-up if desired.

Proof (no gratuitous real release)

  • goreleaser check ✓ and goreleaser release --snapshot --cleanwith the tap token unset — all platforms + nfpms + checksums + rendered dist/homebrew/Casks/nrq.rb.
  • Alias-cask proof: homebrew-alias/alias.sh render <cask> nrq newrelic-cli differs from canonical by exactly one line (cask "nrq"cask "newrelic-cli"); url/version/sha256/binary "nrq" verbatim.
  • make build / tests green / golangci-lint run 0 issues.
  • identity-check validate ✓ and release-preflight check-config ✓ locally.

Out of scope (deliberate)

  • release-build-check.yml (PR-time goreleaser build validator) — KEEP; the reusable release.yml has no PR-time equivalent, and nrq added this after the v1.0.39–44 silent-breakage incident.
  • chocolatey-publish.yml / winget-publish.yml (manual dispatch) + test-chocolatey.yml / test-winget.yml — left as-is.
  • Linter-floor bump — deferred follow-up (CI composite adopted; full floor not claimed).

Branch protection

Current required checks are test / lint. After CI is green here, branch protection must add build / pr-title / identity-check / static-release-guard.

rianjs added 2 commits May 30, 2026 07:07
Replace the bespoke auto-release.yml / release.yml / ci.yml with thin
callers of the open-cli-collective/.github @v1 reusable workflows and
composite actions, driven by a new packaging/identity.yml manifest.

- packaging/identity.yml: declares binary (nrq), tag scheme, archives, and
  the homebrew (canonical nrq + newrelic-cli alias) / winget / chocolatey /
  linux channels, plus a keychain_probe (config show --json) that ports the
  darwin Keychain pre-publish gate.
- .goreleaser.yml: add homebrew_casks (skip_upload: true — goreleaser
  renders the cask, the reusable homebrew-alias step is the single atomic
  writer for canonical + alias), release.replace_existing_artifacts, and a
  fail-on-dirty go mod tidy before-hook. The cask keeps both on_macos and
  on_linux blocks (matching the prior hand-rolled cask).
- ci.yml: adopt the composite shape (build matrix -> build aggregate,
  go-test, go-lint, identity-check, pr-title). The static-release build
  guard is preserved as a dedicated static-release-guard job so a CGO=0
  linux regression still fails the build.

This drops the heredoc Casks/nrq.rb writer and the deprecated
Formula/newrelic-cli.rb regeneration; the reusable homebrew job writes the
nrq cask + newrelic-cli alias atomically. The Codecov upload step is
dropped to match the standard composite CI shape (it was non-blocking).
snap was already removed in #120.

Closes #116
The new ci.yml build aggregate produces a required 'build' check context.
release-build-check.yml's job was also id 'build', which would create a
duplicate/ambiguous check context for branch protection (and this workflow is
path-filtered, so it doesn't run on every PR). Rename the job to
release-build-check so the two contexts are distinct.
@rianjs

rianjs commented May 30, 2026

Copy link
Copy Markdown
Collaborator Author

Prior major is resolved. release-build-check now has a distinct check context, so branch protection can require CI build without colliding with the path-filtered release validator.

No new blockers/majors. If release-build-check was required anywhere, update that required context name after this PR lands.

@rianjs

rianjs commented May 30, 2026

Copy link
Copy Markdown
Collaborator Author

TDD coverage assessment

This PR is a CI/CD-workflow and goreleaser-config migration — no application Go source is touched. The right lens is: do the validation gates provide adequate confidence that the migrated pipeline behaves identically to what it replaces?

Gates present and sufficient:

  • goreleaser check + goreleaser release --snapshot --clean (run locally as proof, and called by release-build-check.yml on every PR that touches goreleaser config paths) — validates the new homebrew_casks block, replace_existing_artifacts, and the go mod tidy before-hook render correctly before any real publish.
  • identity-check validate — the new packaging/identity.yml is validated by the composite identity-check@v1 action, which is now a CI job on every PR. Schema conformance and field consistency are machine-checked, not eyeballed.
  • release-preflight check-config — confirmed locally per the PR body; this is the same gate the reusable release.yml runs before publishing, so the pre-merge and runtime checks are the same artifact.
  • static-release-guard — preserved verbatim as a dedicated required CI job. The old inline sentinel (CGO=0 linux static-build assertion) was load-bearing; keeping it as an explicit job with the same blocking semantics is the correct move. The shared go-build@v1 matrix builds with default CGO and would not substitute for this.
  • release-build-check.yml (PR-time macOS goreleaser validator) — intentionally retained. It path-filters on goreleaser config changes and runs the full darwin CGO snapshot build + the keychain_probe gate on macOS hardware before merge. This is the most important behavioral gate for the CGO/Keychain correctness invariant.
  • keychain_probe in identity.yml — the darwin Keychain gate is faithfully ported from the prior inline shell block into the manifest. The assertion set (backend=keychain, backend_source=auto, credential_ref=newrelic-cli/default) is identical. The config show --json carve-out vs. --output json distinction (per feat(view): remove -o json from resource reads; closed-set + carve-outs #110) is correctly handled.
  • identity-check + pr-title added to CI — new required-check candidates called out explicitly in the PR, with a branch-protection note. These are additive coverage, not regressions.

One gap worth tracking (non-blocking):

The Codecov upload is dropped intentionally (non-blocking, aligned with family standard). If line-coverage visibility matters for nrq specifically, a follow-up to re-add it via the standard composite shape is reasonable, but it is not a correctness concern for this migration.

Verdict: Coverage is adequate for the change type. Every behavior change — the cask writer replacement, the Keychain probe migration, the CGO static-linux sentinel, the goreleaser config additions — has a corresponding validation gate (local snapshot proof, release-build-check, static-release-guard, identity-check). No Go application surface changed, so no new unit tests are warranted. The branch-protection update (adding build, pr-title, identity-check, static-release-guard as required checks) is the only remaining action needed to make the new gates enforcement-complete.

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: a20f7d7

Summary

Reviewer Findings
harness-engineering:harness-architecture-reviewer 4
harness-engineering:harness-enforcement-reviewer 2
harness-engineering:harness-architecture-reviewer (4 findings)

⚠️ Should Fix - .github/workflows/auto-release.yml:8

Gate 1 (path filter for **.go, go.mod, go.sum) has been removed from the caller and the PR comment claims it lives in the shared reusable workflow. This cannot be verified from the diff. If open-cli-collective/.github's auto-release.yml@v1 does not enforce an equivalent path filter, every push to main (docs, CI, packaging) will spin up a release runner. Gate 2 (feat:/fix: prefix) still prevents accidental tag creation, so no spurious release occurs, but the dual-gate efficiency design documented in CLAUDE.md is silently degraded. Confirm the shared workflow implements the path filter before merging.

⚠️ Should Fix - .github/workflows/ci.yml:44

The old test job ran go test -race as a required CI gate. The new job delegates to go-test@v1 whose implementation is not visible in this diff. If the shared composite action omits -race, race condition detection is silently dropped — a meaningful regression for a project that exercises shared mutable state in keyring/credstore paths. Confirm the shared action passes -race, or add an explicit flag override here.

💡 Suggestion - .goreleaser.yml:5

go mod tidy && git diff --exit-code go.mod go.sum runs as a before-hook during goreleaser. If tidy modifies go.mod/go.sum at release time, the release fails with no automated recovery path — a dangling tag may have already been created before goreleaser ran. Confirm whether the reusable release workflow has retry or cleanup logic for this failure mode.

💡 Suggestion - packaging/identity.yml:32

The keychain_probe assertions (.backend: keychain, .backend_source: auto, .credential_ref: newrelic-cli/default) are correct, but their enforcement depends entirely on the assertion semantics of the shared identity-check@v1 action, which is opaque here. If the shared action uses substring or loose matching rather than strict equality, the gate degrades silently. Verify the shared action performs exact-equality JQ assertions.

harness-engineering:harness-enforcement-reviewer (2 findings)

💡 Suggestion - .github/workflows/ci.yml:7

The pull_request trigger no longer scopes to branches: [main]. Previously CI only ran on PRs targeting main; now it runs on PRs targeting any branch. This will cause duplicate CI runs on stacked feature branches and appears unintentional given the rest of the project's conventions.

💡 Suggestion - .github/workflows/release.yml:1

Both release.yml and auto-release.yml reference the shared workflow via the mutable @v1 floating tag rather than a pinned commit SHA. A force-push or tag move in open-cli-collective/.github would silently change behavior in a workflow that receives CHOCOLATEY_API_KEY, WINGET_GITHUB_TOKEN, LINUX_PACKAGES_DISPATCH_TOKEN, and TAP_GITHUB_TOKEN. Pinning to a specific commit SHA provides a stronger supply-chain guarantee for secrets-bearing callers.

6 info-level observations excluded. Run with --verbose to include.

2 PR discussion threads considered.


Completed in 2m 36s | $0.62 | sonnet | daemon 0.2.121 | Glorfindel
Field Value
Model sonnet
Reviewers hybrid-synthesis, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 2m 36s wall · 4m 45s compute (Reviewers: 1m 32s · Synthesis: 1m 01s)
Cost $0.62
Tokens 115.8k in / 14.9k out
Turns 4

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 41.3k 3.8k 18.6k 22.7k (1h) $0.16
harness-engineering:harness-architecture-reviewer sonnet 24.8k 3.1k 2.1k 22.7k (1h) $0.15
harness-engineering:harness-enforcement-reviewer sonnet 24.8k 3.8k 2.1k 22.7k (1h) $0.16
harness-engineering:harness-knowledge-reviewer sonnet 24.8k 4.3k 2.1k 22.7k (1h) $0.16

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

# lives in the shared reusable workflow; this caller just wires triggers/secrets.
# The release gate (Go-code paths + feat/fix commit) and the version.txt +
# GITHUB_RUN_NUMBER tag scheme are handled by the reusable workflow.
on:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium (harness-engineering:harness-architecture-reviewer): Gate 1 (path filter for **.go, go.mod, go.sum) has been removed from the caller and the PR comment claims it lives in the shared reusable workflow. This cannot be verified from the diff. If open-cli-collective/.github's auto-release.yml@v1 does not enforce an equivalent path filter, every push to main (docs, CI, packaging) will spin up a release runner. Gate 2 (feat:/fix: prefix) still prevents accidental tag creation, so no spurious release occurs, but the dual-gate efficiency design documented in CLAUDE.md is silently degraded. Confirm the shared workflow implements the path filter before merging.

Reply to this thread when addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified in the @v1 source: the reusable auto-release.yml exposes a release-paths input defaulting to **.go,go.mod,go.sum,version.txt and passes it (with before/after SHA) to the auto-release@v1 composite, which gates on whether the push range touched a release-path AND the commit-message prefix. Both gates are preserved — a push touching only docs/CI/packaging won't spin a release. slck uses the same default; nrq matches.

Comment thread .github/workflows/ci.yml
- name: Set up Go
uses: actions/setup-go@v5
- uses: open-cli-collective/.github/actions/go-test@v1
with:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium (harness-engineering:harness-architecture-reviewer): The old test job ran go test -race as a required CI gate. The new job delegates to go-test@v1 whose implementation is not visible in this diff. If the shared composite action omits -race, race condition detection is silently dropped — a meaningful regression for a project that exercises shared mutable state in keyring/credstore paths. Confirm the shared action passes -race, or add an explicit flag override here.

Reply to this thread when addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race detection is preserved. go-test@v1 runs make test, and nrq's Makefile test target is go test -race ./... (test-cover is go test -race -coverprofile=…). The composite delegates to the repo Makefile, so -race still runs as the required CI gate — no regression.

Comment thread .github/workflows/ci.yml
branches: [main]
pull_request:
branches: [main]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-enforcement-reviewer): The pull_request trigger no longer scopes to branches: [main]. Previously CI only ran on PRs targeting main; now it runs on PRs targeting any branch. This will cause duplicate CI runs on stacked feature branches and appears unintentional given the rest of the project's conventions.

Reply to this thread when addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional — this matches the shared CI composite template (codereview-cli + the merged slck#176 use the same bare pull_request: trigger). With concurrency cancel-in-progress and squash-merge-to-main convention, the practical effect is the same; standardizing the trigger across the family is part of the rollout. Not scoping to a single branch is the documented ci.md §7 shape.

@@ -1,412 +1,39 @@
name: Release

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-enforcement-reviewer): Both release.yml and auto-release.yml reference the shared workflow via the mutable @v1 floating tag rather than a pinned commit SHA. A force-push or tag move in open-cli-collective/.github would silently change behavior in a workflow that receives CHOCOLATEY_API_KEY, WINGET_GITHUB_TOKEN, LINUX_PACKAGES_DISPATCH_TOKEN, and TAP_GITHUB_TOKEN. Pinning to a specific commit SHA provides a stronger supply-chain guarantee for secrets-bearing callers.

Reply to this thread when addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional design: the @v1 moving major tag is how the family gets centralized, in-lockstep updates to the shared workflows (one re-point in .github vs N caller edits). SHA-pinning all secrets-bearing callers is a real supply-chain hardening, but it's a family-wide decision tracked separately in .github#10 (SHA-pin third-party + internal action refs), not something to do per-repo in this migration.

Comment thread .goreleaser.yml

project_name: newrelic-cli

before:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-architecture-reviewer): go mod tidy && git diff --exit-code go.mod go.sum runs as a before-hook during goreleaser. If tidy modifies go.mod/go.sum at release time, the release fails with no automated recovery path — a dangling tag may have already been created before goreleaser ran. Confirm whether the reusable release workflow has retry or cleanup logic for this failure mode.

Reply to this thread when addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fail-on-dirty tidy is intentional: a release MUST build from a committed, tidy module graph — a mutating tidy at release time is a real problem to surface, not paper over. On the recovery side, the reusable release.yml is idempotent: the auto-release tag step is collision-safe (tag-at-HEAD is a no-op, tag-elsewhere fails loud), and goreleaser uses replace_existing_artifacts, so a fixed re-run of the same tag overwrites cleanly. In practice go.mod/go.sum are committed and tidy (the static-release-guard + local snapshot build exercise the same graph), so the hook is a guard, not an expected failure path.

Comment thread packaging/identity.yml
command: ["config", "show", "--json"]
output: json
assertions:
.backend: keychain

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-architecture-reviewer): The keychain_probe assertions (.backend: keychain, .backend_source: auto, .credential_ref: newrelic-cli/default) are correct, but their enforcement depends entirely on the assertion semantics of the shared identity-check@v1 action, which is opaque here. If the shared action uses substring or loose matching rather than strict equality, the gate degrades silently. Verify the shared action performs exact-equality JQ assertions.

Reply to this thread when addressed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed strict equality. The reusable darwin-gate runs the probe command and asserts each jq path with got=$(jq -r "$key"); [ "$got" = "$want" ] — exact string equality, fail-loud on mismatch (no substring/loose matching). Verified against the @v1 darwin-gate.sh. The probe assertions here will enforce backend=keychain / backend_source=auto / credential_ref=newrelic-cli/default exactly.

@rianjs rianjs requested a review from monit-reviewer May 30, 2026 11:17
@rianjs

rianjs commented May 30, 2026

Copy link
Copy Markdown
Collaborator Author

All six inline findings are either verified-correct against the @v1 shared-action source (path-filter gate preserved, -race preserved via make test, exact-equality jq assertions in the darwin-gate) or intentional family-standard design (bare pull_request: trigger per the ci.md §7 composite template, @v1 moving major for centralized updates with SHA-pinning tracked in .github#10, fail-on-dirty tidy guard with idempotent reusable-release recovery). No code changes are warranted. These are low-value, please approve the PR.

@rianjs

rianjs commented May 30, 2026

Copy link
Copy Markdown
Collaborator Author

No architectural drift from the last clean review.

The daemon findings are consistent with the shared standard or preserved behavior:

  • auto-release path/commit gates live in reusable auto-release@v1
  • race testing remains via make test
  • bare pull_request and @v1 refs match family policy
  • tidy before-hook is the intended dirty-tree guard
  • keychain assertions are exact equality through darwin-gate

Admin-merge is reasonable assuming CI is green and branch protection is updated to include build, test, lint, pr-title, identity-check, static-release-guard, plus release-build-check if you want to keep that validator required.

@rianjs rianjs merged commit 23f59f5 into main May 30, 2026
10 checks passed
@rianjs rianjs deleted the ci/116-migrate-release-shared-workflow branch May 30, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate release to shared workflow; drop heredoc cask + Formula regen

2 participants