Commit 95e818e
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
213 | 213 | | |
214 | 214 | | |
215 | 215 | | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
216 | 230 | | |
217 | | - | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
218 | 268 | | |
219 | 269 | | |
220 | 270 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
| 35 | + | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
62 | | - | |
| 61 | + | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
0 commit comments