Skip to content

fix(ci): perf-k6 was an invalid workflow file, and nothing lints workflows - #441

Merged
khuepm merged 3 commits into
mainfrom
ci/workflow-lint-and-types-react-dom
Aug 31, 2026
Merged

khuepm merged 3 commits into
mainfrom
ci/workflow-lint-and-types-react-dom

Conversation

@khuepm

@khuepm khuepm commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Three things, in order of how much they matter.

1. perf-k6.yml was invalid, not failing

It had failed on every push for weeks (30+ consecutive runs, predating the dependabot batch). The obvious reading — "the k6 load job needs a load environment it doesn't have in CI" — is wrong. Its on: never declared push at all:

on:
  workflow_dispatch:
  schedule:
    - cron: '0 3 * * *'
  pull_request:
    types: [labeled]

The actual cause is one expression. perf-gate's job-level if: read env.PERF_K6_FULL_RUN, and the env context does not exist in a job-level if: — only github, inputs, needs, vars do. That does not evaluate to empty and skip the job. It makes the entire file unparseable, so GitHub never resolves name: or any job, and records a bare failed run against whatever event fired — on: filters included, because it never got far enough to read them.

The diagnostic tell, for next time:

$ gh api repos/khuepm/lumibase/actions/runs/33325227441 --jq '{name,event,conclusion}'
{ "name": ".github/workflows/perf-k6.yml",   ← the *path*, not "Performance (k6)"
  "event": "push",                            ← an event `on:` never declared
  "conclusion": "failure" }
$ gh api .../jobs --jq '.jobs[]'             ← empty

name coming back as the file path with an empty job list means GitHub could not parse the file. Confirmed independently with actionlint:

.github/workflows/perf-k6.yml:60:170: context "env" is not allowed here.
  available contexts are "github", "inputs", "needs", "vars" [expression]

Fix: use vars.PERF_K6_FULL_RUN, which is available at job level, and drop the workflow-level env entry that nothing else read. The workflow now honours on: and no longer runs on push. Behaviour is unchanged otherwise — set the repo variable PERF_K6_FULL_RUN=true to let the nightly schedule run the full compose + k6 job; unset stops the scheduled run after validate-scripts.

Worth noting: validate-scripts has therefore never executed. I checked its assertions by hand — apps/cms/k6/{smoke,load-deliver,load-items}.js and seed.ts all exist — so it should pass on its first real run.

2. Nothing lints the workflows — that is why this lasted weeks

This is the part I'd actually defend. An unparseable workflow is externally indistinguishable from a workflow that ran and failed, and because perf-k6 is not a required check, no gate objected and no one had reason to look. Same shape as B30 in the last PR: a guard that isn't wired up is worse than no guard, because its silence reads as approval.

New workflow-lint job in ci.yml runs actionlint over .github/workflows. Pinned by version and SHA-256 of the release tarball rather than pulling a third-party action — one fewer uses: SHA to keep current, and the binary is verified before it runs. shellcheck is left enabled, since the run: blocks here drive Postgres, Redis and deploy steps and a quoting bug there is the same class of silent breakage.

Zero findings across all workflows as of this commit.

Per DoD §6 I am not adding a new DoD section for this: the guard is mechanical and self-enforcing, which §6 explicitly prefers over a checklist item humans have to remember.

3. @types/react-dom override raised 19.2.419.2.5

Requested as a prerequisite for the pending minor-and-patch group bump (now #435). That PR raises the manifests to ^19.2.5 while the override pins exact 19.2.4; the ranges stop intersecting, the override silently wins, and drift:check fails — correctly. This is the B20 class the drift guard exists for.

Raising the override here clears it without #435 having to touch either declaration site, and 19.2.5 still satisfies the current ^19.2.4 declarations, so it is a no-op for anyone not on the group bump. Both declaration sites updated (package.json for pnpm 9, pnpm-workspace.yaml for pnpm 10+), so settings:check stays green.

Verified by lockfile, not manifest: every importer now resolves 19.2.5(@types/react@19.2.18), and the entire 102-line lockfile diff is that one propagation — no other package moved.

Verification

  • actionlint (1.7.10, with shellcheck) → 0 findings across .github/workflows
  • pnpm check:all green — settings:check 16 overrides, drift:check 20 declarations vs 16 overrides
  • pnpm typecheck 17/17 (relevant here: the React DOM types bump touches studio/docs/landing/consumer/ui)
  • pnpm build 9/9 · pnpm test 12/12 tasks, 0 failures (serialized — see B32)
  • pnpm audit --prod --audit-level high → no known vulnerabilities
  • pnpm install --frozen-lockfile on a fresh checkout of main (09373a2c, including chore(deps): raise the Node floor jsdom 30 requires, retire the dead nanoid@5 override #436) → exit 0, no drift

Backlog: B31fixed, with the root cause recorded rather than just the symptom.

@khuepm

khuepm commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

Correction: the "0 findings with shellcheck" claim in the description was wrong

The workflow-lint job failed on its first real run, and it was right to. actionlint runs shellcheck over run: blocks only when the binary is on PATH, and skips it silently when it isn't. shellcheck was not installed on my machine, so my local run reported clean while the runner (ubuntu-latest ships it) found five pre-existing issues. My verification was weaker than I described it — recording that rather than quietly editing the description.

Installed shellcheck 0.11.0 locally, reproduced all five, and fixed them instead of lowering the gate:

File Rule Fix
deploy-cms.yml:107,123 SC2086 quote "build:${TARGET_ENV}" / "deploy:${TARGET_ENV}"
perf-k6.yml:84,115 SC2034 for i in $(seq …) never used ifor _
release.yml:78 SC2129 three consecutive >> release-notes.md → one grouped { … } >>

All three are behaviour-preserving. TARGET_ENV is production/staging so the quoting is a no-op today; the loops iterate the same number of times; and the grouped redirect is byte-identical because git log --pretty=format: emits no trailing newline, so the closing echo still supplies it.

The more useful outcome: the job now asserts shellcheck --version before running actionlint. Without that, a future runner image dropping shellcheck would silently stop checking every run: block — the same failure mode as B30, which I had just written up in the previous PR and then walked straight into. A gate that can degrade without saying so is the thing this PR is about.

actionlint exit 0 across .github/workflows with shellcheck actually present, and the Workflow files are valid check is green.

khuepm added 3 commits August 31, 2026 10:18
…flows

`perf-k6.yml` had failed on every push for weeks. It was not the load-test job
running and breaking — its `on:` never declared `push` at all. The `perf-gate`
job's `if:` referenced `env.PERF_K6_FULL_RUN`, and the `env` context does not
exist in a job-level `if:` (only `github`, `inputs`, `needs`, `vars`). That does
not evaluate to empty: it makes the entire file unparseable, so GitHub never
resolved `name:` or any job and recorded a bare failed run against every event,
`on:` filters included. The tell was the API reporting the run's `name` as the
file path with an empty job list.

Fixed by using `vars.PERF_K6_FULL_RUN`, which is available at job level. The
workflow now honours `on:` and stops running on push. Set the repo variable
`PERF_K6_FULL_RUN=true` to let the nightly schedule run the full compose + k6
job; unset stops the scheduled run after `validate-scripts`. Dropped the
workflow-level `env` entry, which nothing else read.

The more useful fix is the second one. The reason a broken workflow stayed
broken for weeks is that nothing checked this class: an unparseable workflow
looks, from the outside, identical to a workflow that ran and failed — and
because it is not a required check, no gate objected. New `workflow-lint` job
runs `actionlint`, pinned by version and SHA-256 rather than adding another
third-party action SHA to keep current. Zero findings across
`.github/workflows` today, shellcheck included over the `run:` blocks that
drive Postgres, Redis and the deploy steps. Not adding a DoD section for this:
the guard is mechanical and self-enforcing, which DoD 6 prefers over a
checklist item.

Also raises the `@types/react-dom` override from `19.2.4` to `19.2.5`. The pin
is exact, so it wins over whatever the manifests declare — which is why the
pending minor-and-patch group bump (manifests to `^19.2.5`) fails
`drift:check`: the ranges stopped intersecting. Raising the override first
clears that without the group PR touching it, and `19.2.5` still satisfies the
current `^19.2.4` declarations, so it changes nothing for anyone not on the
group bump. Verified in the lockfile: every importer now resolves 19.2.5, and
the whole lockfile diff is that propagation.

Backlog B31 closes with the root cause recorded.

actionlint 0 findings, `pnpm check:all` green, typecheck 17/17, build 9/9,
`pnpm audit --prod --audit-level high` clean, `pnpm install --frozen-lockfile`
verified against a fresh checkout of main.
…faced

The `workflow-lint` job failed on its first real run, and the failure was
correct. actionlint runs shellcheck over `run:` blocks only when the binary is
on PATH and **skips it silently** otherwise — shellcheck was not installed on
my machine, so the local run reported clean while the runner (ubuntu-latest
ships it) found five pre-existing issues. Installed shellcheck 0.11.0 locally,
reproduced all five, fixed them rather than lowering the gate:

- `deploy-cms.yml` — `run: pnpm ... run build:${TARGET_ENV}` and the matching
  `deploy:` step, both unquoted (SC2086). Behaviourally a no-op today because
  TARGET_ENV is `production`/`staging`, but it is the exact class of quoting bug
  the comment on this job claims to care about.
- `perf-k6.yml` — two `for i in $(seq ...)` readiness loops that never use `i`
  (SC2034), now `for _`.
- `release.yml` — three consecutive `>> release-notes.md` redirects (SC2129),
  now one grouped `{ ... } >> release-notes.md`. Output is byte-identical:
  `git log --pretty=format:` emits no trailing newline, so the closing `echo`
  is still there.

Also makes the gate unable to weaken quietly: the job now asserts
`shellcheck --version` before running actionlint. Without that, a future runner
image dropping shellcheck would silently stop checking every `run:` block —
which is the same failure mode as B30, and I just demonstrated it on myself.

actionlint exit 0 across `.github/workflows` with shellcheck actually present.
Rebasing this branch hit a conflict because #434 and #436 both assigned `B30`,
to unrelated findings — a `localStorage` flake in `analytics-consent.test.tsx`
and the dead `version-check.mjs` guard. Whichever merged second had to be
renumbered by hand (`B33`), and nothing announced the collision; it showed up
only as a rebase conflict.

That is precisely the failure `registry:check` already prevents for the `#`
column of the Setup Impact Registry, which once carried duplicate #20/#31/#32
rows. The backlog table was simply never included.

It matters more than a cosmetic id clash: backlog ids are referenced *by id*
from other rows ("Nối tiếp B13", "cùng class với B10", "Xem B24") and from
CHANGELOG entries, so a silent renumber breaks cross-references that no test
covers.

`check-registry-numbering.mjs` now walks both tables — `#` in setup-impact.md
and `B<n>` in out-of-scope-backlog.md — reporting the offending line numbers,
the next safe id, and a reminder to keep whichever occurrence other rows cite.
Both tables still fail closed if their shape changes and the scan parses zero
rows, so the guard cannot quietly stop guarding (the B30/B33 lesson, applied to
the guard itself).

Verified both directions: injecting a duplicate `B30` exits 1 naming lines 53
and 56 and suggesting B34; removing it exits 0. Already reachable through
`pnpm check:all`, so pre-commit and CI pick it up with no wiring change.

Logged as B34 (fixed).
@khuepm
khuepm force-pushed the ci/workflow-lint-and-types-react-dom branch from fbad9da to 5e72dba Compare August 31, 2026 03:20
@khuepm

khuepm commented Aug 31, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main — and the conflict was itself a finding worth keeping

Rebasing onto 69037175 conflicted on .kiro/steering/out-of-scope-backlog.md, because #434 and #436 both assigned B30 to unrelated findings: a localStorage flake in analytics-consent.test.tsx (#434) and the dead version-check.mjs guard (#436). #434 merged second and renumbered the latter to B33.

Resolution keeps main as the authority — #434's B30 and its B33 renumber both stand — and layers my B31fixed update (root cause of the perf-k6 breakage) on top. Verified after resolving: 32 backlog rows, all ids unique; B31 reads fixed; both B30 and B33 present exactly once.

Nothing announced that collision. It surfaced only as a rebase conflict, and resolving it by hand is exactly the manual step DoD §6 says to mechanise — especially since registry:check already guards this precise failure for the # column of the Setup Impact Registry, which once carried duplicate #20/#31/#32 rows. The backlog table had just been left out of it.

So check-registry-numbering.mjs now walks both tables: # in setup-impact.md and B<n> in out-of-scope-backlog.md. It reports the offending line numbers, the next safe id, and a reminder to keep whichever occurrence other rows cite by id — which is the part that actually matters, since backlog ids are referenced by id from other rows ("Nối tiếp B13", "cùng class với B10", "Xem B24") and from CHANGELOG entries. A silent renumber breaks cross-references no test covers.

Verified in both directions:

$ sed -i 's/^| B32 |/| B30 |/' .kiro/steering/out-of-scope-backlog.md
$ node scripts/check-registry-numbering.mjs
Registry id check failed:
- Out-of-scope backlog (...) has duplicate row ids:
    B30 used on lines 53, 56
    Give each colliding row a new id starting at B34; keep the occurrence that
    other rows cite by id so cross-references stay valid.
$ echo $?
1
# reverted:
Registry ids OK — Setup Impact Registry: 111 rows, all ids unique · Out-of-scope backlog: 32 rows, all ids unique.

Both tables still fail closed if their shape changes and the scan parses zero rows, so the guard cannot quietly stop guarding — the same lesson as the shellcheck skip earlier in this PR, applied to the guard itself. Already reachable via pnpm check:all, so pre-commit and CI pick it up with no wiring change.

Logged as B34 (fixed). pnpm check:all green, pnpm test 12/12 tasks.

@khuepm
khuepm merged commit 5e33c6a into main Aug 31, 2026
11 checks passed
@khuepm
khuepm deleted the ci/workflow-lint-and-types-react-dom branch August 31, 2026 03:32
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