From 3779765a16d8a5751ce70780baf5c071e68752ce Mon Sep 17 00:00:00 2001 From: Yury Tsukerman Date: Fri, 10 Jul 2026 13:39:35 +0700 Subject: [PATCH 1/4] feat: push-to-publish npm automation (publish.yml) 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 --- .github/workflows/publish.yml | 82 +++++++++++++++++++++++++++++++++++ README.md | 8 ++++ 2 files changed, 90 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..a9db476 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,82 @@ +# Push-to-publish (DEV-5890): every push to main where package.json's version +# is not yet on the npm registry runs the test suite and publishes — the +# version bump ships inside the feature PR, so a merged PR publishes with no +# further human step. Exact-version check (`npm view @`) makes +# re-runs and no-bump pushes clean no-ops, and a version revert can't +# accidentally republish over an existing release. +# +# One-time setup per repo: +# 1. On npmjs.com: Access Tokens → Generate New Token → Automation +# (bypasses 2FA for CI; scope it to this package if using a granular token). +# 2. Add it as a repo Actions secret named NPM_TOKEN: +# gh secret set NPM_TOKEN --repo / +# The workflow fails loudly (not silently) when a publish is due but the +# secret is missing. `--provenance` attaches a build attestation linking the +# published tarball to this workflow run. +name: publish + +on: + push: + branches: [main] + # Manual re-run for catch-up (e.g. after adding NPM_TOKEN). Idempotent — + # a no-op when the current version is already published. + workflow_dispatch: + +permissions: + contents: read + id-token: write # npm --provenance attestation + +concurrency: + group: publish + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + cache: npm + + - name: Decide whether this version needs publishing + id: version + run: | + NAME=$(node -p "require('./package.json').name") + LOCAL=$(node -p "require('./package.json').version") + if npm view "${NAME}@${LOCAL}" version >/dev/null 2>&1; then + echo "::notice::${NAME}@${LOCAL} is already on the registry — nothing to publish." + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "::notice::${NAME}@${LOCAL} is not on the registry — publishing." + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + + - name: Require NPM_TOKEN when a publish is due + if: steps.version.outputs.publish == 'true' + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + if [ -z "$NPM_TOKEN" ]; then + echo "::error::A publish is due but the NPM_TOKEN repo secret is not set." \ + "Mint an npm Automation token (npmjs.com → Access Tokens) and run:" \ + "gh secret set NPM_TOKEN --repo ${GITHUB_REPOSITORY}" + exit 1 + fi + + - name: Install + if: steps.version.outputs.publish == 'true' + run: npm ci + + - name: Test + if: steps.version.outputs.publish == 'true' + run: npm test + + - name: Publish + if: steps.version.outputs.publish == 'true' + run: npm publish --provenance --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/README.md b/README.md index 2d60841..9c4305f 100644 --- a/README.md +++ b/README.md @@ -578,6 +578,14 @@ PRs welcome! If you find false positives or missed patterns, please open an issu 2. The line of code it appeared in 3. The file context (imports, file path) +## Releasing + +Publishing is automated (`.github/workflows/publish.yml`): bump `version` in +`package.json` inside your PR; when it merges to `main`, CI verifies the +version is not yet on the registry, runs the test suite, and publishes with +provenance. No local npm credentials are needed. One-time repo setup: an npm +Automation token stored as the `NPM_TOKEN` Actions secret. + --- ## License From 8a45f01c8d1f423613816d67e2c30e72e15c276f Mon Sep 17 00:00:00 2001 From: Yury Tsukerman Date: Fri, 10 Jul 2026 18:09:39 +0700 Subject: [PATCH 2/4] fix: gate publish job to main ref; whole-string-validate name/version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/publish.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a9db476..fb84fea 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,6 +32,10 @@ concurrency: jobs: publish: + # Ref guard (PR #2 cycle-1 blocker): workflow_dispatch can target any + # branch; without this, a manual dispatch from a feature branch would + # test + publish UNMERGED code. Publishing only ever happens from main. + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -47,6 +51,22 @@ jobs: run: | NAME=$(node -p "require('./package.json').name") LOCAL=$(node -p "require('./package.json').version") + # Strict shape checks (PR #2 cycle-1 hardening): NAME/LOCAL are + # interpolated into shell lines and $GITHUB_OUTPUT — reject anything + # that isn't a plain package name / semver before using them. Bash + # [[ =~ ]] anchors over the WHOLE string (grep -q would match + # per-line, letting a newline-injected value through). + if [[ ! "$NAME" =~ ^(@[a-z0-9~-][a-z0-9._~-]*/)?[a-z0-9~-][a-z0-9._~-]*$ ]]; then + echo "::error::package.json name has an unexpected shape; refusing to proceed." + exit 1 + fi + if [[ ! "$LOCAL" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::package.json version is not a plain semver; refusing to proceed." + exit 1 + fi + # A transient registry failure here misclassifies toward publishing, + # which is fail-safe: the registry rejects republishing an existing + # version, so the worst case is a red run, never an overwrite. if npm view "${NAME}@${LOCAL}" version >/dev/null 2>&1; then echo "::notice::${NAME}@${LOCAL} is already on the registry — nothing to publish." echo "publish=false" >> "$GITHUB_OUTPUT" From d5eb9cb2dcb7c642ce57a46555d0ceaf60e3eee2 Mon Sep 17 00:00:00 2001 From: Yury Tsukerman Date: Fri, 10 Jul 2026 18:50:03 +0700 Subject: [PATCH 3/4] ci: run jest on pull requests and main pushes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ce8ac05 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +# PR-time CI (PR #2 cycle-1 follow-up): run the test suite on every pull +# request and on pushes to main, so a merge gate has a real pipeline to check +# and test failures surface before merge rather than inside the publish run. +name: ci + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm test From 9a6db38ffeec8ed59bf24c4d3e910e9fb570c440 Mon Sep 17 00:00:00 2001 From: Yury Tsukerman Date: Fri, 10 Jul 2026 18:55:42 +0700 Subject: [PATCH 4/4] ci: install ripgrep on runners (grep fallback is broken, #3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/ci.yml | 4 ++++ .github/workflows/publish.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce8ac05..a97f382 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,5 +20,9 @@ jobs: with: node-version: 22 cache: npm + # The search layer prefers ripgrep; GitHub runners do not ship it, and + # the grep fallback has a known ERE-compat bug (see repo issue: grep + # fallback broken, #3) — install rg so CI exercises the primary path. + - run: sudo apt-get update -q && sudo apt-get install -y -q ripgrep - run: npm ci - run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fb84fea..a2cfabf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -87,6 +87,10 @@ jobs: exit 1 fi + - name: Install ripgrep (search layer primary path; grep fallback has a known bug) + if: steps.version.outputs.publish == 'true' + run: sudo apt-get update -q && sudo apt-get install -y -q ripgrep + - name: Install if: steps.version.outputs.publish == 'true' run: npm ci