Skip to content

PFM-ISSUE-34453 - github-actions: Normalize package-lock.json resolved URLs onto the JFrog npm proxy (release/25.2) - #163

Open
Kaltenbach wants to merge 27 commits into
release/25.2from
fix/PFM-ISSUE-34453-normalize-package-lock-json/25.2
Open

PFM-ISSUE-34453 - github-actions: Normalize package-lock.json resolved URLs onto the JFrog npm proxy (release/25.2)#163
Kaltenbach wants to merge 27 commits into
release/25.2from
fix/PFM-ISSUE-34453-normalize-package-lock-json/25.2

Conversation

@Kaltenbach

@Kaltenbach Kaltenbach commented Aug 11, 2026

Copy link
Copy Markdown

Problem

This repo ships a mixed package-lock.json: 171 entries resolve via https://registry.npmjs.org/, the other 371 via the cplace JFrog npm proxy.

The composite actions run npm ci inside the action directory, on the consumer's runner, while that consumer's ~/.npmrc is active. npm rewrites an npmjs resolved URL onto the configured host and drops the registry's path prefix (pacote/lib/remote.js: new URL(resolvedURL.pathname, this.registry)), producing E404 — masked as *** because the prefix is the JFROG_URL secret.

⚠️ Scope grew during review — read this before approving

This PR is no longer tooling-only. It now contains a runtime behaviour change affecting every consumer:

.github/actions/use-npmrc appends replace-registry-host=never to the ~/.npmrc it writes.

Why it is here rather than in a follow-up: normalizing this repo's lockfile fixes the composite surface only. A consumer's own npm ci runs in the workspace against its own lockfile, and a survey of 299 repo/branch combinations across 41 FE repos found cplace-paw-fe release/25.2 and release/25.3 are broken today — 14 npmjs entries each, verified failing with a cold cache. Nothing masks it: those branches have no cache, and the key is content-addressed to the lockfile.

What it costs: entries still on npmjs are fetched directly from npmjs, bypassing the proxy — no Xray, no curation. That is a deliberate, temporary supply-chain trade to stop live breakage.

How it gets removed: use-npmrc also runs an advisory warn-foreign-registry.sh against the consumer's lockfile, emitting a ::warning plus job summary. Those warnings are the inventory; when no pipeline reports one, the line comes out. Owned by PFM-ISSUE-34454, which has a dedicated section for it. The two compose safely — on a normalized lockfile the flag is a no-op.

Verified end-to-end: cplace-paw-fe#185, full pipeline green (15 jobs) on release/25.2, with Install modules succeeding (added 2439 packages … in 8m, cold cache) and the annotation correctly reporting 14.

What is in the PR

area files
Normalizer + invariant check tools/scripts/lockfile/{lib.sh,fingerprint.jq,normalize-lockfile.sh,check-lockfile.sh}
Interim mitigation .github/actions/use-npmrc/action.yml, tools/scripts/lockfile/warn-foreign-registry.sh
PR guard .github/workflows/pr-checks.yml (this repo's first on: pull_request workflow)
Tests tools/scripts/lockfile/*.bats51 tests
The lockfile package-lock.json — 171 prefixes, its own commit
Docs tools/scripts/lockfile/README.md, specs/2026-08-10_normalize-package-lock-resolved-urls/

Why bash + jq in a TypeScript repo

Bootstrap independence. npx ts-node needs node_modules, which needs the npm ci that is broken.

Two modes, and why CI uses the weaker one

  • --prefix-only — every entry resolves via the proxy. This is the PR guard.
  • --baseline <ref> — the above plus the dependency graph is unchanged. Verifies a normalization commit.

Graph invariance forbids any dependency change, so gating PRs on it would fail every legitimate npm install. Consequence, stated plainly: a green CI run is not evidence that a lockfile diff changed only prefixes. Where that matters, run the baseline form locally.

Evidence

  • 171 entries rewritten, +4788 bytes, 342 changed lines, 0 outside a "resolved" line; idempotent
  • Output byte-identical to a raw sed prefix rewrite
  • grep -c registry.npmjs.org package-lock.json0
  • Local before/after with the real ~/.npmrc: E404added 542 packages
  • Proxy re-probed for all 171 tarballs: 166 × 200, 5 × 302, zero 403/404
  • 51/51 bats, shellcheck and actionlint clean

Every failure message names a package path, never a URL, so it survives *** masking.

Review history

Two rounds found 12 defects, all reproduced before fixing. Several failed open — reporting success on a lockfile that was never actually verified. See the inline threads; each fix has a regression test.

Known limitation

The guard reports but cannot block until PFM-ISSUE-34465 (Rule Sets) lands. release/25.2 has no branch protection at all today.

Rollout

Branch 1 of 7. master and release/26.3 are not downstream of 26.2, so this cannot ride the upmerge. The other six carry byte-identical tooling plus the same machine-checked lockfile transformation — this is the review that counts.

Spec: specs/2026-08-10_normalize-package-lock-resolved-urls/

Refs PFM-ISSUE-34453. Blocks PFM-ISSUE-34454.

🤖 Generated with Claude Code

Kaltenbach and others added 5 commits August 10, 2026 15:33
…-URL normalization

Adds the research document for normalizing the 171 `registry.npmjs.org`
`resolved` URLs in package-lock.json onto the JFrog npm proxy.

Committed on release/25.2 so it travels upward with the regular upmerge,
matching the rollout the ticket prescribes for the normalizer and the
invariant check.

Verified locally across all seven affected branches: three distinct
lockfile states, 171 npmjs entries each, byte-identical across states.
A prototype rewrite keeps the dependency graph identical and is
idempotent; a simulated bad merge (version + integrity drift) is caught
by the invariant check.

Findings that shape the implementation:
- No `pull_request`-triggered workflow exists in this repo, so the PR
  guard needs the first self-CI workflow rather than an edit to an
  existing one.
- master and release/26.3 are not downstream of release/26.2, so they
  need their own normalizer/check commits instead of inheriting them
  through the upmerge.
- fe-check-upmerge only notifies Slack via a `--no-push` dry run; it
  never performs the merge, so the conflict rule is a human runbook.
- A JSON parse/serialize round-trip is byte-identical on these
  lockfiles, so no formatting-drift risk constrains the implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…package-lock.json normalization

Adds the design document and the implementation plan for normalizing the 171
registry.npmjs.org `resolved` prefixes onto the JFrog npm proxy.

Design: bash + jq normalizer and invariant check under tools/scripts/lockfile/,
plus the repo's first `on: pull_request` workflow as a guard. Bash rather than
TypeScript because every node-based option has a bootstrap dependency on the
`npm ci` that this fix repairs.

Plan: eight phases, `release/25.2` end-to-end followed by a parameterised
runbook for the six remaining branches, with a human checkpoint per phase.
The jq programs, the tarball-path regex and the two-assertion drift matrix
(t1-t6) were verified against the real lockfile during planning: 171 entries
rewritten, +4788 bytes, 342 changed lines, 0 non-`resolved` lines, output
byte-identical to a raw prefix rewrite, and idempotent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nvariant check and PR guard

The composite actions run `npm ci` inside the action directory while the
consumer's ~/.npmrc is active. npm rewrites a `registry.npmjs.org` resolved URL
onto the configured host and drops the registry's path prefix, producing E404 -
masked as *** in logs because the prefix is the JFROG_URL secret.

Adds bash + jq tooling under tools/scripts/lockfile/:

- normalize-lockfile.sh rewrites every `resolved` prefix onto the JFrog npm
  proxy and nothing else; idempotent.
- check-lockfile.sh makes two independent assertions, both required: the
  dependency graph must be identical to a baseline (any git ref, merge stage or
  file), and every `resolved` must carry exactly one prefix - the proxy's.
  Neither subsumes the other; the fingerprint strips the host on purpose, so it
  cannot see an entry left on npmjs, and prefix exactness sees nothing else.
- 24 bats tests, including six injected-drift cases that assert the *right*
  assertion fails in each case.

bash + jq rather than TypeScript for bootstrap independence: `npx ts-node`
requires node_modules, which requires the `npm ci` this repairs.

Adds .github/workflows/pr-checks.yml - this repository's first `on: pull_request`
workflow, since every existing workflow is workflow_call-only. Both jobs are
node-free; bats and shellcheck come from apt-get, never from devDependencies,
which would mutate the lockfile this guards.

Every failure message names a package path, never a URL, so it survives ***
masking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d URLs onto the JFrog npm proxy

Rewrites 171 `resolved` prefixes from https://registry.npmjs.org/ onto the
cplace JFrog npm proxy. Nothing else changes: no version, no integrity, no
dependency edge.

Generated by tools/scripts/lockfile/normalize-lockfile.sh and provable by
construction - this commit's parent is the baseline, so:

  ./tools/scripts/lockfile/check-lockfile.sh --baseline HEAD~1

342 changed lines (171 x 2), +4788 bytes (= 171 x the 28-character prefix length
difference), zero non-`resolved` lines changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ugh Phase 5

Checks off Phases 1-5 with measured results and records four places where the
plan's own verification commands were wrong:

- the node-free grep on pr-checks.yml matched an explanatory comment;
- `git show --stat` matched `package-lock.json` in the commit message prose;
- PyYAML is not installed here (validated with ruby/psych and actionlint);
- a backticked `resolved` inside a double-quoted err string in lib.sh would
  have been executed as command substitution.

Also records the mid-phase switch to runs-on: ${{ vars.SMALL_RUNNER ||
'ubuntu-latest' }} and the proxy re-probe result (166 x 200, 5 x 302).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Kaltenbach
Kaltenbach marked this pull request as draft August 11, 2026 08:47
@Kaltenbach Kaltenbach closed this Aug 11, 2026
@Kaltenbach Kaltenbach reopened this Aug 11, 2026
Kaltenbach and others added 2 commits August 11, 2026 10:58
…nd the green guard run

The first PR run ended in startup_failure at 0s with no jobs and no
annotations. Root cause was not the workflow: the repository's Actions policy
was allowed_actions=local_only, the UI radio "Allow collaborationFactory
actions and reusable workflows", which permits only owner-owned actions and so
rejected actions/checkout - owned by the `actions` organization - before any
job started.

Records the diagnosis, what was ruled out with evidence (the SMALL_RUNNER
expression, YAML validity, the pinned SHA, the merge ref), and the measured
blast radius of raising the policy: zero. All 91 workflow files across the
seven branches are workflow_call-only, nothing under workflow-templates/ is
registered, and reusable-workflow runs are governed by the caller's policy, so
consumers were never affected.

Policy is now allowed_actions=selected with github_owned_allowed=true and
verified_allowed=false - actions/* only, narrower than an unrestricted 'all'.
Run 31475431467 is green on both jobs, with logs confirming the baseline
resolved to release/25.2's tip and both assertions executed. 24/24 bats in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or release/25.2

Canary ran in cplace-paw-fe (PR #184, closed) rather than
cplace-remote-filesystem-fe, whose release branches do not go back to 25.2.

Run 31487742492 green with zero E404. The composite's own npm ci installed 542
packages / 543 audited - the github-actions lockfile's exact entry count,
matching the local run - and `npx ts-node main.ts` then ran successfully on top,
so node_modules was functional and not merely populated.

Records two traps that would have invalidated a naive canary:

- Re-pinning only the consumer's `uses:` tests the WRONG lockfile. The composite
  runs `cd "$GITHUB_ACTION_PATH/../../.." && npm ci`, so the ref on the
  COMPOSITE decides which package-lock.json is installed. fe-pr-snapshot.yml
  pins use-npmrc and artifacts internally at @release/25.2; without re-pinning
  those the canary would have passed for the wrong reason. Handled with a
  throwaway canary branch, since deleted.
- The job is label-gated on 'snapshot' and the trigger omits `opened`, so
  opening a PR fires nothing.

Also records the local before/after npm ci reproduction (E404 with the path
prefix dropped, vs. added 542 packages) and that no artifact was published,
because a workflow-file-only change leaves no nx project affected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Kaltenbach
Kaltenbach marked this pull request as ready for review August 11, 2026 12:17
@Kaltenbach

Copy link
Copy Markdown
Author

Validation complete — ready for review

1. Local reproduction — controlled experiment

Same machine, same ~/.npmrc, lockfile as the only variable:

lockfile npm ci
pre-fix (171 npmjs URLs) npm error code E404GET https://cplace.jfrog.io/unicode-property-aliases-ecmascript/-/…tgz (the /artifactory/api/npm/cplace-npm prefix dropped)
normalized (0 npmjs URLs) added 542 packages, and audited 543 packages in 5s, exit 0

2. Consumer canary — cplace-paw-fe#184 (throwaway, now closed)

cplace-remote-filesystem-fe couldn't be used — its release branches don't go back to 25.2. Ran fe-pr-snapshot in cplace-paw-fe instead; run 31487742492 green, zero E404:

Run cd "$GITHUB_ACTION_PATH/../../.." && pwd && npm ci
added 542 packages, and audited 543 packages in 14s
Run npx ts-node "$GITHUB_ACTION_PATH/../../../tools/scripts/artifacts/main.ts"

542/543 is this repo's lockfile entry count, matching the local run — so the tree under test is what actually got installed. main.ts then ran on top, proving node_modules was functional, not merely populated.

⚠️ Worth knowing for the remaining canaries: re-pinning the consumer's uses: alone tests the wrong lockfile. The composite runs cd "$GITHUB_ACTION_PATH/../../.." && npm ci, so the ref on the composite decides which lockfile is installed — and fe-pr-snapshot.yml pins use-npmrc/artifacts internally at @release/25.2. A naive canary would have installed the un-normalized lockfile and passed for the wrong reason. A throwaway branch with the internal refs re-pinned was used, then deleted.

No artifact was published (a workflow-file-only change leaves no nx project affected), confirmed via jf rt search. Not claimed as a publish test — npm publish never depended on lockfile resolution.

3. Blocker found and fixed: repository Actions policy

The first run of the new guard ended in startup_failure at 0s with no jobs and no annotations anywhere in the API — which reads like a broken workflow file. It wasn't: the repo's Actions policy was allowed_actions=local_only ("Allow collaborationFactory actions and reusable workflows"), which permits only owner-owned actions, so actions/checkout — owned by the actions org — was rejected before any job started.

Now selected with github_owned_allowed=true, verified_allowed=false (actions/* only, narrower than an unrestricted "all"). Measured blast radius: zero — all 91 workflow files across the seven branches are workflow_call-only and never self-execute, and reusable-workflow runs are governed by the caller's policy, so consumers were never affected.

What to focus review on

This is the only PR of the seven with novel content. The other six will carry byte-identical tooling plus the same machine-checked lockfile transformation, so this review is the one that counts.

  • tools/scripts/lockfile/lib.sh — the hard-coded proxy constant and the single tarball-path regex are load-bearing; the constant is deliberately not configurable, because a caller-supplied prefix would make the exactness assertion validate its own typo.
  • check-lockfile.sh — two assertions, both required. Neither subsumes the other, and check-lockfile.bats asserts each drift class fails via the correct one.
  • pr-checks.yml — deliberately not fe--prefixed, since that prefix here means "reusable workflow consumed by other repos".

Known limitation, stated up front: this guard reports but cannot block until the Rule Sets follow-up lands. release/25.2 has no branch protection at all today.

…ot merge

The six remaining branches are technically independent of #163's merge - the
tooling is copied from an already-pushed commit, each branch normalizes its own
pre-fix lockfile, and each PR's guard runs on itself.

The binding constraint is byte-identity: since the tooling files must be
identical across all seven branches, any change from #163's review would have
to be force-pushed to seven PRs. Approval, not merge, is what makes the content
safe to replicate.

Also records the measured review setup: release/25.2 has no branch protection,
only master requires an approval, zero rulesets, no CODEOWNERS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR introduces a bash+jq toolchain to normalize package-lock.json resolved URLs onto the cplace JFrog npm proxy, adds a PR workflow guard to detect drift/reintroduction, and updates the lockfile accordingly to prevent consumer-runner npm ci failures caused by ~/.npmrc registry rewriting.

Changes:

  • Add lockfile normalization + invariant-check scripts (jq fingerprint + two-assertion checker) with bats test coverage.
  • Add a new on: pull_request workflow (pr-checks.yml) to run the lockfile invariant check plus shellcheck/bats.
  • Normalize package-lock.json resolved entries and document the approach in tools/scripts/lockfile/README.md and specs/….

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tools/scripts/lockfile/lib.sh Shared constants/helpers for normalization/checking (proxy constant, regexes, jq helpers).
tools/scripts/lockfile/fingerprint.jq Registry-independent fingerprint transform for graph invariance comparisons.
tools/scripts/lockfile/normalize-lockfile.sh Rewrites resolved URLs onto the proxy while self-asserting graph invariance.
tools/scripts/lockfile/check-lockfile.sh Baseline-vs-candidate invariant checker (graph invariance + prefix exactness).
tools/scripts/lockfile/test-helper.bash Shared bats fixture builders and mutation helper for drift injection.
tools/scripts/lockfile/normalize-lockfile.bats Behavioural tests for the normalizer script.
tools/scripts/lockfile/check-lockfile.bats Behavioural tests for the invariant checker, including drift-class coverage.
tools/scripts/lockfile/README.md Operational documentation: rollout, conflict resolution, interpreting guard failures.
.github/workflows/pr-checks.yml New PR workflow to run lockfile invariant + shell script lint/tests.
.prettierignore Excludes package-lock.json from prettier checks.
specs/2026-08-10_normalize-package-lock-resolved-urls/design.md Design doc capturing constraints/decisions for the solution.
specs/2026-08-10_normalize-package-lock-resolved-urls/plan.md Implementation plan and rollout steps across branches.
specs/2026-08-10_normalize-package-lock-resolved-urls/research.md Research evidence backing the approach and constraints.
package-lock.json Normalized resolved URL prefixes to the JFrog npm proxy.
Suppressed comments (2)

tools/scripts/lockfile/check-lockfile.sh:151

  • mktemp without a template is not portable (it fails on macOS/BSD). Use a portable template path for the baseline temp file.
  baseline_file="$(mktemp)"

tools/scripts/lockfile/lib.sh:36

  • RESOLVED_URL_RE is intended to validate standard npm tarball URLs (as described in the error message), but it currently allows any trailing filename. Consider requiring \.tgz to align validation with the documented contract.
readonly RESOLVED_URL_RE='^https?://[^/]+/.*(?:@[^/]+/)?[^/]+/-/[^/]+$'

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/scripts/lockfile/normalize-lockfile.bats Outdated
Comment thread tools/scripts/lockfile/normalize-lockfile.sh
Comment thread tools/scripts/lockfile/check-lockfile.sh Outdated
Comment thread tools/scripts/lockfile/test-helper.bash
Comment thread tools/scripts/lockfile/lib.sh Outdated
@senthanal
senthanal self-requested a review August 11, 2026 12:52
… the xxd dependency

Two fixes from Copilot's review of PR #163:

- TARBALL_PATH_RE and RESOLVED_URL_RE now require a `.tgz` suffix instead of
  accepting any filename, so the regexes match what the comments and error
  messages promise. All 542 entries are .tgz today, the fingerprint of the real
  lockfile is unchanged (223e40e6ff642ff3), and a new bats case proves a .zip
  tarball URL now fails by package path rather than being silently rehosted.

- The trailing-newline test used `xxd`, which ships with vim-common rather than
  coreutils. The workflow installs only bats and shellcheck, so that was an
  undeclared dependency on the runner image - and it contradicted this plan's
  own criterion that nothing may rely on the image happening to ship a tool.
  Replaced with `od`.

bats 25/25, shellcheck clean, check-lockfile.sh still green against HEAD~2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Kaltenbach Kaltenbach added PR: In Review The PR is currently being reviewed and removed PR: Needs Review labels Aug 11, 2026
@Kaltenbach
Kaltenbach requested a review from Benno42 August 11, 2026 13:30
… review

Measured, without touching any branch:

- No drift since research. All seven branches still show 171 npmjs entries and
  the three expected lockfile states; the last commit touching any lockfile is
  from 2025, so there is no Dependabot churn to account for.
- The normalizer dry-runs cleanly against all six remaining branches. The +4788
  delta holds across every state (it is 171 x 28, independent of entry count):
  262061 -> 266849 for 25.3/25.4, 270244 -> 275032 for 26.x/master. 342 changed
  lines, 0 non-resolved, idempotent in every case.
- .prettierignore is byte-identical on all seven branches, so the conflict this
  plan warned about cannot occur.
- No branch already contains tools/scripts/lockfile or pr-checks.yml, so every
  copy is a clean add.
- The canary trap recurs per state: each branch pins its composites to its own
  release, so the 25.4 and 26.x canaries each need their own throwaway branch
  with internal refs re-pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@Benno42 Benno42 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Looks good. Just two questions.

Comment thread .github/workflows/pr-checks.yml Outdated
Comment thread tools/scripts/lockfile/check-lockfile.sh Outdated
Kaltenbach and others added 3 commits August 11, 2026 16:13
…unbook and reusable PR body

Adds consumer-survey.md: 299 repo/branch combinations across 41 FE repos
measured for registry.npmjs.org. 8 affected branches across 3 repos, 291 clean.
cplace-loomeo-fe 22.4-23.3 are essentially un-migrated (~20500 of the 20537
total entries) and look dormant; cplace-paw-fe 25.2/25.3 (14 each) and
cplace-bayer-prompt-fe sprint-56/57 (3 each) are the residue cases the
normalizer is built for.

Two findings that change 34454s scope: paw-fe release/25.2 and 25.3 look
latently broken for the same reason at the workflow-level npm ci, and four
consumer repos carry local-path resolved values that assert_resolvable would
hard-fail on, so the tooling cannot be pointed at them unmodified.

Also records the per-state canary runbook (both Phase 6 traps) and a reusable
PR description for the six replication PRs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and 25.3 are broken, not latent

The survey flagged 14 npmjs entries on each branch as a possible masked
failure. Verified it is a real one:

- npm ci on release/25.2's real lockfile with the cplace ~/.npmrc and a cold
  cache fails: E404 GET https://cplace.jfrog.io/readdirp/-/readdirp-3.6.0.tgz
- cplace-paw-fe has 13 caches and ZERO on release/25.2 or release/25.3
- the cache key embeds hashFiles('**/package-lock.json'), so a cross-branch hit
  is impossible by construction - a different lockfile is a different key
- no restore-keys in fe-install-deps, fe-pr-snapshot or fe-licenses, so only an
  exact match sets cache-hit=true and nothing can skip Install modules

Per GitHub's documented scoping a PR run CAN restore the base branch's cache,
so "a feature branch gets no cache" is not what makes this fail - the absent
cache and the content-addressed key are. Recorded because the base-branch rule
is the one people forget.

Any PR into those two branches reaching fe-install-deps, fe-licenses,
fe-pr-snapshot or fe-pr-close fails at Install modules with a masked E404. It
is unobserved only because no recent PR has targeted them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gistry-host, and inventory what still needs fixing

Normalizing seven branches fixes this repo's lockfile, but it does not help
consumer repositories, and cplace-paw-fe release/25.2 and 25.3 are broken today
for exactly the same reason (verified: cold npm ci fails E404, no cache exists
on those branches, and the cache key is content-addressed so no cross-branch
hit is possible).

use-npmrc now appends `replace-registry-host=never` to the ~/.npmrc it writes.
npm then fetches each `resolved` URL verbatim instead of rewriting its host and
dropping the registry path prefix. Measured against real lockfiles with the
real secret:

  github-actions, un-normalized  -> added 542 packages
  cplace-paw-fe release/25.2     -> added 2576 packages

JFrog URLs stay authenticated by the secret, so unlike reverting use-npmrc to a
workspace-level .npmrc this adds no dependency on anonymous JFrog access - that
alternative was measured at 730 anonymous requests per run and would collide
with PFM-ISSUE-34454.

This is a mitigation, not the fix: entries still on npmjs are fetched directly,
bypassing Xray and curation. So use-npmrc also runs warn-foreign-registry.sh
against the CONSUMER's lockfile and emits a ::warning annotation plus a job
summary naming the offending package paths. Those warnings are the inventory of
lockfiles still to normalize; when none report, the mitigation line comes out.

The two compose safely in either order - on a normalized lockfile the flag is a
no-op - so the mitigation can be removed lazily per branch.

The check is advisory by construction: missing lockfile, missing jq and invalid
JSON all exit 0 silently, and local-path `resolved` values (present in 4 of 41
FE repos) are ignored rather than reported. It must never fail someone else's
build. bats 34/34, shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Kaltenbach

Copy link
Copy Markdown
Author

Why replace-registry-host=never was added to use-npmrc

New commits since the review started. This one changes use-npmrc, so it deserves an explanation rather than being discovered in the diff.

The problem this PR originally solved was only half of it

Normalizing this repo's package-lock.json fixes the composite surface: npm ci running inside the action's own checkout, outside the workspace. It does nothing for the consumer surface — a consumer's own npm ci, running in the workspace against its own lockfile.

surface where npm ci runs whose lockfile fixed by
composite the action's checkout this repo's normalization (this PR)
consumer the workspace the consumer's nothing, until now

That mattered more than expected. A survey of 299 repo/branch combinations across 41 FE repos found 8 affected branches — and cplace-paw-fe release/25.2 and release/25.3 are broken right now, each with 14 registry.npmjs.org entries. Reproduced with a cold cache and the real ~/.npmrc:

npm error code E404
npm error 404 Not Found - GET https://cplace.jfrog.io/readdirp/-/readdirp-3.6.0.tgz

Nothing masks it: that repo has zero caches on those branches, and the cache key is hashFiles('**/package-lock.json') — content-addressed, so a cross-branch cache hit is impossible by construction. It is unobserved only because no recent PR has targeted those branches.

What the one line does

echo 'replace-registry-host=never' >> ~/.npmrc

npm's default (replace-registry-host=npmjs) rewrites a lockfile's npmjs URLs onto the configured registry and drops that registry's path prefix — the bug. never makes npm fetch each resolved URL verbatim. Measured against real lockfiles with the real secret:

github-actions, un-normalized added 542 packages
cplace-paw-fe release/25.2 added 2576 packages

JFrog URLs stay authenticated by the secret, so this adds no dependency on anonymous JFrog access.

Why not just revert use-npmrc to a workspace-level .npmrc

That was the first instinct, and it was measured rather than argued:

  • It does not fix the consumer surface. A consumer's npm ci runs in the workspace, where that file lives — so cplace-paw-fe stays broken.
  • It costs 730 anonymous JFrog requests per run, creating exactly the dependency PFM-ISSUE-34454 exists to remove. If the anonymous shutdown landed while that revert was in place, every composite would break — worse than today.

This is a mitigation, and it inventories its own removal

Under it, entries still on npmjs are fetched directly from npmjs, bypassing Xray and curation. So use-npmrc also runs warn-foreign-registry.sh against the consumer's lockfile:

⚠️ 14 resolved entries in package-lock.json point somewhere other than the cplace npm proxy… First 10 affected packages

Those warnings are the removal criteria. When no pipeline reports one, the line comes out — by then it is already a no-op, since a normalized lockfile has no foreign URLs left to rewrite. The two compose safely in either order, so removal is lazy and per-branch, not a coordinated switchover. Tracked on PFM-ISSUE-34454, which now has a dedicated section for it.

The check is advisory by construction — it runs in every consumer's pipeline, so missing lockfile, missing jq and invalid JSON all exit 0 silently, and local-path resolved values (found in 4 of 41 FE repos) are ignored rather than reported as false positives. Four of the 34 bats tests exist solely to pin that property.

One consequence worth flagging for PFM-ISSUE-34454: a clean JFrog access log no longer proves everything resolves through the proxy while this is in place, because npmjs-direct requests never reach JFrog at all.

design.md gains Dimension 9 and plan.md gains Phase 4b/6b covering all of this.

@Kaltenbach

Copy link
Copy Markdown
Author

Mitigation canary: green on the branch that is broken today

cplace-paw-fe#185, run 31509557594 — a draft PR against cplace-paw-fe release/25.2, chosen deliberately because that branch carries 14 registry.npmjs.org entries in its own lockfile and fails npm ci today.

Result

echo 'replace-registry-host=never' >> ~/.npmrc
##[warning]14 entries in package-lock.json do not resolve via the cplace npm proxy.
          They install today only because use-npmrc sets replace-registry-host=never.
          Normalize this lockfile - see PFM-ISSUE-34453.
added 2439 packages, and audited 2440 packages in 8m
step outcome
Use .npmrc success
Install modules success — this is the step that returns E404 without the mitigation

Two details that make this a real test rather than a lucky one:

  • The install took 8 minutes, i.e. it ran genuinely cold. Nothing was restored from cache — which is exactly the condition under which this branch fails. Had a cache been restored, Install modules would have been skipped and the run would have proved nothing.
  • The warning independently counted 14, matching the survey's count for that branch, and surfaced in the run UI without failing the job — the advisory contract holding under real conditions.

Why the internal re-pin was necessary

The canary branch re-pins use-npmrc/artifacts/snapshots internally to itself. The mitigation lives in use-npmrc, and fe-install-deps.yml pins use-npmrc@release/25.2. Re-pinning only the consumer's uses: would have loaded the old use-npmrc and tested nothing — the same trap as the Phase 6 canary, one level deeper.

What this does and does not prove

Proven: the mitigation fixes the consumer surface on a real runner, on a branch that genuinely fails without it.

Not claimed: the rest of paw-fe's pipeline (build, e2e, code-quality) is still running and is not the subject — those exercise paw-fe's own code, not this change. install-deps was the whole test, and it is green.

Canary PR and both throwaway branches will be deleted.

Kaltenbach and others added 4 commits August 11, 2026 18:07
…design and plan

design.md gains Dimension 9. It records that Dimension 8 declared consumer-repo
lockfiles out of scope on a false premise - those pipelines are not "currently
green": cplace-paw-fe release/25.2 and 25.3 are broken today. That splits the
problem into two failure surfaces the design had treated as one, and only the
mitigation covers the consumer side.

Also records why reverting use-npmrc to a workspace-level .npmrc was rejected
on measurement (fixes only the composite surface; 730 anonymous JFrog requests
per run), and why the flag is applied unconditionally rather than conditionally
(it is a no-op on a clean lockfile, so the warning carries the signal instead).

plan.md gains Phase 4b (the mitigation, with its measured evidence) and Phase 6b
(a consumer-surface canary against paw-fe release/25.2, where the internal
use-npmrc re-pin is what makes the test valid at all).

PFM-ISSUE-34454 has been updated with a dedicated section owning the removal:
removal criteria, the survey blast radius, the local-path finding, and a warning
that a clean JFrog access log no longer proves proxy-only resolution while the
mitigation is in place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
paw-fe#185 against cplace-paw-fe release/25.2 - the branch carrying 14
registry.npmjs.org entries that fails npm ci today. install-deps SUCCEEDS with
the mitigation:

  echo 'replace-registry-host=never' >> ~/.npmrc
  ##[warning]14 entries in package-lock.json do not resolve via the cplace npm proxy...
  added 2439 packages, and audited 2440 packages in 8m
  Install modules -> success

The 8m install time confirms a genuinely cold cache - nothing was restored,
which is exactly the condition under which this branch fails. Had a cache been
restored, Install modules would have been skipped and the run would have proved
nothing.

The advisory warning independently counted 14, matching the survey, and
surfaced in the run UI without failing the job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d record a near-miss

The mitigation canary went green across the whole paw-fe pipeline (15 jobs), not
just install-deps, on a branch that cannot install without it. PR and both
throwaway branches deleted.

Also records a process near-miss: the two documentation commits for Dimension 9
and Phase 4b/6b were made while still standing on the throwaway canary branch,
pushed to its remote, and then orphaned when that branch was deleted local and
remote. Recovered by cherry-picking the SHAs out of the reflog. Noted so the
next canary does not repeat it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#163

Both raised by @Benno42, both confirmed by reproduction.

1. The PR guard blocked every legitimate dependency change.

   check-lockfile.sh compared the whole dependency graph against the base
   branch, so a PR that added or updated a package failed even when its
   lockfile was perfectly normalized - and the failure then advised running the
   normalizer, which cannot fix a graph difference. Reproduced: adding one
   entry correctly on the proxy, with zero npmjs URLs, exits 1.

   Graph invariance is the right assertion for verifying a NORMALIZATION commit
   ("prove I changed only prefixes"), and the wrong one for everyday pull
   requests. The ticket's actual criterion is prefix exactness alone.

   Adds --prefix-only and switches pr-checks.yml to it. Since that needs no
   baseline, the guard no longer needs git or fetch-depth: 0 either. The
   graph-failure message no longer suggests the normalizer and points at
   --prefix-only instead.

2. Root-package drift was silently ignored.

   Investigating the second comment - whether the graph comparison should skip
   the "" root key like assert_resolvable does - showed the opposite problem:
   it was ALREADY being skipped by accident. The root key is the empty string,
   so when it was the only drifted entry jq emitted a blank line and
   `[[ -n "${drift}" ]]` read that as "no drift". Changes to this project's own
   declared dependencies passed silently. Now rendered as "<root package>".

   The root must NOT be filtered: unlike assert_resolvable, where the root has
   no `resolved` by construction and would be a guaranteed false positive, the
   root entry holds the declared dependencies and a change there is exactly the
   drift this assertion exists to catch.

bats 42/42, shellcheck clean, actionlint clean. The Phase 5 normalization
verification flow (--baseline HEAD~1) is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread .github/actions/use-npmrc/action.yml Outdated
@Kaltenbach
Kaltenbach requested review from Benno42 and a balanced review from Copilot August 11, 2026 16:51

Copilot AI 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.

Pull request overview

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

specs/2026-08-10_normalize-package-lock-resolved-urls/design.md:537

  • This integration statement is false in the current diff: use-npmrc/action.yml is modified to append replace-registry-host=never and run the advisory scanner. Update the architecture summary so the material consumer-runtime mitigation is not documented as out of scope.
- `.github/actions/use-npmrc/action.yml` is **not modified** — that is PFM-ISSUE-34454's territory.

tools/scripts/lockfile/check-lockfile.sh:75

  • The union comparison treats a missing top-level key and an explicitly present null key as equal because both $A[$key] and $B[$key] evaluate to null. As a result, adding or removing null-valued top-level metadata passes the claimed whole-document graph invariant. Compare key presence with has as well as values.
    | [ ((($A | keys) + ($B | keys)) | unique)[]
        | select(. != "packages")
        | select(($A[.] | tojson) != ($B[.] | tojson))
        | "top-level key: " + . ]

tools/scripts/lockfile/README.md:125

  • This guard description contradicts the mode table immediately above and the workflow: pr-checks.yml now invokes --prefix-only, so graph invariance is not run on pull requests. Rewrite this section to distinguish the PR guard's single prefix assertion from the two assertions used by baseline verification; otherwise readers may incorrectly assume ordinary PRs are checked for graph drift.
`.github/workflows/pr-checks.yml` runs `check-lockfile.sh` on every pull request. It makes **two independent
assertions, both of which must pass**:

specs/2026-08-10_normalize-package-lock-resolved-urls/design.md:529

  • The implemented PR guard no longer compares against the base commit; it runs check-lockfile.sh --prefix-only so legitimate dependency changes are allowed. This architecture summary still documents the superseded behavior.

This issue also appears on line 537 of the same file.

**Guard (every PR):** `pr-checks.yml` → `lockfile` job runs `check-lockfile.sh` against the base commit → on failure,
prints offending package paths plus the remediation command; `scripts` job runs shellcheck + bats.

Comment thread .github/actions/use-npmrc/action.yml
Comment thread tools/scripts/lockfile/check-lockfile.sh Outdated
Comment thread tools/scripts/lockfile/check-lockfile.sh
Comment thread tools/scripts/lockfile/check-lockfile.sh
Comment thread tools/scripts/lockfile/check-lockfile.sh
Comment thread .github/actions/use-npmrc/action.yml Outdated
Comment thread tools/scripts/lockfile/check-lockfile.sh Outdated
Comment thread tools/scripts/lockfile/lib.sh Outdated
Comment thread tools/scripts/lockfile/lib.sh Outdated
Comment thread tools/scripts/lockfile/check-lockfile.sh
Comment thread tools/scripts/lockfile/README.md Outdated
Comment thread tools/scripts/lockfile/check-lockfile.sh Outdated
Kaltenbach and others added 2 commits August 12, 2026 10:59
…iew round

All ten raised by @senthanal (nine) and @Copilot (one) reproduced before being
fixed. Four of them FAILED OPEN - the check reported success on a lockfile it
had not actually verified, which is worse than any false positive.

Failing open:

- assert_graph_invariant reported PASS whenever its own jq failed. main() calls
  it inside a `||` list, which disables `set -e` for the function body, so an
  unchecked failure left `drift` empty and fell through to "PASS: dependency
  graph identical to baseline". Reproduced with fingerprint.jq moved aside and a
  poisoned integrity: PASS, exit 0. Every step is now checked and dies.
- --prefix-only silently ignored an explicitly passed --baseline, so
  `--baseline HEAD~1 --prefix-only` - the natural thing to type once the README
  calls --prefix-only "the PR guard" - answered a weaker question and exited 0
  on a poisoned integrity. The combination is now rejected.
- count_foreign_entries used a different predicate than the rewrite, so the
  normalizer reported "0 rewritten / already normalized" while silently
  rewriting the file and dropping a path segment. README Flow 1 makes that count
  the documented verification signal. Both now use one shared predicate.
- A v1 lockfile guarded only with `(.packages // {})` would pass vacuously,
  reporting "resolves entirely via the proxy" having examined nothing. Now
  rejected by an explicit lockfileVersion check.

Wrong answers, quietly:

- resolve_baseline hardcoded `package-lock.json`, so a nested candidate was
  compared against the wrong file - relevant because consumer-survey.md plans to
  point this tooling at the FE repos.
- A second positional silently replaced the first, so `check base.json cand.json`
  dropped the baseline, fell back to HEAD, and printed a drift report naming
  every package in the repo.
- --prefix-only hard-failed on workspace and link: entries that the mode
  promises to allow, and that warn-foreign-registry.sh already tolerates.
- "FAIL: 1 distinct registry prefixes found (expected exactly 1)" - the rollout
  case, phrased so the check reads as broken.

Robustness and docs:

- use-npmrc invoked the advisory script as a bare unquoted path, so anything
  preventing it from being reached (exec bit, spaces, a Windows action_path)
  failed the consumer's job at exit 127 - the one thing the script's own
  "never fail a build" contract cannot cover. Now `bash "<path>" || true`.
- README Flow 3 still described the two-assertion guard, so a reader would
  conclude CI proves integrity hashes were not poisoned. It does not.

bats 51/51, shellcheck clean. Real lockfile still reports 171 / +4788 and stays
idempotent; both CI and normalization-verification flows verified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fail closed

Raised by @Benno42. Of the three locations named, two were already fixed in
7fee8d4; this one was still live and is the most serious instance of the class,
because it does not merely report a false PASS - it WRITES the lockfile.

    if ! diff -q <(fingerprint_of "${lockfile}") <(fingerprint_of "${tmp}")

`if !` disables set -e for the condition, so when fingerprint_of failed both
process substitutions produced empty output, diff called them identical, the
self-assertion "passed", and the rewritten file was copied over the original.
Reproduced with fingerprint.jq moved aside: exit 0, "normalized", and the
lockfile overwritten with its only verification step silently skipped.

Both fingerprints are now materialised and checked before the comparison, and
the jq rewrite itself is guarded. Every failure path says "left untouched" and
leaves the file byte-identical.

bats 52/52, shellcheck clean; the real lockfile still reports 171 / +4788 and
stays idempotent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@Benno42 Benno42 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 LGTM

Kaltenbach and others added 8 commits August 12, 2026 16:22
…solve the use-npmrc contradictions

Checkboxes. Phases 1-4 carried zero ticked criteria while 5, 6, 4b and 6b were
ticked. `git log -S` confirms Phase 1's criteria were never committed as ticked
at all, so the marks made during implementation were lost - most plausibly to
the mid-session reformat that rewrote plan.md. Restored with the evidence each
was verified against, and the four criteria that are reviewer judgement rather
than machine-checkable are labelled as such instead of ticked.

use-npmrc contradictions. Both documents still forbade exactly what Phase 4b /
Dimension 9 did:

  plan.md:116   "Modifying `use-npmrc` ..." under What We're NOT Doing
  design.md:537 "`use-npmrc/action.yml` is **not modified**"

Both are now struck through and annotated with what superseded them and why -
the exclusion assumed consumer pipelines were green, and two are not. The four
composite actions remain genuinely unmodified, so that half of each statement is
preserved rather than deleted.

Also records that master is explicitly in scope and that #163 is the seed: the
tooling set is settled here and replicated, while each PR still carries its own
branch-specific differences (lockfile content, base branch). The byte-identity
gate compares only the tooling paths, which is what keeps that distinction
honest.

Factual corrections in the same class: Phase 6b claimed the Phase 6 canary ran
on master (both canary PRs were against release/25.2); Desired End State omitted
warn-foreign-registry.sh and use-npmrc from the byte-identical set; stale suite
counts (24/24, 25/25, 34/34) annotated against the current 52; design.md Key
Components gained the advisory script and the drift-case count corrected from
t1-t4 to t1-t6; the guard data-flow now states it runs --prefix-only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n-foreign-registry

Found by the correctness reviewer. warn-foreign-registry.sh carried its own copy
of the proxy URL and its own `startswith($proxy)` predicate - the exact bug
lib.sh documents as already found and fixed once in count_foreign_entries. An
entry on the right host with a stray path segment satisfies `startswith` and was
silently reported as compliant:

  .../cplace-npm/extra/beta/-/beta-2.0.0.tgz
    advisory : SILENT
    check    : FAIL

That mattered more here than anywhere else. These warnings are the inventory
that decides when the replace-registry-host=never mitigation can be removed, so
a false negative could green-light removing it while lockfiles are still broken.

Now sources lib.sh and uses the shared JQ_REGISTRY_ENTRIES predicate, so the
advisory and the guard cannot disagree - which also removes the duplicated
proxy constant the craft reviewer flagged separately. Sourcing is guarded on
both sides: an unreachable or unloadable lib.sh exits 0 silently, because the
advisory contract cannot be met by a script that dies referencing an undefined
constant under `set -u`.

Three tests added: the stray-segment case, an agreement test asserting the
advisory warns exactly when check-lockfile fails, and one pinning the silent
exit when lib.sh is hidden. bats 55/55, shellcheck clean. Missing file, invalid
JSON and a missing packages section all still exit 0 silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five reviewer agents over origin/release/25.2...HEAD. 15 findings kept, 0
dropped in verification, 0 merged. Four fixed during the review.

Two fail-open defects lead: the advisory scan reproduced the startswith($proxy)
bug already fixed in count_foreign_entries (fixed, 4a160a7), and an uppercase
URL scheme bypasses the PR guard entirely - reproduced, so that finding was
upgraded from inferred rather than downgraded.

Also records that the guard shipped as --prefix-only while the plan still
specifies --baseline base.sha with fetch-depth: 0, which needs reconciling
before the tooling is replicated to six more branches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… 1.4

Three of the eleven open findings from review iteration 1.

1.3 is the one that mattered: jq's test() is case-sensitive, so an entry
resolving to HTTPS://attacker.example.com/evil/-/evil-1.0.0.tgz was not a
registry entry at all and --prefix-only - the sole automated guard on every
pull request - reported OK, exit 0. npm reads the scheme case-insensitively,
so the scheme is now matched that way in both places it is spelled, in
JQ_REGISTRY_ENTRIES and in RESOLVED_URL_RE. The .tgz suffix stays
case-sensitive. Two tests: the guard rejects such an entry, and the normalizer
rewrites it onto the proxy - the guard's remediation advice has to stay true.

1.2: fp_base/fp_cand were cleaned up by an explicit rm -f at the end of
assert_graph_invariant, which every `|| die` between them skipped, including
the fingerprint failure path a bats test drives on every run. Both mktemp calls
move into main() beside baseline_file, under one widened EXIT trap; the
function takes the two paths as arguments.

1.4: the guard shipped as --prefix-only in 604f95e while plan.md still
specified --baseline base.sha on a fetch-depth: 0 checkout, and Phase 4 was
ticked throughout. Phase 4's block now shows what shipped, with the reason and
its four consequences. Key Discovery 6, the fetch-depth reference and
design.md's matching implication are struck through as superseded. Phase 5's CI
evidence is kept verbatim but labelled as predating the switch, since the
current workflow cannot produce those lines.

Suite is 57. shellcheck -x clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s into plan Phase 9

Mode A triage of the eight open iteration-1 findings. [1.15] is ticked in
review.md as a no-change resolution: the widened trust boundary is accepted,
documented in three places and owned by PFM-ISSUE-34454. The seven that need a
code change - [1.5], [1.9], [1.10], [1.11], [1.12], [1.13], [1.14] - become
Phase 9, so their boxes stay unchecked until it is implemented.

Phase 9 also carries an argument-by-argument audit of every jq invocation,
which found a latent defect the review missed: normalize-lockfile.sh and
fingerprint.jq transform every entry with a string `resolved`, including the
root "" key that JQ_REGISTRY_ENTRIES excludes. capture() on a non-match yields
empty rather than raising, and `.x |= empty` deletes the key, so a root entry
resolving to a non-tarball value loses `resolved` silently - and fingerprint.jq
shares the flaw, so the self-assertion cannot see it. A root entry on npmjs is
rewritten while the run reports `entries rewritten: 0`.

Phase 9 must land before Phase 7 replicates the tooling to the six remaining
branches, or every fix in it has to be made seven times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fter /-/

The 2026-08-13 consumer canary reported 18 foreign entries in cplace-paw-fe
release/25.2 where only 14 are foreign. The four extras are on the correct
proxy host but carry a tarball path the regex rejected: a repeated scope
(@cplace-next/cf-frontend-sdk/-/@cplace-next/cf-frontend-sdk-25.2.30.tgz) or an
interposed version segment (@fortawesome/fontawesome-pro/-/5.15.4/...tgz). Both
are served by JFrog and installed by npm without complaint.

The false positives are not cosmetic. The advisory's warnings are the inventory
that decides when replace-registry-host=never can be removed, so a permanent
non-zero count means it can never be removed; and assert_resolvable rejected the
same four, so normalize-lockfile.sh refused to touch that lockfile at all.

TARBALL_PATH_RE and RESOLVED_URL_RE are widened from [^/]+\.tgz to .+\.tgz, in
lockstep: everything the validator accepts must stay capturable, or an entry
passes validation and is then handed to a capture that yields empty - and
`|= empty` deletes the key rather than raising.

Measured: paw-fe now reports 14, normalizes 14 (+392 bytes) and then passes the
guard with the four odd-shaped entries untouched; this repo's guard passes and
its normalizer stays idempotent (0 entries, 0 bytes); the fingerprint is
byte-identical on all seven branches, so Phase 5's evidence stands. bats 65/65,
shellcheck clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ixes and review findings

Discharges the seven review findings needing a code change, plus the four
defects found by auditing every jq invocation.

The audit fixes:

- normalize-lockfile.sh and fingerprint.jq now carry the shared predicate's own
  terms. Both transformed every entry with a string `resolved`, including the
  root "" key that JQ_REGISTRY_ENTRIES excludes, so a root entry on npmjs was
  rewritten while the run reported "entries rewritten: 0, already normalized" -
  and a root `resolved` that is not a tarball URL lost its key outright, because
  a non-matching capture yields empty and `|= empty` deletes. fingerprint.jq
  made the same deletion on both sides, so nothing could observe it.
- lib.sh records what jq's capture actually does. The old comment claimed it
  raises on a non-match, which is what made the above read as safe.
- check-lockfile.sh version-checks the resolved baseline before fingerprinting.
  A lockfileVersion 1 baseline produced `null (null) has no keys` followed by
  "cannot fingerprint the baseline (is fingerprint.jq present?)" - a jq trace
  and a wrong diagnosis. assert_supported_lockfile takes a display name so the
  message names the ref rather than a mktemp path.
- check-lockfile.sh reports how many entries it examined and refuses to report
  compliance from an empty set. A lockfile with only link:/workspace entries
  printed "exactly 0 registry prefix, matching the expected proxy". It returns
  2 for that case, so main withholds "run the normalizer" - advice that cannot
  fix zero entries.
- An offender differing from the proxy only in scheme is now named as a
  plaintext-http downgrade rather than left reading as a path mistake.

The review findings: [1.5] a :2: merge-stage baseline test driven through a real
conflict, [1.9] $GITHUB_ACTION_PATH matching its four siblings, [1.10] the
README's self-contradicting section, [1.11] the missing-lockfileVersion arm in
both suites, [1.12] design.md and README recording that assert_resolvable does
not run in --prefix-only, [1.13] single-quoted jq fragments in place of the
escaped double-quoted ones, [1.14] plan.md annotated with the shipped regex.

bats 74/74, shellcheck clean. On the real lockfile the guard passes with 542
entries examined and the normalizer stays idempotent; the baseline form still
passes across f08ac98; the fingerprint is byte-identical on all seven branches
after the fingerprint.jq guards. All 15 review findings are now resolved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: In Review The PR is currently being reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants