Skip to content

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

Merged
Kaltenbach merged 28 commits into
release/25.2from
fix/PFM-ISSUE-34453-normalize-package-lock-json/25.2
Aug 14, 2026
Merged

PFM-ISSUE-34453 - github-actions: Normalize package-lock.json resolved URLs onto the JFrog npm proxy (release/25.2)#163
Kaltenbach merged 28 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
Contributor

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
Contributor 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
Contributor 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.

…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 9 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>
…le toolchain

specs/.../overview.md explains how the shipped result works, for a reader
meeting the toolchain for the first time: the rewrite that breaks `npm ci`, the
two-part fix (normalize the lockfile; `replace-registry-host=never` as the
interim mitigation), the two assertions and what each is blind to, the two
modes, the bats suite, and the seven-branch byte-identity rule. Four mermaid
diagrams, so GitHub renders it in place.

overview.html beside it is the same document with hand-drawn SVG figures.

It lives in the spec folder rather than tools/scripts/lockfile/ on purpose: that
directory is the byte-identical set, so a document there would have to be
replicated - and re-replicated on every edit - across all seven branches.

The README's Related section points at it. That file IS in the byte-identical
set, so the six pre-staged branches are rebuilt in the same breath and the
tooling digest is recomputed.

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

@senthanal
senthanal self-requested a review August 14, 2026 09:21

@senthanal senthanal 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
Kaltenbach merged commit 77caa33 into release/25.2 Aug 14, 2026
2 checks passed
@Kaltenbach
Kaltenbach deleted the fix/PFM-ISSUE-34453-normalize-package-lock-json/25.2 branch August 14, 2026 11:12
@Kaltenbach Kaltenbach added PR: Ready to Merge and removed PR: In Review The PR is currently being reviewed labels Aug 14, 2026
Kaltenbach added a commit that referenced this pull request Aug 14, 2026
…d URLs onto the JFrog npm proxy (release/25.3) (#165)

## What this is

Branch 2 of 7 for PFM-ISSUE-34453. Normalizes this branch's
`package-lock.json` `resolved` URLs onto the JFrog
npm proxy, and adds the same lockfile tooling and PR guard.

**The tooling is byte-identical to the seed
([#163](#163),
merged) — verified, not asserted:**

```
sha1(tools/scripts/lockfile/* + pr-checks.yml + use-npmrc/action.yml + .prettierignore) = f28f73029d4cb426
```

Same on all seven branches. So the only thing needing review here is the
lockfile commit, and that is machine-checked.
The substantive review happened on #163.

> **Note on the digest.** `release/25.2` currently reads
`4aa3bcb389875d53` because
>
[#164](#164)
— a wording-only correction to the advisory
> message and the docs — is still open. These six already carry it.
Merging #164 closes the gap; nothing else differs.

## Two commits

1. **Tooling**, copied verbatim from the seed.
2. **The normalized lockfile alone.** Its parent *is* the baseline, so
the invariant is provable by construction:
   `./tools/scripts/lockfile/check-lockfile.sh --baseline HEAD~1`.

## Evidence

- **171** entries rewritten, **+4788** bytes (262061 → 266849), **342**
changed lines, **0** outside a `"resolved"` line
- `grep -c registry.npmjs.org package-lock.json` → **0** (542 entries
total)
- `check-lockfile.sh --baseline HEAD~1` exits 0: graph identical to the
baseline, exactly one registry prefix
- bats **74/74** and shellcheck clean, both in CI via `pr-checks.yml`

## Why this matters now

The prefix-drop that motivates this is a **regression in newer npm**,
measured 2026-08-14: npm 10.2.4 — which every
pipeline pins today via node 18.19.1 — rewrites correctly and resolves
through the proxy, while npm 11.3.0 drops the
registry's path prefix and fails with an `E404` masked as `***`. So
nothing is failing in CI today, and everything
un-normalized fails the moment runners move to node 24. This is a
prerequisite for that migration, not a cleanup
after it.

Known limitation, same as #163: the guard **reports but cannot block**
until PFM-ISSUE-34465 (Rule Sets enforcement)
lands.
Kaltenbach added a commit that referenced this pull request Aug 14, 2026
…d URLs onto the JFrog npm proxy (release/25.4) (#166)

## What this is

Branch 3 of 7 for PFM-ISSUE-34453. Normalizes this branch's
`package-lock.json` `resolved` URLs onto the JFrog
npm proxy, and adds the same lockfile tooling and PR guard.

**The tooling is byte-identical to the seed
([#163](#163),
merged) — verified, not asserted:**

```
sha1(tools/scripts/lockfile/* + pr-checks.yml + use-npmrc/action.yml + .prettierignore) = f28f73029d4cb426
```

Same on all seven branches. So the only thing needing review here is the
lockfile commit, and that is machine-checked.
The substantive review happened on #163.

> **Note on the digest.** `release/25.2` currently reads
`4aa3bcb389875d53` because
>
[#164](#164)
— a wording-only correction to the advisory
> message and the docs — is still open. These six already carry it.
Merging #164 closes the gap; nothing else differs.

## Two commits

1. **Tooling**, copied verbatim from the seed.
2. **The normalized lockfile alone.** Its parent *is* the baseline, so
the invariant is provable by construction:
   `./tools/scripts/lockfile/check-lockfile.sh --baseline HEAD~1`.

## Evidence

- **171** entries rewritten, **+4788** bytes (262061 → 266849), **342**
changed lines, **0** outside a `"resolved"` line
- `grep -c registry.npmjs.org package-lock.json` → **0** (542 entries
total)
- `check-lockfile.sh --baseline HEAD~1` exits 0: graph identical to the
baseline, exactly one registry prefix
- bats **74/74** and shellcheck clean, both in CI via `pr-checks.yml`

## Why this matters now

The prefix-drop that motivates this is a **regression in newer npm**,
measured 2026-08-14: npm 10.2.4 — which every
pipeline pins today via node 18.19.1 — rewrites correctly and resolves
through the proxy, while npm 11.3.0 drops the
registry's path prefix and fails with an `E404` masked as `***`. So
nothing is failing in CI today, and everything
un-normalized fails the moment runners move to node 24. This is a
prerequisite for that migration, not a cleanup
after it.

Known limitation, same as #163: the guard **reports but cannot block**
until PFM-ISSUE-34465 (Rule Sets enforcement)
lands.
Kaltenbach added a commit that referenced this pull request Aug 14, 2026
…d URLs onto the JFrog npm proxy (release/26.1) (#167)

## What this is

Branch 4 of 7 for PFM-ISSUE-34453. Normalizes this branch's
`package-lock.json` `resolved` URLs onto the JFrog
npm proxy, and adds the same lockfile tooling and PR guard.

**The tooling is byte-identical to the seed
([#163](#163),
merged) — verified, not asserted:**

```
sha1(tools/scripts/lockfile/* + pr-checks.yml + use-npmrc/action.yml + .prettierignore) = f28f73029d4cb426
```

Same on all seven branches. So the only thing needing review here is the
lockfile commit, and that is machine-checked.
The substantive review happened on #163.

> **Note on the digest.** `release/25.2` currently reads
`4aa3bcb389875d53` because
>
[#164](#164)
— a wording-only correction to the advisory
> message and the docs — is still open. These six already carry it.
Merging #164 closes the gap; nothing else differs.

## Two commits

1. **Tooling**, copied verbatim from the seed.
2. **The normalized lockfile alone.** Its parent *is* the baseline, so
the invariant is provable by construction:
   `./tools/scripts/lockfile/check-lockfile.sh --baseline HEAD~1`.

## Evidence

- **171** entries rewritten, **+4788** bytes (270244 → 275032), **342**
changed lines, **0** outside a `"resolved"` line
- `grep -c registry.npmjs.org package-lock.json` → **0** (558 entries
total)
- `check-lockfile.sh --baseline HEAD~1` exits 0: graph identical to the
baseline, exactly one registry prefix
- bats **74/74** and shellcheck clean, both in CI via `pr-checks.yml`

## Why this matters now

The prefix-drop that motivates this is a **regression in newer npm**,
measured 2026-08-14: npm 10.2.4 — which every
pipeline pins today via node 18.19.1 — rewrites correctly and resolves
through the proxy, while npm 11.3.0 drops the
registry's path prefix and fails with an `E404` masked as `***`. So
nothing is failing in CI today, and everything
un-normalized fails the moment runners move to node 24. This is a
prerequisite for that migration, not a cleanup
after it.

Known limitation, same as #163: the guard **reports but cannot block**
until PFM-ISSUE-34465 (Rule Sets enforcement)
lands.
Kaltenbach added a commit that referenced this pull request Aug 14, 2026
…d URLs onto the JFrog npm proxy (release/26.2) (#168)

## What this is

Branch 5 of 7 for PFM-ISSUE-34453. Normalizes this branch's
`package-lock.json` `resolved` URLs onto the JFrog
npm proxy, and adds the same lockfile tooling and PR guard.

**The tooling is byte-identical to the seed
([#163](#163),
merged) — verified, not asserted:**

```
sha1(tools/scripts/lockfile/* + pr-checks.yml + use-npmrc/action.yml + .prettierignore) = f28f73029d4cb426
```

Same on all seven branches. So the only thing needing review here is the
lockfile commit, and that is machine-checked.
The substantive review happened on #163.

> **Note on the digest.** `release/25.2` currently reads
`4aa3bcb389875d53` because
>
[#164](#164)
— a wording-only correction to the advisory
> message and the docs — is still open. These six already carry it.
Merging #164 closes the gap; nothing else differs.

## Two commits

1. **Tooling**, copied verbatim from the seed.
2. **The normalized lockfile alone.** Its parent *is* the baseline, so
the invariant is provable by construction:
   `./tools/scripts/lockfile/check-lockfile.sh --baseline HEAD~1`.

## Evidence

- **171** entries rewritten, **+4788** bytes (270244 → 275032), **342**
changed lines, **0** outside a `"resolved"` line
- `grep -c registry.npmjs.org package-lock.json` → **0** (558 entries
total)
- `check-lockfile.sh --baseline HEAD~1` exits 0: graph identical to the
baseline, exactly one registry prefix
- bats **74/74** and shellcheck clean, both in CI via `pr-checks.yml`

## Why this matters now

The prefix-drop that motivates this is a **regression in newer npm**,
measured 2026-08-14: npm 10.2.4 — which every
pipeline pins today via node 18.19.1 — rewrites correctly and resolves
through the proxy, while npm 11.3.0 drops the
registry's path prefix and fails with an `E404` masked as `***`. So
nothing is failing in CI today, and everything
un-normalized fails the moment runners move to node 24. This is a
prerequisite for that migration, not a cleanup
after it.

Known limitation, same as #163: the guard **reports but cannot block**
until PFM-ISSUE-34465 (Rule Sets enforcement)
lands.
Kaltenbach added a commit that referenced this pull request Aug 14, 2026
…d URLs onto the JFrog npm proxy (release/26.3) (#169)

## What this is

Branch 6 of 7 for PFM-ISSUE-34453. Normalizes this branch's
`package-lock.json` `resolved` URLs onto the JFrog
npm proxy, and adds the same lockfile tooling and PR guard.

**The tooling is byte-identical to the seed
([#163](#163),
merged) — verified, not asserted:**

```
sha1(tools/scripts/lockfile/* + pr-checks.yml + use-npmrc/action.yml + .prettierignore) = f28f73029d4cb426
```

Same on all seven branches. So the only thing needing review here is the
lockfile commit, and that is machine-checked.
The substantive review happened on #163.

> **Note on the digest.** `release/25.2` currently reads
`4aa3bcb389875d53` because
>
[#164](#164)
— a wording-only correction to the advisory
> message and the docs — is still open. These six already carry it.
Merging #164 closes the gap; nothing else differs.

## Two commits

1. **Tooling**, copied verbatim from the seed.
2. **The normalized lockfile alone.** Its parent *is* the baseline, so
the invariant is provable by construction:
   `./tools/scripts/lockfile/check-lockfile.sh --baseline HEAD~1`.

## Evidence

- **171** entries rewritten, **+4788** bytes (270244 → 275032), **342**
changed lines, **0** outside a `"resolved"` line
- `grep -c registry.npmjs.org package-lock.json` → **0** (558 entries
total)
- `check-lockfile.sh --baseline HEAD~1` exits 0: graph identical to the
baseline, exactly one registry prefix
- bats **74/74** and shellcheck clean, both in CI via `pr-checks.yml`

## Why this matters now

The prefix-drop that motivates this is a **regression in newer npm**,
measured 2026-08-14: npm 10.2.4 — which every
pipeline pins today via node 18.19.1 — rewrites correctly and resolves
through the proxy, while npm 11.3.0 drops the
registry's path prefix and fails with an `E404` masked as `***`. So
nothing is failing in CI today, and everything
un-normalized fails the moment runners move to node 24. This is a
prerequisite for that migration, not a cleanup
after it.

Known limitation, same as #163: the guard **reports but cannot block**
until PFM-ISSUE-34465 (Rule Sets enforcement)
lands.
Kaltenbach added a commit that referenced this pull request Aug 14, 2026
…d URLs onto the JFrog npm proxy (master) (#170)

## What this is

Branch 7 of 7 for PFM-ISSUE-34453. Normalizes this branch's
`package-lock.json` `resolved` URLs onto the JFrog
npm proxy, and adds the same lockfile tooling and PR guard.

**The tooling is byte-identical to the seed
([#163](#163),
merged) — verified, not asserted:**

```
sha1(tools/scripts/lockfile/* + pr-checks.yml + use-npmrc/action.yml + .prettierignore) = f28f73029d4cb426
```

Same on all seven branches. So the only thing needing review here is the
lockfile commit, and that is machine-checked.
The substantive review happened on #163.

> **Note on the digest.** `release/25.2` currently reads
`4aa3bcb389875d53` because
>
[#164](#164)
— a wording-only correction to the advisory
> message and the docs — is still open. These six already carry it.
Merging #164 closes the gap; nothing else differs.

## Two commits

1. **Tooling**, copied verbatim from the seed.
2. **The normalized lockfile alone.** Its parent *is* the baseline, so
the invariant is provable by construction:
   `./tools/scripts/lockfile/check-lockfile.sh --baseline HEAD~1`.

## Evidence

- **171** entries rewritten, **+4788** bytes (270244 → 275032), **342**
changed lines, **0** outside a `"resolved"` line
- `grep -c registry.npmjs.org package-lock.json` → **0** (558 entries
total)
- `check-lockfile.sh --baseline HEAD~1` exits 0: graph identical to the
baseline, exactly one registry prefix
- bats **74/74** and shellcheck clean, both in CI via `pr-checks.yml`

## Why this matters now

The prefix-drop that motivates this is a **regression in newer npm**,
measured 2026-08-14: npm 10.2.4 — which every
pipeline pins today via node 18.19.1 — rewrites correctly and resolves
through the proxy, while npm 11.3.0 drops the
registry's path prefix and fails with an `E404` masked as `***`. So
nothing is failing in CI today, and everything
un-normalized fails the moment runners move to node 24. This is a
prerequisite for that migration, not a cleanup
after it.

Known limitation, same as #163: the guard **reports but cannot block**
until PFM-ISSUE-34465 (Rule Sets enforcement)
lands.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants