Skip to content

fix(ci): green the Format gate, restore the pnpm pin — and the missing content schema - #25

Open
amielnoy wants to merge 11 commits into
mainfrom
fix/ci-format-generated
Open

fix(ci): green the Format gate, restore the pnpm pin — and the missing content schema#25
amielnoy wants to merge 11 commits into
mainfrom
fix/ci-format-generated

Conversation

@amielnoy

@amielnoy amielnoy commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Four commits. The first three green main; the fourth completes the Supabase content migration.

1. The Format gate was policing a generated file

mockupPreviewPlugin.ts rewrites .generated/mockup-components.ts on every sandbox build in its own formatting, so Prettier and the generator fought and whoever built last committed the difference. Ignored now, alongside the build artefacts .prettierignore already lists. A sandbox build leaves the tree clean.

2. The pnpm pin was missing

"packageManager": "pnpm@11.20.0" was dropped on 20 August by a Replit Agent commit titled "Remove dependency from package.json". Corepack then walks up the filesystem and finds whatever it hits first — on a dev machine with a ~/package.json, that is yarn.

3. Vercel's own Git builds are off

They have nowhere to land: the single output directory only exists after the workflow's assembly step, so every push produced a red No Output Directory named "public" found next to the green deployment that actually shipped.

4. The content schema the migration never had

The migration was built except for one thing: nothing in this repository creates the tables. The extractor reads the question bank and the two catalogs, the generator writes 299 inserts, the server queries six tables over PostgREST, and the client already fetches all three collections with the bundled copy as a fallback — but the seed only truncates and inserts, so it cannot be applied to a fresh database. /api/content/question-bank returns 503 {"error":"Content temporarily unavailable"} in production right now.

scripts/src/academy-schema.sql creates them. Two details in it are load-bearing:

  • Identity columns. The seed's last six lines call setval('<table>_id_seq', …) so a hand-added row cannot collide with a seeded id. Against plain bigint columns those calls abort the transaction and nothing lands — which is what a throwaway Postgres reported when asked.
  • grant select to anon plus a read policy. The API reads with the anon key. Without both, the tables are full and every response is empty — indistinguishable from having no tables.

Two defects found while verifying:

  • The extractor dropped every lecture URL. Lectures store a deck number and the client turns it into an href at render time; the extractor wrote the records raw, so regenerating the seed produced cards marked ready that open nothing. The committed JSON had absolute URLs someone had patched in by hand. It resolves them through lectureHref() now and throws if a ready lecture ends up without one. The fixed extractor reproduces the previously committed JSON exactly.
  • A one-byte churn: the JSON was written without a trailing newline, so every regeneration dirtied the file.

Verified end to end, not by eye: schema and seed applied to a scratch Postgres — 150 question items, 80 coding challenges, 40 lecture items in both languages, ordered as the API orders them, text[] answers and Hebrew intact, zero ready-but-linkless cards.

tests/unit/contentSchema.spec.ts holds the schema, the seed and content_store.py to one set of column names without needing a database.

295 unit tests pass; typecheck, lint and format are clean.

🤖 Generated with Claude Code

amielnoy and others added 3 commits August 26, 2026 05:44
`mockupPreviewPlugin.ts` rewrites `src/.generated/mockup-components.ts` on
every sandbox build, in its own formatting. Prettier reformats it, the next
build writes it back, and whoever built last commits the difference — which
is precisely how it reached `main` and turned the Format step red there.

Formatting generated output is a fight with the generator, so it is
ignored instead, alongside the build artefacts already listed for the same
reason. It stays committed, because a fresh checkout has to typecheck
before anything has run the plugin.

A sandbox build now leaves the working tree clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`"packageManager": "pnpm@11.20.0"` was removed on 20 August by a Replit
Agent commit titled "Remove dependency from package.json", which appears
to have taken the pin along with the dependency it meant to delete.

Corepack reads that field to decide which package manager runs. Without
it, it keeps walking up the filesystem — and on a machine with any
package.json above the checkout, it finds that one instead. Here it lands
on `~/package.json`, which pins yarn, so every `pnpm run` in the
repository answers "This project is configured to use yarn".

CI was unaffected because the workflow installs pnpm itself, which is why
this survived six days without being noticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Vercel project connected to a GitHub repository builds on every push,
and such a build has nowhere to land here: the twelve apps each write
their own `artifacts/<name>/dist/public/`, and the single directory
Vercel wants only exists after the workflow's assembly step. Left on, it
failed every push with *No Output Directory named "public" found* — a red
deployment sitting next to the green one that actually shipped.

`git.deploymentEnabled: false` is read from the pushed commit, so it
silences the branches that carry it. The file is inert for the deployment
itself: `--prebuilt` serves the routes in `.vercel/output/config.json` and
never reads `vercel.json`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

The content migration had everything except the tables. The extractor
reads the question bank and the two catalogs, the generator writes 299
insert statements, the server queries six tables through PostgREST, and
the client already fetches all three collections with the bundled copy as
a fallback — but nothing in this repository ever created the tables the
seed inserts into, so the seed cannot be applied to a fresh database and
`/api/content/*` answers 503. That is what the live API is doing today.

`academy-schema.sql` creates them, and two details in it are not
decoration. The ids are identity columns because the seed's last six
lines call `setval('<table>_id_seq', …)` so a hand-added row cannot
collide with a seeded one — against plain `bigint` columns those calls
abort the transaction and nothing lands, which is what a throwaway
Postgres said when asked. And the anon role is granted select with a read
policy, because the API reads with the anon key: without both, the tables
are full and every response is empty, which looks exactly like having no
tables at all.

Verified end to end rather than by eye: schema and seed applied to a
scratch Postgres, 150 question items, 80 challenges and 40 lecture items
in both languages, ordered the way the API orders them, `text[]` answers
and Hebrew intact.

`tests/unit/contentSchema.spec.ts` holds the schema, the seed and
`content_store.py` to one set of column names, without needing a
database. A rename in one of the three is otherwise reported as a 503
that reads like an outage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@amielnoy amielnoy changed the title fix(ci): green the Format gate, restore the pnpm pin fix(ci): green the Format gate, restore the pnpm pin — and the missing content schema Aug 26, 2026
Applying the seed meant composing a connection string by hand from a
password, a pooler hostname and a role that only exist together in
`server/app/config.py` — and pasting it into a shell, where it lands in
history. The first attempt at that pasted the placeholders verbatim and
asked DNS to resolve `db.<ref>.supabase.co`.

`seed:academy` reads the one value that is not already in the repository
from `.env.local`, which is git-ignored, and composes the rest. It uses
psql when it is installed and the same client out of a container when it
is not, since Docker is already required by the test suite. The password
travels in the environment, never in argv, where `ps` would show it.

A password found in a git-tracked file is refused rather than used, with
advice to rotate it. This repository commits `.env` files on purpose —
they hold public build-time config — so a secret that lands in one is a
secret on its way to GitHub, and quietly accepting it here is how it
would stay there.

It finishes by counting what landed, per collection and per language, and
fails if any lecture is marked ready without a URL — the invariant the
seed has broken before, now checked against the database rather than only
against the file that fills it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A `DATABASE_URL` left in the shell from a copied example wins over
everything else the seeder can compose, and the failure it produces is a
DNS error about a host called `db.<ref>.supabase.co` — which reads like a
network problem rather than a setting nobody filled in. That is exactly
how it failed the first time someone ran it.

Angle brackets are now refused by name, with the two ways out: fix the
value, or unset it and let the password in `.env.local` compose the
connection. The banner also prints the user and host it is about to
write to — never the password — so the wrong target is visible before
anything is applied rather than after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
learn-practice-work-ai-testing-academy Ready Ready Preview Aug 26, 2026 2:03pm

@vercel
vercel Bot temporarily deployed to Preview – learn-practice-work-ai-testing-academy August 26, 2026 03:33 Inactive
Every API path on the deployed site was returning the academy's HTML
shell at HTTP 200, because the catch-all rewrite that makes client-side
routing work does not know `/api` is different. The client cannot tell:
`loadServerConfig` checks `res.ok`, the status passes, `res.json()` throws
on HTML, the catch returns `{}`, and the site concludes there is no
server-side key. The AI proxy, Google sign-in, the content API and
checkout all disappear, and the page looks perfectly healthy while it
happens. This repository already documents the same trap for the dev
server; the deployment had grown its own.

`/api/*` is now settled before the filesystem, and where it goes is a
deploy-time decision rather than a committed hostname — the API has moved
host twice already. With `API_ORIGIN` set it is proxied there, which also
means the browser sees a single origin, so the login cookie stays
first-party and CORS never applies. Without it, 503 with a JSON content
type: the same message the swallowed HTML was trying to give, said out
loud and immediately.

The smoke check now fails the deploy if `/api/ai/config` comes back as
text/html, and the unit suite exercises both shapes of the table.

It also stops failing every pull request. Preview deployments are behind
Vercel Authentication by design, so each route assertion met a login
redirect and went red on a deployment that was fine. A preview is now
checked for what is true there — that it deployed and is protected — and
the full pass runs on production, which is public.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
amielnoy and others added 3 commits August 26, 2026 17:02
The red deployments beside every green one did not come from this
repository's configuration. A Vercel project belonging to an entirely
different codebase, `home-economy-stabilation`, had its Git integration
pointed here, so it built twelve Vite apps and then went looking for a
single `public/` that only exists after the workflow's assembly step.

`git.deploymentEnabled: false` silenced it, and would have silenced every
other project linked to this repo along with it — Vercel reads that flag
per repository, not per project, and more than one project is linked here
with at least one deploying successfully. Quieting all of them to fix one
misconfigured project trades a visible problem for an invisible one.

The fix belongs in that project's own settings, and the runbook now says
so. This reverses c336396, whose reasoning was sound and whose premise —
that the failing build was ours — was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`vercel deploy` prints the URL when the deployment is created, which is
not when the edge starts serving it. Run 32950109049 went red on that
gap: the URL was printed at 08:55:32.801 and the first check failed at
08:55:33.095 — 294 milliseconds later, against a deployment that answers
correctly to this day.

The check now waits for the root to stop answering 404, treats a public
200 and a protected 302 alike as ready, and distinguishes "no response at
all" from "responded, but wrongly" when it gives up.

Two smaller corrections to the preview branch. It read the status and the
Location header with two separate requests, which can land on different
states and disagree about what the deployment did; one request now
provides both. And a 401 challenge counts as protection working:
Deployment Protection answers a non-browser client — which `curl -I` is —
with a bare challenge rather than the SSO redirect in several
configurations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…atic

The accessibility scan waited for the main landmark to prove the app had
rendered, and it does not prove that: `index.html` ships a prerendered
shell inside `#root` that already contains `<main id="main-content">`, so
the wait resolved against markup present before any script ran — and axe,
sampling an unstyled page, reported every text node as a contrast failure
at once.

It now waits for two things that are only true afterwards: a painted
`body` background, which the UA leaves transparent until `app.css` is
applied and which therefore proves the stylesheet axe is about to measure
is live, in dev and in a production build alike; and the prerender marker
being gone, since `createRoot().render()` replaces `#root` wholesale
rather than hydrating it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant