Skip to content

docs: record the dashboard toolchain (npm, Node 24.22.1, oxlint) - #23

Merged
AbdikhaniAbdirashid merged 1 commit into
mainfrom
docs/dashboard-toolchain-adr
Nov 3, 2026
Merged

AbdikhaniAbdirashid merged 1 commit into
mainfrom
docs/dashboard-toolchain-adr

Conversation

@AbdikhaniAbdirashid

Copy link
Copy Markdown
Collaborator

Records the dashboard's toolchain, which you asked for in the #9 review: npm + Node + oxlint were chosen in the code without ever being written down, so neither of us could point at a reason for them.

New file: docs/decisions/0005-dashboard-toolchain.md. 0004 is taken (public images, #18), hence 0005. Docs only, no code.

The three decisions

Part Choice Short reason
Package manager npm Vite's engines names Node, Playwright is released against Node; Bun would mean trusting its compatibility layer for the two tools that decide whether the dashboard builds and whether the e2e tests can be believed
Node 24.22.1, pinned matches your laptop, fixes until Apr 2028
Linter oxlint 1.89.0 create-vite default, no config, fast enough to actually stay on; ESLint's extra rules need a typed-lint setup first

On the Node version — a correction

My answer to your question 2 on #9 said Node 22, current LTS. Both halves were wrong, and I've corrected it there too. The real picture, from nodejs/Release:

Node Status on 31 Oct 2026 Supported until
22 maintenance since 21 Oct 2025 30 Apr 2027
24 maintenance since 20 Oct 2026 30 Apr 2028
26 active LTS since 28 Oct 2026 30 Apr 2029

So 22 had left active LTS a year before I called it current, and 24 moved to maintenance eleven days ago. I'm staying on 24 rather than moving to 26 because 26 is three days old as an LTS and our rule is to prefer releases at least two weeks old — the week of the defense is the wrong time to be early. Maintenance still means security and bug fixes, and Apr 2028 is well past us. Happy to move to 26 after the defense if you'd rather.

Two things the ADR writes down as consequences

  • Two runtimes on purpose. Bun for api/ and the worker, Node for dashboard/. Each folder declares its own: api/.bun-version, dashboard/.nvmrc.
  • package.json has no engines field. Today npm installs happily on a Node that Vite then refuses. I hit exactly that this morning: my shell was falling back to the system Node 18, so the dashboard build could not run on my machine at all. Adding engines turns that into one clear error at install time instead. Worth doing when the dashboard enters CI.

Still open, noted in the ADR

The dashboard isn't in CI. Whether it builds currently depends on whose laptop ran it, which is how the Node 18 problem went unnoticed. Step 5.2 should run tsc -b, oxlint and the Playwright tests on Node 24.22.1.

@devinder-dev

Copy link
Copy Markdown
Owner

The choice itself is right: npm + Node 24 + oxlint is easy to defend, and writing it down was the
point. I had it reviewed against the sources, and checked the important claims myself. No
blocker, but seven places say a bit more than is true, and an examiner would find them. I'll
approve once they're in. All docs, no code:

  1. engines alone doesn't stop an install. npm's engine-strict is false by default
    (npm config get engine-strict), so engines only warns. Either say "documents the version
    and warns", or plan engines + engine-strict=true in dashboard/.npmrc.
  2. CI is described as existing. "run in CI" and "its own CI cache" read as today's facts, but
    the dashboard isn't in CI yet. Use the future tense, and list the future steps: Node from
    .nvmrc, npm ci, lint, npm run build (Playwright uses the dev server, so it doesn't prove
    the production build), the e2e tests, and the relay-dashboard image build and push in the
    same PR as its manifest in k8s/app/
    .
  3. CONTEXT.md still says Bun is "pinned everywhere". Change that row to "API/worker: Bun
    1.4.4; dashboard build and e2e: Node 24.22.1", with a link to this ADR, in the same PR.
  4. The ESLint reason overstates it. Many ESLint rules work without typed linting, and "too
    slow, we'd stop running it" isn't measured. Better: oxlint is the scaffold's linter, covers our
    checks with a small config, and we haven't found an ESLint-only rule we need.
  5. The two-week rule. 24.22.1 came out on 20 Oct, 11 days before the ADR, and 24.22.2 exists
    since 27 Oct. The real reason is "stay on the Node 24 line we already tested, no major change
    before the defense". And "no upgrade needed" → "no major upgrade before the exam; patch
    updates still apply".
  6. npm and Node are two decisions. Vite and Playwright's Node requirements explain Node,
    not npm. npm's own reasons: the existing package-lock.json, npm ci, a workflow we know.
  7. "A Node CVE isn't a production concern" is too absolute. Node isn't in the final image,
    but a compromised build step could change the files it builds. "Node never runs in
    production" is accurate.

The Node release table and the supported-until dates check out.

@AbdikhaniAbdirashid
AbdikhaniAbdirashid force-pushed the docs/dashboard-toolchain-adr branch from b79769a to 90fb22b Compare November 3, 2026 20:43
@AbdikhaniAbdirashid

Copy link
Copy Markdown
Collaborator Author

All seven are in, plus the ADR status. Rebased on main first, so it stays one commit.

  1. engines alone doesn't stop an install. Right, and the bullet says so now: engine-strict
    is false by default, so engines only warns. A real install-time failure needs engines
    plus engine-strict=true in dashboard/.npmrc. I documented that instead of adding it:
    CI pins Node from .nvmrc, which is the guard that actually decides whether the dashboard
    builds, and engine-strict would only protect a local npm install on the wrong Node.

  2. CI described as existing. This one changed under us — the dashboard is in CI now (infra: build, test and publish the dashboard image #30),
    so I kept the present tense and replaced "Still to decide" with an "In CI" section describing
    the real jobs: Node from .nvmrc, npm ci, lint, npm run build, Playwright, then the
    relay-dashboard image built, smoke-tested and pushed on main. I also wrote down why
    npm run build is a separate step: Playwright drives Vite's dev server, so only the build
    proves the production build. This PR therefore needs infra: build, test and publish the dashboard image #30 merged first, or the ADR
    describes CI that isn't on main yet.

    On "the same PR as its manifest in k8s/app/" — I split those on purpose. The image tag has
    to exist before a manifest references it, otherwise the rollout fails on a missing image and
    relay-deploy rolls the whole commit back, API included. infra: build, test and publish the dashboard image #30 is the image, infra: add the dashboard manifests for cdn.myrelay.tech #24 is the manifest.

  3. CONTEXT.md. The Runtime row now reads "API and worker: Bun 1.4.4 … Dashboard build and
    e2e: Node 24.22.1", linked to 0005. Same commit.

  4. The ESLint reason. Dropped the speed claim — we never measured it. Now: oxlint is the
    linter create-vite scaffolds, it covers our checks with a small config, and we haven't hit
    a rule that only ESLint has.

  5. The two-week rule. Fair hit — the rule cut against our own choice. The reason is now that
    the dashboard is already built and tested on the Node 24 line, and a major Node version is the
    kind of change that moves Vite's and Playwright's own supported ranges, which are the two tools
    that decide whether it builds and whether the e2e tests mean anything. Not something to find
    out in defense week. Also changed "no upgrade needed" to "no major upgrade before the exam;
    patch releases on the 24 line still apply".

    On 24.22.2: noted. .nvmrc and the Dockerfile still pin 24.22.1, and a patch bump is a code
    change, so I'd rather do it as its own PR after the defense than widen a docs PR.

  6. npm and Node are two decisions. Split. npm now has its own reasons (the existing
    package-lock.json, npm ci, a workflow we both know), and the row says outright that the
    Node version is a separate decision.

  7. "A Node CVE isn't a production concern." Now "Node never runs in production", with the
    caveat that a compromised build step could change the files it produces, and those files are
    exactly what users are served.

Also flipped Status from Proposed to Accepted, to match 0001-0004. Unrelated, but 0006 is still
Proposed as well - yours to flip whenever you're ready.

@devinder-dev devinder-dev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All seven are in, and I checked the facts against the files: no engines field in dashboard/package.json, oxlint 1.89.0, api/.bun-version 1.4.4, and create-vite 9.3.3's react-ts template really does scaffold oxlint. The npm/Node split reads much better. Approved.

Merge order: #30 first, as you say, since the "In CI" section describes its jobs.

Two small things, neither blocking:

  1. Splitting the image from the manifest: I agree with the split, but for a different reason. With #30, deploy already needs dashboard-image, so in one PR the tag would still be pushed before the manifest is applied. What would break is visibility: a new GHCR package starts private and k3s pulls without credentials, so the first rollout would hit ImagePullBackOff and roll back, API included. Splitting lets us make relay-dashboard public and test an anonymous pull before #24 depends on it.
  2. The npm row: the Bun paragraph argues about Bun as a runtime (Vite and Playwright on its compatibility layer). Bun as only the package manager (bun install, with Node still running Vite) is a different option; one sentence on why not (a second tool and lockfile format in a Node folder, and npm ci is what we both know) would close it. Fine as a follow-up.

Thanks for the 0006 reminder.

AbdikhaniAbdirashid added a commit that referenced this pull request Nov 3, 2026
Two jobs, so the dashboard gets the same lane in the pipeline the API
already has:

```
dashboard-test ──► dashboard-image ──┐
api-test ──────────► api-image ──────┤──► deploy
deploy-script-test ──────────────────┘
```

`dashboard-test` runs the 35 Playwright tests that until now only ever
ran on my laptop.
`dashboard-image` builds `relay-dashboard`, smoke-tests it, and pushes
it on main only.
`deploy` waits for both image jobs: every image a commit's manifests ask
for has to be on
GHCR before the server is told to pull them.

## Why the smoke test checks two things, not one

A status check on `/index.html` looks sufficient and is not.
`nginx.conf` hands every
unknown path to `index.html` (it has to, or `/deliveries` would 404 on
reload), so that
request answers 200 almost no matter what is in the image.

I built a deliberately broken image — the real one with `assets/`
deleted — to see which
check catches it:

```
/index.html                 → 200   passes
/assets/index-BimGaWWm.js   → 404   fails
```

`location /assets/` has `try_files $uri =404` and no fallback, so the
bundle that
`index.html` names is the request that proves the build is really inside
the image. The
step reads that path out of `index.html` rather than hardcoding it,
because the filename
is content-hashed and changes on every build.

Run against the real image the step exits 0; against the broken one it
exits 1 with
`::error::/assets/index-BimGaWWm.js answered 404: the build is not in
the image`.

## Details worth a second look

- **`cache-from: type=gha,scope=dashboard`.** The scope defaults to
`buildkit` for
everyone, and Docker's docs are explicit: build two images without it
and each
overwrites the other's cache. The API keeps the default, so the two no
longer collide.
Naming the API's scope `api` would be tidier but means touching your
job, so I left it.
- **`npm ci`, Node from `dashboard/.nvmrc`.** The dashboard is npm and
Node 24, not Bun.
`npm install` would be free to resolve versions `package-lock.json`
never pinned.
- **`npx playwright install --with-deps chromium`.** The runner has no
browser. Without
  this line the job fails on its first run with "browser not found".
- **`curl -fs`, not `-fsS`, inside the wait loop.** A refused connection
while nginx
starts is expected, and printing it would make a green run look broken.
- **Comparing `%{http_code}` by hand.** `curl -s` exits 0 on a 404, so
`set -e` does not
  catch it.

## One request, and one ordering note

**Please add `dashboard-test` as a required check** on main. A green job
stops nobody from
merging; only branch protection does, and only you can change it.

The comment in `dashboard-test` names **decision 0005**, which is still
open as #23. Merging
that first keeps the reference from dangling.

## How it was checked

- `npm ci`, `npm run lint`, `npm run build`, `npm run test:e2e` locally:
**35 passed** in 19.5 s.
- The image built and served: `/index.html` 200, `/deliveries` 200
(fallback), uid 101.
- The smoke step extracted straight out of this YAML and run against
both a good and a
  broken image, with the results above.
- `pipeline.yml` parses, and `deploy` really lists all three jobs in
`needs`.

## Not in this PR

- `k8s/app/dashboard.yaml` — #24, which goes out of draft once this is
merged and the first
  `relay-dashboard` image is on GHCR.
- Uploading the Playwright HTML report and traces as artifacts on
failure. The `github`
reporter annotates failing tests on the PR, which is enough until a red
test is hard to read.

Co-authored-by: Abdikhani Abdirashid <Abdikhani_abdirashd@hotmail.com>
@devinder-dev

Copy link
Copy Markdown
Owner

Correction to my note above: relay-dashboard came out public by itself on the first push from #30 (an anonymous pull of c145056 answers 200; a missing image answers 403), so there was no visibility step after all. The split is still fine as two smaller PRs.

@AbdikhaniAbdirashid
AbdikhaniAbdirashid merged commit ba9bb25 into main Nov 3, 2026
4 checks passed
@AbdikhaniAbdirashid
AbdikhaniAbdirashid deleted the docs/dashboard-toolchain-adr branch November 3, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants