* fix(build): include shared UI stylesheets in Nx cache inputs
`libs/ui/styles` held shared SCSS partials but had no `project.json`, so its
files belonged to no Nx project and were absent from every task hash. Editing
a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and
shipped the previous CSS — a silent wrong build rather than a failure.
Nx derives its project graph from TypeScript imports only, so a relative Sass
`@use` that crosses a project root creates no edge. Verified directly: after
adding the project but before declaring anything, `ui-styles` still had zero
dependents in the graph.
Make it the `ui-styles` project (no targets — it exists to be hashed) and
declare `implicitDependencies` on the 8 consumers. Chosen over adding the path
to `sharedGlobals`, which would put shared styles into every project's hash and
make a one-line SCSS tweak mark the whole workspace affected. A styles edit now
marks 15 projects affected and leaves electron-backend, website, the mock
servers and the shared libs alone.
`libs/ui/styles` was the only projectless directory holding files under `libs/`
or `apps/`.
Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every
relative stylesheet import against Nx's real project graph and fails when one
escapes the input closure of a build that compiles it, naming the project to
declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of
`apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes
that file, and a lib -> app edge would make the graph cyclic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(build): spawn git without a shell in the stylesheet check
`execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where
single quotes are literal characters rather than quoting. Git received the
pathspec with the quotes intact, matched nothing and exited 0, so
`styles:inputs:validate` reported success after checking zero stylesheets —
silently disabling the check for Windows developers while staying green.
Spawn with `execFileSync` so no shell is involved and git expands its own
pathspec; verified to return the identical 133 files.
Both this and the eslint glob trap next to it in the docs report success while
covering nothing, so also make an empty scan fail rather than pass: the
workspace always contains SCSS, and a listing that returns none means the scan
broke.
Reported by Codex review on #1360.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(styles): move nav-list partial into ui-styles (#1361)
* fix(build): count every target of a comma-separated Sass @import
`@import` is the only rule that takes a list, and the scan read just its
first target. A later entry crossing an Nx project boundary escaped the
cache key while the check still reported success — the same silent-pass
failure the tool exists to prevent.
Parse every target of an `@import` list. The obvious "read all quoted
strings" fix trades one silent gap for a phantom one, so the rule decides:
`@use`/`@forward` load exactly one module and a quoted string after it is
`with (...)` configuration, and `url(...)` stays a plain CSS import the
browser resolves at runtime. Neither is a module Sass compiles.
The workspace has no relative `@import` at all today, so the scan still
finds the same 42 imports across 133 files; this closes the gap before
someone writes one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Third slice of the release-notes pipeline (#1256 format+generator, #1257 CI
gate): release screenshots become reproducible and provably mock-only.
The v0.20 capture script was single-use (hard-coded slugs, paths, hero) and
fail-open: a lost IPTVNATOR_E2E_DATA_DIR silently fell back to the user's
real ~/.iptvnator database, `...process.env` leaked ambient TMDB keys and
proxies, nothing gated network access, and no frame content was ever
validated. Each hole leaks real playlists, credentials, or copyrighted
artwork into published screenshots without a single signal.
New pipeline:
- tools/release/screenshots.manifest.json — declarative shots (slug, title,
named setup steps, themes). Adding a feature shot = one manifest entry.
- capture-release-screenshots.ts — orchestrator; output goes to
apps/website/public/blog/<release>/screenshots/<slug>-<theme>.png, release
slug derived from package.json (or --release), --only/--theme filters.
- capture-app-driver.ts / capture-navigation.ts — launch, seeding, theme,
and the named-action vocabulary; actions are order-independent (every
portal action starts from the dashboard).
- screenshot-guards.mjs — the fail-closed policy, pure and unit-tested:
G1 the real database is snapshotted (sha256+mtime) before launch and must
be byte-identical after; the isolated DB must actually exist
G2 the app receives an allowlisted environment, never ...process.env
G3 deny-by-default network gate; known app-level calls (GitHub update
check) are answered by local stubs; any other blocked request fails
the run — a silently-blocked TMDB call would leave a frame that looks
broken rather than unsafe
G4 every frame is scanned before capture: external img/background URLs,
credential-shaped text, MAC addresses, non-localhost m3u8 references
G5 TMDB enrichment asserted disabled via the renderer's IndexedDB
Any violation deletes every frame captured in the run and exits non-zero.
The guards paid for themselves on the first live run: G3 caught the mock
server redirecting stream endpoints to a public demo HLS
(test-streams.mux.dev) — meaning earlier hand-run captures could embed
third-party video frames. The M3U shot now deliberately captures the groups
layout without starting playback.
`.changes` validation now cross-checks `screenshot:` slugs against the
manifest, so a note cannot reference an image the capture run never
produces.
Verified end-to-end: 10/10 shots (5 slugs × dark/light) captured against
dist build + xtream-mock-server, frames visually inspected (fictional
titles/artwork only), guard-violation paths exercised live. 67 unit tests
in release-tools, lint green, script files within the repo size limit.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* ci(release): gate PRs on an authored release note
Second slice of the release-notes pipeline (#1256 landed the format and
generator): make the .changes/ habit survive contact with reality.
- "Release note gate" job in ci.yml, PR-only: validates every .changes/*.md,
then requires an added note (or the no-release-note label) when the PR
touches runtime code under apps/ or libs/. Tests, e2e projects, the
website, mock servers, shared testing helpers, snapshots and docs are
auto-exempt.
- Policy lives in tools/release/check-release-note-gate.mjs as a pure
function fed PR files+labels as JSON — unit-tested (10 cases) instead of
encoded in workflow bash. The failure message lists the triggering files
and names the exact fix.
- Labels are fetched live rather than from the stale event payload, so
applying the label and re-running the check works without a new push.
- The job is dependency-free Node: no pnpm install, runs in seconds.
- release-notes and release-cut skills added under .claude/skills/ and
mirrored to .codex/skills/; CLAUDE.md/AGENTS.md sections updated to point
at the gate and the skills.
The no-release-note label itself was created in the repository.
Tests: 47 passing in release-tools (10 new gate cases); gate-step shell
verified with shellcheck at the CI severity; ci.yml YAML-parse checked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(agents): make the release skills discoverable by Claude Code too
`.codex/skills/**` was un-ignored so Codex picks up repository skills in any
clone, but `.claude` was ignored wholesale — and Claude Code only discovers
skills under `.claude/skills/`. The release-notes and release-cut skills
therefore existed only on whichever machine authored them.
Mirror both skills into `.claude/skills/` and opt them in by name rather than
un-ignoring the directory: contributors keep personal skills there
(i18n-fill, website, …) which must stay local and out of `git status`.
CLAUDE.md/AGENTS.md updated so the "skills live under .codex/skills/" claim
does not go stale, including the requirement to keep mirrored copies in sync.
The CI gate and the CLAUDE.md/AGENTS.md section remain the load-bearing
enforcement; skills only carry the detail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): only a note this PR authored satisfies the release-note gate
Review follow-ups on #1257 (Codex P2 ×2, Greptile P1).
- Drop `renamed` from the accepted statuses. The PR files API compares
base…head, so a note created and then renamed inside the same PR still
reports as `added`; a `renamed` entry means the file already existed on the
base branch. Accepting it let a runtime-code PR pass by moving another
PR's unconsumed note, which documents nothing and gives the generator no
adding commit to resolve a PR link from.
- Require a direct child of `.changes/`. `loadNotes()` reads only the
immediate directory, so `.changes/sub/note.md` satisfied the old prefix
check while never being validated or rendered into any release surface.
Tests: renamed and nested notes now assert a failing gate (12 gate cases).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>