Skip to content

ci: test the frontend on every labelled PR - #315

Open
FrameAutomata wants to merge 7 commits into
mainfrom
ci/frontend-test-workflow
Open

ci: test the frontend on every labelled PR#315
FrameAutomata wants to merge 7 commits into
mainfrom
ci/frontend-test-workflow

Conversation

@FrameAutomata

Copy link
Copy Markdown
Collaborator

Why

No workflow covers frontend/**. The only place the SPA gets built today is release-traceway.yml, during a release — so a broken frontend build surfaces where it blocks shipping, rather than failing a PR. That's the same argument that justified backend.yml (#306).

What it runs

Step Status on main today
npm run check (svelte-check) 0 errors (12 warnings, 9 files)
npm run test (vitest) 25 tests, 4 files
npm run build ok

All three pass right now, so the gate is honest from the first run rather than starting red.

Conventions followed from backend.yml

  • Label-gated on PRs (pull_request: types: [labeled] + if: ... label.name == 'ci'), unconditional on push to main, plus workflow_dispatch
  • Node version from frontend/package.json engines.node — the same field flake.nix reads (nodeMajor at flake.nix:28), so CI and nix develop can't drift, mirroring go-version-file: backend/go.mod
  • One job instead of three: these steps share a single node_modules and npm ci dominates the run, so splitting would pay the install cost three times

Why lint is excluded

npm run lint is prettier --check . && eslint ., and it fails on pre-existing code:

  • prettier: 10 files unformatted
  • eslint: 10 errors across 5 files — 7 × svelte/no-navigation-without-resolve, 3 × svelte/prefer-svelte-reactivity (mutable Map / URLSearchParams in organization/overview-tab.svelte)

Those aren't style nits — the navigation and reactivity fixes change behaviour, and the prefer-svelte-reactivity ones look like latent Svelte 5 bugs. That belongs in its own reviewed PR, not folded into CI plumbing. The reason is recorded as a comment in the workflow so nobody adds the step and gets a red gate. Filed separately as the tracking issue linked below.

Verification

check, test and build were each run locally in the nix develop .#frontend shell with the exact commands above. The YAML was parsed and its job structure asserted before commit. Applying the ci label to this PR exercises the workflow itself.

🤖 Generated with Claude Code

@FrameAutomata FrameAutomata added the ci Run CI on this PR (remove and re-add to re-validate after a push) label Aug 26, 2026
FrameAutomata and others added 5 commits August 27, 2026 09:38
`npm run lint` is `prettier --check . && eslint .`, and the prettier half
fails on `main`. This is `npm run format` and nothing else -- reflow only,
no token changes -- kept as its own commit so it does not bury the eslint
fixes that follow.

Refs #316

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`npm run lint` is `prettier --check . && eslint .`; the previous commit fixed
the prettier half, this fixes the eslint half.

Scope note: `paths.base` is unset in svelte.config.js, so `resolve()` is
identity today and none of this changes behaviour. It is NOT a claim that the
app is base-path correct -- ~30 sites the rule cannot see (`redirect()` in
load functions, `window.location.href` in api.ts, `window.open` in the row
click handlers) still emit unresolved absolute paths. This clears the flagged
sites so the gate can go green; base-path support is a separate job.

## svelte/prefer-svelte-reactivity (3, all in overview-tab)

All three were false positives, not the latent staleness bugs #316 suspected.
The rule does no escape or scope analysis: it flags any `new Map()` /
`new URLSearchParams()` on which `set`/`delete`/`clear` is called anywhere in
a `.svelte` file. All three instances are function-local scratch -- two inside
`$derived.by` callbacks that return arrays, one inside a pure function
returning a string -- so a reactive SvelteMap would only add proxying to paths
that re-run on every search keystroke.

Two sites drop the mutation instead, which is both simpler and satisfies the
rule honestly: `projectOptions` builds its Map from entries, and
`instanceHref` folds its conditional search param into the constructor. The
third genuinely needs get-or-create and carries a disable with the reason.

## svelte/no-navigation-without-resolve (7)

Four were real, and each needed a different shape:

- root + connection pages: `goto('/setup')` is a static route, so it just
  takes `resolve()`.
- dashboards `setServerScope`: was re-emitting `url.pathname` read back from
  `window.location`, which already carries the base path -- wrapping that in
  `resolve()` would prepend it twice. This function only ever runs on
  /dashboards, so it resolves the route id instead. The `let` split is forced:
  the rule follows an identifier to its declarator but not through a `+`
  chain.
- finish-setup: `returnTo` is a concrete path from the SSO handoff, and there
  is no safe `resolve()` form for it -- `resolve()` substitutes `[params]`,
  and `resolve_route('/a[b]c', undefined)` throws
  `TypeError: Cannot read properties of undefined (reading 'b')` (verified
  against the installed kit 2.49.2). So it prefixes `base` directly and
  carries a disable explaining why. Note `gotoHref`/`resolveHref` in
  utils/navigation and utils/links have this same latent crash, which already
  reaches login/+page.svelte:94 and auth/callback/+page.svelte:45; that is
  pre-existing and left for its own change.

The three `href` errors in overview-tab were false positives -- both helpers
already resolved internally, but the rule only recognises a literal
`resolve()` at the link site. These now use `resolveHref` from utils/links
behind the `{...{ href }}` spread, the idiom six other `<a>` elements already
use for a helper-built href, with the helpers returning unresolved paths so
resolution happens exactly once. A plain `href={instanceHref(server)}` still
errors, so this does not quietly widen what the rule accepts.

Closes #316

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things this repo's own drift is traceable to.

`npm run lint` was missing from the Development Commands table, which lists
dev/build/check only. An agent following CLAUDE.md runs `npm run check` --
which passes on unformatted, lint-failing code -- and never learns the gate
exists. That is how 11 unformatted files and 10 eslint errors accumulated.

The "Adding a New Frontend Page" template taught `catch (e: any)`, which trips
@typescript-eslint/no-explicit-any; #314 introduced one by following it. No
page actually does this -- it is `catch (e)` 93 times and `catch (e: unknown)`
31 times, reading status and message through getErrorStatus/getErrorMessage
from $lib/utils/errors. The template now shows that, matching e.g.
routes/tasks/[task]/+page.svelte.

Refs #316

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No workflow covered frontend/** . The only place the SPA was built was
release-traceway.yml, during a release -- so a broken build surfaced
where it blocks shipping instead of failing a PR.

Runs check (svelte-check), test (vitest) and build, all of which pass
on main today, so the gate is honest from the first run. Follows the
conventions in backend.yml: label-gated on PRs, unconditional on push
to main, workflow_dispatch, and the Node version read from
frontend/package.json engines.node -- the same field flake.nix reads,
so CI and `nix develop` cannot drift.

One job rather than backend.yml's three, because these steps share a
single node_modules and npm ci dominates the run.

`npm run lint` is deliberately excluded and the reason is recorded in
the file: it fails on pre-existing code, and the eslint fixes change
routing and reactivity behaviour, so they need their own PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the step this workflow was written without, and deletes the comment
block explaining its absence.

The gate could not go green before the errors it checks were fixed, so this
branch is now stacked on fix/316-frontend-lint (#316) rather than main --
without it `npm run lint` exits 1 here on 11 unformatted files and 10 eslint
errors. GitHub retargets this to main once #316 merges.

Lint runs after build rather than before: a lint failure is cheap to fix
locally, a broken build is the signal worth surfacing first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FrameAutomata
FrameAutomata force-pushed the ci/frontend-test-workflow branch from 1c26068 to 9f5a4ad Compare August 27, 2026 15:52
@FrameAutomata
FrameAutomata changed the base branch from main to fix/316-frontend-lint August 27, 2026 15:52
@FrameAutomata

Copy link
Copy Markdown
Collaborator Author

Rebased onto fix/316-frontend-lint (#324) and added the Lint step, replacing the comment block that explained its absence.

Retargeted because the gate cannot precede the fixes: on the previous base npm run lint exits 1 here — 11 unformatted files and 10 eslint errors — so adding the step would have started this workflow red, which is the one thing the original description said it was avoiding. With #324 underneath it exits 0.

GitHub retargets this to main automatically once #324 merges.

Lint runs after build rather than before: a lint failure is cheap to fix locally, a broken build is the signal worth surfacing first.

@FrameAutomata FrameAutomata added ci Run CI on this PR (remove and re-add to re-validate after a push) and removed ci Run CI on this PR (remove and re-add to re-validate after a push) labels Aug 27, 2026
The job gained a Lint step but kept the name "Typecheck, test and build",
which is what the check shows up as on a PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@FrameAutomata FrameAutomata added ci Run CI on this PR (remove and re-add to re-validate after a push) and removed ci Run CI on this PR (remove and re-add to re-validate after a push) labels Aug 27, 2026
checkout@v4 and setup-node@v4 both declare runs.using: node20, which the
runner now force-migrates to Node 24 with a deprecation annotation. No point
landing a new workflow already carrying it.

The rest of the repo has the same problem on 12 actions across all 16
workflow files -- tracked in #326, which this does not attempt to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@FrameAutomata FrameAutomata added ci Run CI on this PR (remove and re-add to re-validate after a push) and removed ci Run CI on this PR (remove and re-add to re-validate after a push) labels Aug 27, 2026
@dusanstanojeviccs

Copy link
Copy Markdown
Collaborator

We should make a PR against main, stacked PRs are almost unmergable :/

@FrameAutomata
FrameAutomata changed the base branch from fix/316-frontend-lint to main August 28, 2026 22:29
@FrameAutomata

Copy link
Copy Markdown
Collaborator Author

Retargeted to main. Nothing was rebased — the branch already contained #324's three commits at its base, so it was self-contained all along and just pointed at the wrong branch. #324 is now closed as folded in here.

That makes this one PR rather than two: the lint fixes (11 prettier files, 10 eslint errors) plus the workflow that keeps them fixed. They belong together — the fixes have no independent purpose, they exist so the gate can exist.

Verified locally in nix develop .#frontend, all four steps the workflow runs:

step result
npm run lint prettier clean, eslint clean
npm run check 0 errors, 12 pre-existing warnings
npm run test 25 passed / 4 files
npm run build built in 41.77s

One correction to my earlier comment here: I said this PR pins @v7 so #328 has nothing to do in frontend.yml. That is still true (20bac031), but worth restating now that this targets main — the two PRs touch disjoint files and merge in either order.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Run CI on this PR (remove and re-add to re-validate after a push)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants