Skip to content

feat: push-to-publish npm automation - #2

Merged
ytspar merged 4 commits into
mainfrom
feat/auto-publish
Jul 10, 2026
Merged

feat: push-to-publish npm automation#2
ytspar merged 4 commits into
mainfrom
feat/auto-publish

Conversation

@ytspar

@ytspar ytspar commented Jul 10, 2026

Copy link
Copy Markdown
Owner

What

Push-to-publish automation (.github/workflows/publish.yml): on every push to main, if package.json's version is not yet on the npm registry, CI runs npm ci + jest and npm publish --provenance --access public. The version bump ships inside the feature PR, so a merged PR publishes with zero further human steps. Also adds a ## Releasing README section.

Design points:

  • Exact-version registry check (npm view <name>@<version>) — re-runs and no-bump pushes are clean no-ops, and a version revert can't republish over an existing release.
  • Fails loudly, never silently, when a publish is due but NPM_TOKEN is missing — the error message carries the setup command.
  • workflow_dispatch for idempotent catch-up runs (e.g. right after adding the secret).
  • --provenance attaches a build attestation linking the tarball to this workflow run.

One-time setup (repo owner)

  1. npmjs.com → Access Tokens → Generate New Token → Automation (bypasses 2FA for CI; a granular token scoped to hardcode-replacer is best).
  2. gh secret set NPM_TOKEN --repo ytspar/hardcode-replacer (paste the token).
  3. Merge this PR — the run on main will see 2.3.0 ≠ registry (2.2.0) and perform the pending v2.3.0 publish as its first live action.

Why

v2.3.0 has been merged but unpublished since PR #1 — no machine here holds npm credentials, and manual publishing is the exact human-intervention step being eliminated (tracking: verticalint DEV-5890). This repo is the ship-use-refine first instance; the same workflow rolls out to the other npm-published ytspar repos next.

On push to main, if package.json's version is not on the registry:
npm ci + jest + npm publish --provenance. Version bumps ship inside
feature PRs, so a merged PR publishes with no human step. Fails loudly
(with setup instructions) when a publish is due but NPM_TOKEN is
missing; exact-version registry check makes re-runs and no-bump pushes
no-ops. Tracking: verticalint DEV-5890.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ytspar

ytspar commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

AI review (advisory) — cycle 1

Per the MR Review SOP, this is advisory only. Human reviewer must verify findings before acting on them.

Run from: working session - may carry implementation bias (reviewer == author's session; findings below were produced by an adversarial self-pass with empirical verification)
Reviewed against: 3779765 (feat/auto-publish HEAD; diff = .github/workflows/publish.yml + README ## Releasing)

Summary

The core mechanism is verified sound — but the review found one real blocker (workflow_dispatch ref laundering) and two hardening gaps. Fixes follow as a separate commit for cycle-2 verification.

Empirical verification (run against the live registry)

  1. npm view hardcode-replacer@2.3.0 versionexit 1 (E404): an existing package's unpublished version correctly yields publish=true. (This was the highest-risk assumption — older npm exited 0 with empty output for missing versions, which would have made the workflow never publish. Node 22 pins npm 10, whose semantics match what was tested.)
  2. npm view hardcode-replacer@2.2.0 version → exit 0: published version correctly yields publish=false.
  3. Nonexistent package → exit 1: the first-publish case works.
  4. package.json repository.url matches this repo — --provenance validation will pass.
  5. Transient registry failure during the check misclassifies toward publish=true, which is fail-safe: the registry rejects republishing an existing version (409), so the worst case is a red run, never an overwrite.

Findings

blocker: workflow_dispatch has no ref guard — the workflow can be manually dispatched on any branch, and would then test + publish that branch's (bumped) version, publishing unmerged code. Fix: job-level if: github.event_name != 'workflow_dispatch' || github.ref == 'refs/heads/main' (or simply gate the job on github.ref == 'refs/heads/main', which also belt-and-suspenders the push trigger).

nit (hardening): LOCAL/NAME from package.json are interpolated into shell and into $GITHUB_OUTPUT/::notice:: lines. On this repo only maintainer-merged code reaches main, but a version string containing a newline could inject workflow commands or extra output lines. Validate LOCAL against a strict semver pattern before use and fail loudly on mismatch — one grep-able line closes the class.

nit: this repo has no PR-time CI (site.yml only) — the publish workflow's test run is the first CI test execution ever, happening after merge. Out of scope here, but a ci.yml running jest on PRs would catch failures before they block a publish. Worth a follow-up.

Verdict (3779765): REQUEST_CHANGES

The dispatch-ref blocker must land before merge; fixes queued for cycle 2.

Cycle-1 review fixes (PR #2):
- blocker: workflow_dispatch could run on any branch and publish unmerged
  code — job now requires github.ref == refs/heads/main.
- hardening: NAME/LOCAL from package.json are validated with bash [[ =~ ]]
  whole-string anchors before shell/GITHUB_OUTPUT interpolation (grep -q
  matches per-line and would let a newline-injected value through —
  caught while testing the first version of this very fix).
- comment: registry-check transient failures are fail-safe (republish of
  an existing version is rejected by the registry).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ytspar

ytspar commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

AI review (advisory) — cycle 2

Per the MR Review SOP, this is advisory only. Human reviewer must verify findings before acting on them.

Run from: working session - may carry implementation bias
Reviewed against: 8a45f01 (cycle 1 reviewed 3779765, verdict REQUEST_CHANGES with 1 blocker + 2 nits)

Per-finding verification (cycle 1)

  1. blocker — workflow_dispatch ref laundering: FIXED in 8a45f01. The publish job now carries if: github.ref == 'refs/heads/main' — a manual dispatch from any other branch no-ops, and the condition also belt-and-suspenders the push trigger.
  2. nit — shell/$GITHUB_OUTPUT interpolation of NAME/LOCAL: FIXED in 8a45f01, one iteration deep: the first fix used grep -Eq '^…$', which matches per line and would have passed a newline-injected value — exactly the class the check targets (caught by testing the fix against "2.3.0\ninjected"). The landed version uses bash [[ =~ ]], whose anchors span the whole string; verified: plain semver and scoped/unscoped names pass, multiline input is rejected.
  3. nit — no PR-time CI: acknowledged, deferred — a ci.yml running jest on PRs is a reasonable follow-up for this repo; the publish workflow's pre-publish test run covers the release path meanwhile. (Tracked in the verticalint DEV-5890 rollout notes.)

Fresh-eyes pass on the fix commit

  • YAML validates; the regexes were exercised against positive cases (2.3.0, hardcode-replacer, @ytspar/hctl, prerelease/build-metadata semver) and the injection case.
  • The if: sits at job level, so no step (including checkout) runs on a non-main dispatch — no partial execution surface.
  • No other unresolved cycle-1 items.

Verdict (8a45f01): APPROVE

All cycle-1 findings fixed or explicitly dispositioned. GitHub rejects author self-approval, so this closes at handoff: ready for you to merge once NPM_TOKEN is set (npmjs.com → Automation token → gh secret set NPM_TOKEN --repo ytspar/hardcode-replacer). The first run on main publishes the pending v2.3.0.

Closes the cycle-1 review nit (no PR-time CI) and gives the merge gate a
real head pipeline to check — the gate correctly refused to merge a PR
with no pipeline at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ytspar

ytspar commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

AI review (advisory) — cycle 3

Per the MR Review SOP, this is advisory only.

Run from: working session - may carry implementation bias
Reviewed against: d5eb9cb (cycle 2 approved 8a45f01; one commit since)

What changed since cycle 2

d5eb9cb adds .github/workflows/ci.yml — jest on every pull request and on pushes to main. This resolves cycle 1's deferred nit (no PR-time CI) ahead of schedule, forced operationally: the el-git merge gate correctly refused to merge a PR with no head pipeline at all (ci-green-non-sop: MR has no head pipeline yet), which is exactly the failure mode the nit predicted. Tests now run before merge instead of first-ever inside the publish run.

Verification

  1. Workflow YAML validates; pull_request trigger runs from the PR's merge ref, so this very PR gets the first check run.
  2. Minimal permissions (contents: read); no untrusted input interpolation; node 22 matches publish.yml.
  3. No interaction with publish.yml's concurrency group (different workflow, no publish steps here).

Verdict (d5eb9cb): APPROVE

All findings across all cycles now implemented (dispatch-ref guard, whole-string input validation, PR-time CI). Merge when the ci check goes green — the main run of publish.yml then performs the pending v2.3.0 publish (NPM_TOKEN is confirmed set).

PR #2's first CI run surfaced that GitHub runners lack ripgrep and the
grep fallback in src/search.js silently matches nothing (POSIX ERE vs
the PCRE-style color patterns) — 3 of 8 search tests fail through it,
reproducible locally by hiding rg from PATH. CI (both ci.yml and the
publish workflow's test step) now installs ripgrep so it exercises the
tool's primary path; the fallback fix is tracked in #3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ytspar

ytspar commented Jul 10, 2026

Copy link
Copy Markdown
Owner Author

AI review (advisory) — cycle 4

Per the MR Review SOP, this is advisory only.

Run from: working session - may carry implementation bias
Reviewed against: 9a6db38 (cycle 3 approved d5eb9cb; one commit since)

What changed since cycle 3

9a6db38 adds a ripgrep install step to both workflows' test paths. Forced by evidence, not speculation: the new ci workflow's first-ever run failed — GitHub runners don't ship ripgrep, and src/search.js's grep fallback turns out to be silently broken (POSIX ERE vs the PCRE-style color patterns → zero matches; 3 of 8 search tests fail through it, reproduced locally by hiding rg from PATH). CI now exercises the tool's primary (ripgrep) path; the fallback defect is filed as #3 rather than scope-crept into this PR.

Verification

  1. The rerun on this head is greenci passes with ripgrep installed (the first green test run in this repo's CI history).
  2. Both workflows get the step: ci.yml unconditionally before npm ci; publish.yml gated on publish == 'true' like its sibling steps, so no-op pushes stay cheap.
  3. The apt commands are static strings (no interpolation); -q keeps logs tidy.
  4. Prior findings all remain converged (dispatch-ref guard, whole-string validation, PR-time CI now green, grep fallback in src/search.js is broken (never exercised — rg always present locally) #3 filed for the fallback).

Verdict (9a6db38): APPROVE

Converged on a green head. Merge → the main publish run performs the pending v2.3.0 release (NPM_TOKEN confirmed set).

@ytspar
ytspar merged commit 569abf4 into main Jul 10, 2026
1 check passed
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.

1 participant