Skip to content

ci: the repo's first .github/ - a verify workflow plus Renovate with the holds encoded - #56

Merged
wormeyman merged 3 commits into
mainfrom
ci/verify-workflow-and-renovate
Jul 30, 2026
Merged

ci: the repo's first .github/ - a verify workflow plus Renovate with the holds encoded#56
wormeyman merged 3 commits into
mainfrom
ci/verify-workflow-and-renovate

Conversation

@wormeyman

Copy link
Copy Markdown
Collaborator

Closes #54.

git log --all -- .github was empty, so this is the first .github/ directory in the repository's history. Two files.

.github/workflows/verify.yml

Runs pnpm run verify verbatim - not a re-listing of its three phases as separate steps. verify is the single definition of "this repo is consistent"; splitting it in the workflow would create a second definition free to drift from package.json. It is verify, not the check script, so CI never rewrites files.

  • Triggers: pull_request, push to main, plus workflow_dispatch. Concurrency-cancels superseded PR pushes.
  • permissions: contents: read and nothing else. The job needs no secrets at all - Cloudflare Pages does not build this repo, so CI here is a check, not a deploy path.
  • Third-party actions pinned to full commit SHAs, with the release named in a trailing comment.
  • No version: input on pnpm/action-setup. v6+ reads devEngines.packageManager, so the pnpm pin stays in one place instead of being duplicated into YAML. It runs before setup-node because cache: pnpm resolves the store path by invoking pnpm.
  • Confirmed rather than assumed: preview:test needs no Docker. The worker tests are pool-workers (workerd installs from npm) and the container tests are node --test against render.mjs. The whole gate runs on a plain runner - see the run linked below.

Deliberately not included: pnpm refs:sync (needs a Factorio binary and ~/GitHub/factorio-data; verify's no-Factorio property is what makes this workflow possible at all), any deploy step, and pnpm vp build - the build reads git history for the stamp and wants a checkout policy of its own. The build gap is real and left as a follow-up rather than silently dropped.

.node-version is now machinery

setup-node reads it via node-version-file, which gives the file its first consumer and flips it from documentation to machinery. CLAUDE.md said the opposite and is corrected here. engines.node stays a permissive floor and is explicitly not what CI runs.

.github/renovate.json5

Renovate rather than Dependabot for the reason #54 gives: the decisions here are holds, and ignore entries cannot express them. Encoded:

rule why
typescript disabled outright the row that would otherwise be re-litigated weekly (#48, #52)
pako: 14-day age, note pointing at deflate.ts legacyHash: true is a pako extension with no zlib-API contract, from a library that flipped that default inside one major
wrangler + @cloudflare/vitest-pool-workers grouped pool-workers hard-pins an exact wrangler; a split PR cannot resolve
brace-expansion override disabled 2.1.3 is a deliberate maintenance backport, not three majors stale; pnpm audit staying red is not a reason to take 5.x
matchDepTypes: ["engines"] disabled engines.node is a permissive floor, not a version to keep current
helpers:pinGitHubActionDigests makes SHA-pinning the rule for any action added later, so pin-vs-tag cannot drift file by file

Three rules carry prBodyNotes, so the reasoning arrives attached to the proposal instead of waiting in CLAUDE.md for someone to remember it.

The pnpm interaction, called out explicitly

This workspace's release-age guard is a pnpm 11 default, not a line in pnpm-workspace.yaml. When pnpm is asked for something fresher than that cooldown it does not refuse - it writes a minimumReleaseAgeExclude: bypass into pnpm-workspace.yaml, which is exactly how vue-tsc@3.3.8 once waived the guard silently. A dependency bot is a machine for manufacturing that situation.

So minimumReleaseAge: "3 days" is declared in the Renovate config, above pnpm's default, so Renovate can never open a PR for a release pnpm would want a bypass for. Declaring it also means a future pnpm changing its own default cannot move this repo's floor silently. If minimumReleaseAgeExclude: ever shows up in a Renovate PR diff, that PR is wrong.

Renovate is inert until the GitHub App is enabled on the repo (https://github.com/apps/renovate) - a one-click repo-owner step I can't do. The config was validated with renovate-config-validator (INFO: Config validated successfully).

Verification

  • Baseline pnpm run verify green locally before any of this: 142 test files / 1249 tests, 91s on a loaded machine.
  • The workflow's proof is the run on this PR, linked in the thread below - a YAML file that has never executed is not a working CI setup.

Not done here, on purpose

Branch protection on main. #54 raises it and it is the right follow-up, but it is a repository setting, not a file, and "a required check that can be bypassed is most of the cost for none of the benefit" is a decision for the repo owner. Worth doing once this check has a track record.

Interaction with the concurrent PRs

🤖 Generated with Claude Code

https://claude.ai/code/session_01F3a2HLJ4beKARi7SPmaM54

wormeyman and others added 2 commits July 29, 2026 21:02
… history)

`pnpm run verify` was the whole quality gate and it only ran when a human
remembered to, or at deploy time - which can be days after the merge that
broke it. `git log --all -- .github` was empty. Closes the structural half of
issue #54.

The workflow runs `pnpm run verify` verbatim rather than re-listing its three
phases, so CI and local cannot drift into two definitions of "consistent".
No refs:sync (needs a Factorio binary), no build step, no deploy, no secrets.

Third-party actions are pinned to full commit SHAs with the release named in a
trailing comment; permissions are `contents: read` only.

Renovate over Dependabot because the holds here need encoding, not ignoring:
typescript is disabled outright, pako carries a 14-day age and a note pointing
at the byte-exactness invariant, wrangler + vitest-pool-workers are grouped
because pool-workers hard-pins wrangler, and the brace-expansion override and
`engines.node` floor are both marked not-a-stale-version. `minimumReleaseAge`
is declared at 3 days specifically so Renovate can never propose a release
fresh enough to make pnpm write a `minimumReleaseAgeExclude:` bypass.

Config validated with `renovate-config-validator`. Renovate stays inert until
the GitHub App is enabled on the repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3a2HLJ4beKARi7SPmaM54
…ailed on

The first CI run of the new workflow failed on exactly one test, and it was not
a bug: `elevationRenderRequest.spec.ts`'s `view 'all' composites all five
overlays` needs 9.8s on a 4-core GitHub runner (the whole suite is ~3x slower
there - 230s vs 71s locally) against Vitest's 5s default.
https://github.com/wormeyman/FactorioMapWebUI/actions/runs/30512820959

Raising the default rather than adding a 25th annotation. 24 individual tests
across 10 files already carry an explicit `}, 120000)`, which is the same
complaint made 24 times by hand, and elevationRenderRequest.spec.ts has 27
tests with zero annotations - so the annotation approach was already failing in
its characteristic way, by depending on the next author remembering. 30s is ~3x
the slowest measured case and still fails a genuine hang far inside the job's
15-minute cap; the existing 120000 annotations keep winning where they are set.

No retries. These tests compare pixels against captured game output, so nothing
here is nondeterministic - a retry could only hide a real regression.

Also corrects two things this repo's docs asserted and CI disproved:

- `verify` is ~65-90s locally and ~4 minutes on a runner, not the ~9.5s
  CLAUDE.md claimed. The gap is the point: 63s is where people start skipping a
  manual gate, which is half the argument for having CI at all.
- `.node-version` is machinery now, not documentation - `setup-node` reads it,
  so it is the version the gate runs on. `engines.node` stays a permissive
  floor. The README also said "Requires Node 24.18.0 (see `.node-version`)",
  conflating the floor with the pin; `.node-version` says 26.5.0.

Adds a CI section to CLAUDE.md (the conventions the workflow establishes, the
Renovate/pnpm minimumReleaseAge interaction, and why branch protection is not
configured here) and a verify badge + CI section to the README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3a2HLJ4beKARi7SPmaM54
@wormeyman

Copy link
Copy Markdown
Collaborator Author

The workflow has actually run, twice, and the first run earned its keep

Run 1 - https://github.com/wormeyman/FactorioMapWebUI/actions/runs/30512820959 - FAILURE (4m19s)

Every step up to the gate worked first time: checkout, pnpm resolved from devEngines.packageManager with no version: input, setup-node printed Resolved .node-version as 26.5.0, and pnpm install --frozen-lockfile reported Scope: all 3 workspace projects. All three verify phases ran - including preview:test, confirming it needs no Docker.

It failed on exactly one thing, and it was not a bug in this repo:

FAIL test/elevationRenderRequest.spec.ts > runRenderRequest > view 'all' composites all five overlays onto terrain
Error: Test timed out in 5000ms.
    × view 'all' composites all five overlays onto terrain   9826ms
    ✓ view 'all' composites rocks OVER resources             4020ms
Test Files  1 failed | 141 passed | 1 skipped (143)

A 4-core runner is ~3x slower than a dev machine (231s vs 71s for the same suite), which put that test at 9.8s against Vitest's 5s default - and the next one down at 4.0s, i.e. passing by one second. This is the pre-existing too-tight-default problem, surfaced rather than introduced.

Run 2 - https://github.com/wormeyman/FactorioMapWebUI/actions/runs/30514284066 - SUCCESS (4m31s), on 0458303:

pass: All 316 files are correctly formatted (1466ms, 4 threads)
pass: Found no warnings, lint errors, or type errors in 301 files (1.5s, 4 threads)
Test Files  142 passed | 1 skipped (143)
Tests  1246 passed | 6 skipped (1252)
✨ Types at worker-configuration.d.ts are up to date.
Test Files  4 passed (4)        # preview worker
ℹ pass 3                        # preview container

The 6 skips vs 3 locally are the it.skipIf(!oracleAvailable()) tests - correct behaviour, no runner has a Factorio install.

The failing run is worth as much as the passing one: it is the negative control. A real test failure produced a red job with exit 1, so the check gates rather than merely reporting.

How the flaky-timeout risk was handled: raise the default, no retries

vite.config.ts now sets testTimeout: 30_000. Reasoning, in order:

  1. The 5s default was already rejected 24 times by hand - 24 individual tests across 10 files carry an explicit }, 120000). Adding a 25th annotation would restate a complaint the codebase has been making for months.
  2. The annotation approach fails in a specific, observed way. elevationRenderRequest.spec.ts has 27 tests and zero annotations, which is exactly how a 9.8s test ends up under a 5s cap. It depends on the next author remembering, and the symptom of forgetting reads like a hang rather than a slow machine.
  3. 30s is ~3x the slowest measured case. It absorbs runner variance and still fails a genuine hang far inside the job's 15-minute cap. The existing 120000 annotations still win wherever they are set - this is a floor, not a ceiling.
  4. No retry. These tests compare pixels against captured game output, so nothing here is nondeterministic. A retry could only ever hide a real regression, so raising a timeout is honest where a retry would not be.

Two local findings, both pre-existing, neither caused by this branch

  • test/oracle/oracle.spaceAge.spec.ts fails locally right now, timing out against its own explicit 30_000. It is it.skipIf(!oracleAvailable()), so it only runs where Factorio is installed and is skipped in CI. Verified pre-existing: stashed every change on this branch and ran it on the pristine tree - identical failure. A live Factorio process is currently running on the machine, which is the likely contention.
  • Heavy render tests time out locally under load in a way no timeout value fixes. During this work the machine hit load average 14 (an lldb at 102% CPU plus a running game), and previewAgreement's Nauvis case took 223s against its own 120000 annotation - 6-15x its CI duration. That is a machine-contention phenomenon, not a suite defect, and deliberately not chased with a higher default: the same commit passes all 1246 tests on a runner.

Local vp check (exit 0) and pnpm run preview:test (exit 0) are green here; the authoritative full-gate evidence is run 2 above.

Follow-ups deliberately left out of this PR

The brace-expansion hold was scoped with `matchDepTypes: ["overrides",
"pnpm.overrides"]`, which is a guess: the override lives in
pnpm-workspace.yaml rather than package.json, and if the depType Renovate
reports for that location is neither string the rule matches nothing and the
hold silently does not exist. `renovate-config-validator` cannot catch that -
the config is valid, just inert.

brace-expansion appears in no manifest in this repo (it is a transitive of
js-beautify via @vue/test-utils), so matching by package name alone is both
correct and the only form that cannot miss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3a2HLJ4beKARi7SPmaM54
@wormeyman
wormeyman merged commit 672c66e into main Jul 30, 2026
1 check passed
@wormeyman
wormeyman deleted the ci/verify-workflow-and-renovate branch July 30, 2026 05:34
wormeyman added a commit that referenced this pull request Jul 30, 2026
#51)

Verified locally **and** in CI before merge.

## Local gate
`pnpm run verify` exit **0** in 58s on an uncontended box:
- format 314 files; lint+type 301 files clean
- app **1249 passed / 3 skipped** (142 files, 1 skipped)
- worker **12 passed** (4 files)
- `wrangler types --check` under 4.115.0: `worker-configuration.d.ts` **up to date**, and the check did not rewrite the file

## CI
Rebased onto `672c66e` (the new `.github/` from #56) so the required `verify` check could actually compute; it reports **pass**. Head `90a941b`.

## Independently re-derived, not taken on trust
- **Release-age gate clear from the registry:** both published `2026-07-28T19:41Z`, **~32h** old at merge against pnpm 11's 1440-minute default.
- **No freshness bypass:** `pnpm-workspace.yaml` is untouched - no `minimumReleaseAgeExclude` block, which was #48's stated acceptance criterion.
- Worker still carries **no** `typescript` and **no** `@cloudflare/workers-types` devDependency.

## One note for the record
Earlier verify runs on this branch failed with 29-34 **timeout-only** failures. That was CPU starvation, not a defect: two other agents were running full suites on the same 12-core box, and the run got 48s of CPU across 154s of wall time. On a quiet box the same tree reproduces the documented ~63s baseline exactly. #56's `testTimeout: 30_000` removes most of that fragility going forward.
wormeyman added a commit that referenced this pull request Jul 30, 2026
Caught by the CI workflow from #56 on its first real use - `vp check` (no --fix)
failed in 27s on two formatting issues. Worth noting the gate earned its keep
immediately on a comment-only change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3a2HLJ4beKARi7SPmaM54
wormeyman added a commit that referenced this pull request Jul 30, 2026
…#52)

Verified locally **and** in CI. The guard was proven non-vacuous independently of the author's own plant.

## The `.vue` gap is real, and `check:vue` is what closes it
Planted a `TS2322` in **`src/ui/FNumberInput.vue`** (deliberately a different file from the author's `FInfo.vue`, so this is an independent reproduction) inside `<script setup lang="ts">`:

| command | result |
| --- | --- |
| `pnpm vp check` | **exit 0** - "Found no warnings, lint errors, or type errors in 301 files" (blind to it) |
| `pnpm run check:vue` | **exit 2** - `src/ui/FNumberInput.vue(16,7): error TS2322: Type 'string' is not assignable to type 'number'.` |
| `pnpm run verify` | **exit 2**, and `vp test` never ran - the chain stops at `check:vue` |

Reverted, then re-confirmed `check:vue` exit 0. Full red-green-green cycle, so the check can genuinely fail.

## No freshness guard was waived
`pnpm-workspace.yaml` is untouched - **no `minimumReleaseAgeExclude` block**, checked in the diff and after a real `pnpm install`. Confirmed from the registry rather than assumed: `vue-tsc@3.3.8` and `@vue/language-core@3.3.8` published `2026-07-22T15:54Z`, so **7.6 days** old, comfortably past pnpm 11's 1440-minute default. The PR's claim that no separate tsconfig is needed also checks out - bare `./node_modules/.bin/tsc --noEmit` now exits **0** with no `Debug Failure`, so the `vue() as Plugin` cast did fix all three tools.

## Local gate
`pnpm run verify` exit **0** in 61s: format 316 files, lint+type 301 clean, `vue-tsc` clean (2.2s), app **1249 passed / 3 skipped**, worker **12 passed**.

## CI
`verify` check: **pass**, on head `9934a45` rebased onto `5f676ea`.

## Added while merging: three stale phase lists the PR missed
`verify` gained a fourth phase, and three places that enumerate the list were left behind (CLAUDE.md was updated, these were not):
- `README.md` spelled the gate out as `vp check` + `vp test` + `preview:test` in **two** places and had no `check:vue` entry at all, making the one command that type-checks `.vue` bodies undiscoverable from the README.
- `.github/workflows/verify.yml`'s comment said "its **three** phases" - and the count is that comment's whole point, since it explains why CI calls `pnpm run verify` verbatim instead of listing phases as steps. **Comment-only change; no step added and no phase mirrored into CI.**

Also resolved the CLAUDE.md conflict with #56, which had rewritten the same `verify` bullet: kept #56's newer measured timings (~65-90s dev / ~4 min CI) and folded in `check:vue`, rather than letting either side's number win by rebase order.
wormeyman added a commit that referenced this pull request Jul 30, 2026
…ps, lockfile hold (#59)

* chore(renovate): tune for "Automated PRs" - security bypass, burst caps, lockfile hold

The Renovate GitHub App went live with "Automated PRs", "Require config file"
and "Create onboarding PRs", so the config now has to hold up unattended rather
than describe an intent.

- `timezone: America/Los_Angeles`. The schedule was UTC, so "before 6am on
  monday" actually fired around midnight Sunday local.
- `prConcurrentLimit` / `prHourlyLimit`. The first run after enabling sees every
  pending update at once, and each PR now costs a ~4 minute CI run.
- `vulnerabilityAlerts` bypasses the weekly window - a CVE fix could otherwise
  sit ~10 days (up to 3 for the age floor, then up to 7 for Monday). Its
  `minimumReleaseAge` is **25 hours, not 0**: pnpm 11 refuses anything younger
  than 24h without writing a `minimumReleaseAgeExclude:` bypass into
  pnpm-workspace.yaml, so a same-day security PR would reintroduce exactly the
  hazard this config exists to prevent.
- `osvVulnerabilityAlerts` - GitHub's advisories miss ecosystem-only reports.
- `lockFileMaintenance` pinned off. It is automated `pnpm up` for the lockfile,
  and transitive re-resolution is what triggered the `TS2321: Excessive stack
  depth ... UserConfig` pathology in vite.config.ts while installing the same
  target versions directly did not. Off by default; pinned so it cannot get
  switched on as tidying.
- `dependencyDashboardApproval` deliberately NOT set - it would re-impose
  scan-only behaviour at the config layer and defeat the app's own setting.

CLAUDE.md's "Renovate is inert until the GitHub App is enabled" is now false and
is corrected, along with the two settings whose reasoning is not guessable from
the outside.

Validated with `renovate-config-validator` (exit 0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3a2HLJ4beKARi7SPmaM54

* style: apply vp check --fix to the renovate config and CLAUDE.md

Caught by the CI workflow from #56 on its first real use - `vp check` (no --fix)
failed in 27s on two formatting issues. Worth noting the gate earned its keep
immediately on a comment-only change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3a2HLJ4beKARi7SPmaM54

---------

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.

No CI and no dependency automation: this repo has never had a .github directory

1 participant