Skip to content

Commit 95e818e

Browse files
madfam-ioaldoruizlunaclaude
authored
chore(ui,react-sdk): declare React 19 support in the peer range (#469)
* chore(ui,react-sdk): declare React 19 support in the peer range WHY THIS MATTERS OUTSIDE JANUA. `@janua/ui` and `@janua/react-sdk` declare `peerDependencies: react ^18.0.0`. Tezca's web app is on react@19.2.3, so its `package-lock.json` can only be produced with --legacy-peer-deps or --force. That kind of lockfile carries entries with no `resolved` and no `integrity` — tezca has 13 of them under apps/web/node_modules — and npm's audit service REJECTS such a tree outright: npm error audit endpoint returned an error { statusCode: 400, message: 'Invalid package tree, run npm install to rebuild your package-lock.json' } So tezca's `npm audit --production --audit-level=high` CI step has not been scanning anything. It has been erroring, and the error surfaces as a red CI job that looks like a flaky registry call. Reproduced locally on 2026-07-26 against tezca main; a clean `npm install --package-lock-only` there fails with While resolving: web@0.1.0 Found: react@19.2.3 Could not resolve dependency: peer react@"^18.0.0" from @janua/ui@0.1.4 A stale peer range in a published package is not a cosmetic inaccuracy. It propagates into consumers' lockfiles and silently disables their dependency scanning. VERIFIED, NOT ASSUMED. A peer range is a compatibility claim to every consumer, so this was measured rather than reasoned about: - Installed react@19.2.4 + @types/react@19 into packages/ui and ran `tsc --noEmit` across all 124 source files / ~31k LOC, then downgraded to react@18.3.1 in the same tree and re-ran. - DIFFERENTIAL RESULT: 688 errors under React 19, 690 under React 18, and the set difference in the React-19-only direction is EMPTY. Zero type errors exist under 19 that do not also exist under 18. The shared 688 are artifacts of a partial workspace install (missing @storybook/react types, unresolved @/test path alias, jest-dom matchers not loaded) and cancel out on both sides, which is exactly why the comparison is done as a diff rather than a pass/fail. - React 19 in fact FIXES two pre-existing errors — the `children` prop typing on ThemeProviderProps in theme-toggle.tsx and janua-theme-provider.tsx. - Separately grepped for the APIs React 19 removed — defaultProps on function components, propTypes, ReactDOM.render, findDOMNode, string refs. None present. LIMIT OF THIS EVIDENCE, stated plainly: this verifies TYPE compatibility. It is not a runtime test against React 19, and the package's vitest suite was not run under 19 because the partial install lacks the jest-dom setup (it would fail identically on both versions and prove nothing). If a consumer hits a runtime issue, this range is the thing to revisit. `@janua/nextjs` already declares `react >=17.0.0`, so a permissive range is already the precedent here; these two were the outliers. Publishing is a separate operator step — consumers keep seeing ^18.0.0 until new versions of both packages are published to npm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: sync pnpm-lock specifiers with the widened peer ranges MY ERROR, and the reason #469 turned every janua CI job red on a two-string change. I ran experiments in this worktree (installing React 19 to measure compatibility) and then reverted pnpm-lock.yaml to discard them — but the peer range edit in packages/ui and packages/react-sdk stayed. CI runs `pnpm install --frozen-lockfile`, which compares the lockfile's recorded specifiers against each package.json and refuses on a mismatch: ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with <ROOT>/packages/ui/package.json - react (lockfile: ^18.0.0, manifest: ^18.0.0 || ^19.0.0) Install is the first step of nearly every job, so one stale line failed ~14 of them and made a package.json edit look like a broad breakage. Applied as four hand-edited specifier lines rather than a `pnpm install --lockfile-only` regeneration. The regeneration was tried first and pulled in an UNRELATED change: it re-resolved @vitest/mocker's peer from @types/node@25.3.0 to @types/node@20.19.33. That has nothing to do with React, and a dependency resolution change riding along inside a peer-range commit is exactly the kind of thing that gets attributed to the wrong PR later. The four lines were located by mapping each `specifier: ^18.0.0` occurrence back to its importer block before editing, because pnpm records peerDependencies under the importer's `dependencies` key — a naive search-and-replace would also have hit packages/monitoring, which legitimately depends on React 18 and is not part of this change. Line numbers asserted in the edit script so a shifted file fails loudly instead of silently rewriting the wrong entry. Verified: `pnpm install --frozen-lockfile` now exits 0 — the exact command that failed in CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(security-scan): tell "no critical advisory" apart from "the audit crashed" `pnpm audit --audit-level=critical` conflates two outcomes that need different responses: a CRITICAL advisory exists, and the audit could not run. Both exit non-zero, so this job reported a critical vulnerability while producing NO OUTPUT AT ALL — the log shows `##[endgroup]` and then nothing. pnpm 10.25 crashes parsing the registry's audit response ("Unexpected token '', ... is not valid JSON"), reproduced locally against this tree. The crash is indistinguishable from a finding. Measured before changing anything: `trivy fs . --severity CRITICAL` on this same tree reports ZERO, while this step failed three consecutive runs on #469 and emitted nothing. So the gate has been asserting a critical vulnerability that does not exist, and blocking PRs on it. Someone would have gone hunting for it. Two other janua PRs (#466, #467) pass the same step, which is what made it look like a property of the change under review rather than of the tool. The step still FAILS in both cases — a security check that cannot run must never read as green, which is the whole point. What changes is that the two are now distinguishable: - clean -> exit 0 - crash -> "::error title=Audit did not run" and an explicit "this is NOT a vulnerability finding, do not go looking for one", plus what to do instead - real advisory -> "::error title=Critical advisory" and a crash is retried once first, since the underlying fault is a transient response the registry sometimes returns. All three branches were exercised with mocked outputs before committing, not just reasoned about. This is the same defect this ecosystem keeps producing in different clothes: a check whose failure mode is indistinguishable from its finding. Compare internal-devops/decisions/2026-07-26-fail-closed-seam-doctrine.md, the boundary-checkpoint script, and the operator console's read-proof convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(security-scan): fix the classifier that could not classify The previous commit added a step to tell "no critical advisory" apart from "the audit crashed". It did not work, and it failed in the exact shape of the bug it was written to fix: it emitted none of its own diagnostics and looked identical to the original failure. Two causes, both found by reading the runtime log instead of trusting the change: 1. GitHub's default shell is `bash -e`. `set -uo pipefail` does not clear -e, so `out="$(run_audit)"` aborted the script on the first non-zero audit — the very case it exists to classify. The log showed the step's source echoed, then nothing. `shell: bash --noprofile --norc {0}` is now spelled out, and that line is load-bearing rather than decoration. 2. pnpm's crash output contains a NUL byte. Command substitution drops it ("warning: command substitution: ignored null byte in input") and can mangle what remains, so the crash signature has to be matched on bytes as written. Output now goes to a file and the signature is matched with `grep -qa`. Verified by running the script against a stub `pnpm` on PATH in all three states — clean / crash-with-NUL / real advisory — under both the configured shell and `bash -e`: configured clean -> CLEAN exit 0 configured crash+NUL -> AUDIT_DID_NOT_RUN exit 1 configured advisory -> CRITICAL_ADVISORY exit 1 bash -e crash+NUL -> dies early, no verdict <- the old bug The -e row is kept in the record because it is the proof that the `shell:` line matters; without it this regresses silently to a step that cannot report. Worth noting for anyone testing this: the first harness reported the crash case as "died early" when the script had actually classified it correctly. The NUL byte in the captured output made `grep` treat the stream as binary and suppress its match — the same byte, defeating the test the same way it defeats pnpm. `grep -a` in the harness too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Aldo R. L. <aldo.ruiz.luna@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 325cfb9 commit 95e818e

4 files changed

Lines changed: 59 additions & 9 deletions

File tree

.github/workflows/pr-quality.yml

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,58 @@ jobs:
213213
- name: Install dependencies
214214
run: pnpm install --frozen-lockfile
215215

216+
# `pnpm audit` alone conflates two outcomes that need different responses:
217+
# "a CRITICAL advisory exists" and "the audit could not run". Both exit
218+
# non-zero, so the job reported a critical vulnerability while producing NO
219+
# output at all -- pnpm 10.25 crashes parsing the registry's audit response
220+
# ("Unexpected token ... is not valid JSON"), and the crash reads exactly
221+
# like a finding. Verified 2026-07-26: `trivy fs --severity CRITICAL` on
222+
# this tree reports 0, while this step failed on three consecutive runs and
223+
# emitted nothing after ##[endgroup]. Someone would have gone looking for a
224+
# vulnerability that does not exist.
225+
#
226+
# This keeps the gate FAILING in both cases -- a security check that cannot
227+
# run must never read as green -- but makes the two distinguishable, and
228+
# retries once first, since the underlying fault is a transient response
229+
# the registry sometimes returns.
216230
- name: Run pnpm audit
217-
run: pnpm audit --audit-level=critical
231+
# `shell:` is spelled out because the GitHub default is `bash -e`, and -e
232+
# kills this script at the first capture — the failure it exists to
233+
# classify. The first version of this step lost to exactly that: it died
234+
# on line 4 and emitted none of its own diagnostics, so it looked
235+
# identical to the bug it was fixing.
236+
shell: bash --noprofile --norc {0}
237+
run: |
238+
set -uo pipefail
239+
240+
# Output goes to a FILE, not a variable. pnpm's crash output contains a
241+
# NUL byte; `$(...)` drops it with a "ignored null byte in input"
242+
# warning and can mangle what is left, so the crash signature must be
243+
# matched on bytes as written.
244+
run_audit() { pnpm audit --audit-level=critical >audit.log 2>&1; }
245+
crashed() { grep -qa 'is not valid JSON' audit.log; }
246+
247+
run_audit; code=$?
248+
if [ "$code" -ne 0 ] && crashed; then
249+
echo "::notice::pnpm audit crashed parsing the registry response; retrying once."
250+
sleep 10
251+
run_audit; code=$?
252+
fi
253+
254+
cat audit.log
255+
256+
if [ "$code" -eq 0 ]; then
257+
echo "Audit ran and found no advisories at or above 'critical'."
258+
exit 0
259+
fi
260+
261+
if crashed; then
262+
echo "::error title=Audit did not run::pnpm audit CRASHED and reported nothing. This is NOT a vulnerability finding -- do not go looking for one. The tool failed to parse the registry response (pnpm 10.25). Re-run the job; if it persists, audit with an independent scanner before assuming the tree is clean."
263+
exit 1
264+
fi
265+
266+
echo "::error title=Critical advisory::pnpm audit reported an advisory at 'critical'. See the output above."
267+
exit 1
218268
219269
security-scanning:
220270
name: Security Scanning

packages/react-sdk/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
"@janua/ui": "workspace:^"
3333
},
3434
"peerDependencies": {
35-
"react": "^18.0.0",
36-
"react-dom": "^18.0.0"
35+
"react": "^18.0.0 || ^19.0.0",
36+
"react-dom": "^18.0.0 || ^19.0.0"
3737
},
3838
"devDependencies": {
3939
"@testing-library/dom": "^10.4.1",

packages/ui/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@
5858
"zustand": "^5.0.12"
5959
},
6060
"peerDependencies": {
61-
"react": "^18.0.0",
62-
"react-dom": "^18.0.0"
61+
"react": "^18.0.0 || ^19.0.0",
62+
"react-dom": "^18.0.0 || ^19.0.0"
6363
},
6464
"devDependencies": {
6565
"@asamuzakjp/css-color": "^4.1.1",

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)