Repository navigation
fork: fast CI on GitHub-hosted runners (fork-tests.yml) - #6
Conversation
Upstream's workflows are disabled in the fork, so fork pull requests ran no checks. fork-tests.yml now runs upstream ci.yml's checks on pull_request and push to main: lint (knip, generated GraphQL, vp check), build + typecheck + desktop launch + built web boot, package tests, web tests, 8 server shards, Rust, macOS mobile lint and release smoke. It keeps the workflow_call / workflow_dispatch `ref` interface, so a release run tests one commit with every Linux job and no path filter. Upstream's composite actions and change detector are used as they are. A new `drift` job fails, naming each job and step, when upstream's ci.yml or release.yml test jobs change and the copy did not follow. The Check job lists every job and the reason for every skip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- fork-ci-result needs every job and fails on a skip without a reason; its summary lists each job, why it skipped, and job wall times. - Secret scan (gitleaks, checksum-pinned) of the commits a pull request or push adds; workflow lint (actionlint) next to the drift check. - Ubuntu 24.04 runners as upstream: ubuntu-latest (26.04) fails an apps/server test with its newer git. - Test is split across two runners by test file count; vp's task cache is kept between runs for the typecheck and the server shards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Test and Test Web run on two runners each. A package with 100+ test files (and Test Web's 548) runs a vitest --shard slice on every runner; the rest are placed whole by test file count. - Script header comments fit upstream's 4-line comment rule; the jq program in fork-ci-result passes shellcheck. - The Ubuntu 24.04 pin is about choosing OS upgrades, not git: the 24.04 image now ships git 2.56 as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workflows job runs each .github/scripts/fork-*.test.{sh,py,cjs} and
checks them against .github/fork-ci-tests.txt. A present test that is not
listed fails. A listed test that is missing fails once it has been in the
tested commit's history; before that it is a note in the job summary, so a
test whose pull request has not merged yet does not fail the run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job list API needs it; a failed call now warns in the log instead of failing silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now carries #1, #2 and a newer upstream. fork-tests.yml keeps this branch's version (it replaces #2's copy) and ports what the drift check listed: test-delta.test.cjs in the release checks, Vitest's module cache on the server shards (saved only by pushes), and no test-cache report upload. Web was already split in two; the built-client boot stays in the Build job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
apps/desktop's typecheck includes scripts/lib, and this test imports scripts/build-desktop-artifact.ts, which that project does not list (TS6307). In scripts/ the scripts project covers both files, and the scripts package still runs the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
lens: correctness+tests (scripts) · reviewer: opus high (t3lens-6-scripts) · read at: f956059 covered: .github/scripts/fork-ci-tests.cjs, fork-test-shards.cjs, fork-tests-drift.cjs and their .test.cjs files, .github/fork-ci-tests.txt and .github/fork-tests-upstream.json, read line by line. Also traced how fork-tests.yml calls them (the workflows job with its sparse /.github/ checkout, the Test matrix's "Pick this shard's packages" and Test steps) and ci-changes.cjs readWorkspace/decide, which the shard planner depends on. Run 43213388747: job 131166404449 ran the unshallow path, because fork-follow.test.sh and fork-schema-check.test.cjs are listed but not present yet, and it passed. checks:
finding: minor .github/scripts/fork-tests-drift.cjs:29 Only |
|
lens: security+correctness (workflow) · reviewer: opus high (t3lens-6-wf) · read at: f956059 covered: .github/workflows/fork-tests.yml line by line (all 12 jobs, triggers, concurrency, permissions, caches, the fork-ci-result gate) and scripts/release-repository.test.ts (the move). Traced into fork-test-shards.cjs, fork-ci-tests.cjs, fork-tests-drift.cjs, ci-changes.cjs, setup-ci-deps, scripts/test-cached.ts, fork-release.yml (the workflow_call caller) and upstream ci.yml/release.yml for the job-by-job comparison. Read the logs of run 43213388747 (Secret scan, Test 1). checks:
finding: major .github/workflows/fork-tests.yml:199 (with :175) — the secret scan uses |
…t per run, drift) Secret scan (.github/scripts/fork-secret-scan.sh, tests: fork-secret-scan.test.sh) - Pull requests scan base.sha..head.sha on a full blob-less clone instead of the newest N commits by date. After a merge of main, the PR's oldest commits fell outside --max-count and were never scanned; a squash merge never brings them to main, yet they stay public under refs/pull/N/head. - Merge commits are diffed with --remerge-diff. Plain `git log -p` shows merges with no diff, so a key added only in a merge commit was never read. - Push: before..after; a force push scans what the new tip has and the old one had not. A new branch, or an old tip that cannot be fetched, scans only the pushed commit and says so in a warning. - The Secret scan job runs the test against the pinned gitleaks before scanning. Every test file under .github/scripts runs somewhere (fork-ci-tests.cjs) - Finds *.test.* in subdirectories too. Runs .sh, .py, .cjs, .mjs and .js; a listed test with another extension fails instead of being skipped. - Upstream's tests are listed as "<test> run by <file>"; the file must name the test and cannot be a disabled upstream workflow. thread-transfer-report.test.cjs was run nowhere (upstream runs it from a workflow_run of its disabled CI); Test 1 runs it now with upstream's other release-check tests. One commit per release run - `changes` resolves a workflow_dispatch/workflow_call ref to a SHA once; every job checks that out, so a branch that moves mid-run cannot split a run over two commits. workflows, lint and the server shards now wait for `changes`. Upstream drift (fork-tests-drift.cjs) - Also digests the top-level env, defaults, concurrency and permissions. - --record refuses (exit 1) while a tracked job is missing instead of saving null, and an old null entry is reported instead of crashing. - --record prints the upstream changes it accepts; the commit and PR must say what was ported for each. Re-recorded .github/fork-tests-upstream.json. It accepts: - ci.yml top-level permissions (contents: read): fork-tests.yml has the same. - ci.yml top-level concurrency (group per PR/sha, cancel on pull_request): fork-tests.yml has the equivalent, with the event name in the group. - release.yml top-level concurrency and permissions: they apply to upstream's release run, not its test jobs. fork-release.yml has its own. No job or step changed upstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
fix round 1 · head 6d6e0b5 · fixes f956059..6d6e0b5 (one commit) · lens comments 6823256526 (workflow) and 6823238680 (scripts) CI: not run on the new head. Each fix was checked red on f956059's code and green on the fix, locally (node 24, git 2.43, gitleaks 8.30.1 and actionlint 1.7.12 pinned by checksum as in the workflow):
The record is re-recorded. It accepts only the four newly digested top-level keys; no upstream job or step changed. ci.yml Unproven: CI on 6d6e0b5 (workflow disabled). The Secret scan on a real GitHub PR event (lazy blob fetch, |
|
lens: quick check r2 · reviewer: opus high (t3lens-6-r2) · read at: 6d6e0b5 covered: the whole fix diff f956059..6d6e0b5 (9 files): fork-secret-scan.sh and its test, the fork-tests.yml changes (secrets job, resolved sha for release runs, needs: changes, thread-transfer-report in Test release checks), fork-ci-tests.cjs/.txt and test, fork-tests-drift.cjs/.json and test. Ran every script test at the head with the stand-in and with the pinned gitleaks 8.30.1 (checksum checked): all pass, drift check passes. Put the f956059 inline scan logic into a script and ran fork-secret-scan.test.sh against it with real gitleaks: 8 of 11 red, including "key added then deleted before a merge of main". Broke the fix by hand: no --remerge-diff -> both merge-commit cases red; PR range back to --max-count= HEAD -> 2 red; push range newest-only -> 2 red. Read CI run 43218151901. checks:
finding: major .github/scripts/fork-secret-scan.sh:79 the checkout is blob:none, so gitleaks makes git fetch file contents on demand. When that fetch fails (network error or GitHub 5xx mid-scan), gitleaks 8.30.1 logs "ERR [git] fatal: could not fetch ... from promisor remote", reports "0 commits scanned", and exits 0. The step summary then says "No secrets found". I reproduced it: blob-less clone, origin made unreachable after the clone, a PR range with a key -> exit 0. In the same state CI 43218151901 at read time: in progress. Lint failed (formatting, above). Secret scan, Changes, Workflow lint and upstream drift, Build and Typecheck, Test 2, Test Server 1-8, Rust, Release Smoke and Mobile Native passed. Test 1, Test Web 1 and Test Web 2 were still running. |
Secret scan (.github/scripts/fork-secret-scan.sh, tests: fork-secret-scan.test.sh) - The checkout is blob-less, so gitleaks fetched file contents on demand. gitleaks 8.30.1 stops at the first git error (a failed content fetch), logs "0 commits scanned" and exits 0, and the summary said "No secrets found". The script now runs gitleaks' own `git log -p -U0 <opts>` first: it fetches every blob the scan reads and fails the job (exit 2) when a fetch fails. gitleaks then runs with GIT_NO_LAZY_FETCH=1. - The job also fails when gitleaks scanned fewer commits than that git log has commits with a hunk in a file they do not delete (what gitleaks counts; a pure rename or a clean merge has none, so it can be below rev-list --count). - New tests: contents unreachable after the clone, gitleaks stopping early, and a base or head commit that cannot be fetched. Lint (vp check) - Formatting of fork-ci-tests.cjs, fork-ci-tests.test.cjs, fork-tests-drift.test.cjs. - The header comments of fork-ci-tests.cjs and fork-tests-drift.cjs cut to 4 lines (t3code/no-low-value-comments). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
fix round 2 · head 675886a · fix range 6d6e0b5..675886a (1 commit) MAJOR: secret scan passed when it could not read the files. Fixed in
MAJOR: Lint. CI: run 43220629206 (pull_request, 675886a) passed: all 21 jobs, Lint, Secret scan and fork-ci-result included. Its scan-range test ran 15/15 with the pinned gitleaks. Unproven:
|
|
lens: quick check r3 · reviewer: opus high (t3lens-6-r3) · read at: 675886a covered: the fix diff 6d6e0b5..675886a in full (fork-secret-scan.sh and its test, fork-tests.yml checkout comment, formatting-only changes in fork-ci-tests.cjs / fork-tests-drift.cjs and their tests), plus what it calls into: gitleaks v8.30.1 source (detect/detect.go DetectSource and addCommit, sources/git.go NewGitLogCmd, listenForStdErr and Fragments, sources/file.go archive handling, cmd/root.go exit codes). Local runs used gitleaks 8.30.1 checked against the workflow's SHA256 (OK), with git 2.43 locally; CI runs git 2.56.0. gh was used read-only, with GH_TOKEN and GITHUB_TOKEN unset. checks:
finding: minor .github/scripts/fork-secret-scan.sh:113 <git writes any stderr line that is not on gitleaks' allow list (rename-limit and auto-gc are allowed) during the gitleaks run, after a pre-fetch that exited 0 -> gitleaks logs Note, not a finding: GIT_NO_LAZY_FETCH=1 has no test and is ignored by git < 2.44 (CI has 2.56.0, so it works there). It is defense in depth behind the pre-fetch. Round-2 majors: (1) fixed at the root (pre-fetch with the same git log, exit 2 on failure, an offline gitleaks run, and a count that matches gitleaks' rule); (2) Lint is green in run 43220629206. |
Why
Upstream's workflows can't run in this fork: they use runner labels and secrets the fork doesn't have. So fork pull requests get no lint, typecheck or tests at all. This PR adds the fork's own CI on GitHub-hosted runners, which are free on a public repository.
What
.github/workflows/fork-tests.ymlruns upstreamci.yml's checks step for step, onpull_requestandpush: main. It also runs as the release test gate throughworkflow_call/workflow_dispatchwithref. Thechangesjob resolvesrefto one commit, and every job checks out that commit, so a branch that moves mid-run can't split a run over two commits..github/actions/ci-changes)base..head, merges included)vp checkvpr typecheck.github/scriptsteststest:cachedshards and the transfer-budget reportcargo fmt --checkandcargo testfor the three cratesscripts/release-smoke.tsfork-ci-resultis the required checkRequire only
fork-ci-resultin a ruleset. It needs every other job and fails when any of them fails or is cancelled. It also fails when a job is skipped without a reason. The two valid reasons are:changessaid the job's gates aren't needed.The summary lists every job with its result, the reason for each skip, and each job's queue and run time. A new job is covered automatically once it is added to
needs.Every test file must run somewhere
.github/scripts/fork-ci-tests.cjschecks every*.test.*file under.github/scripts, subdirectories included, against.github/fork-ci-tests.txt. The list has two kinds of entry:<test>: the workflows job runs it by extension:.shwithbash,.pywithpython3,.cjs/.mjs/.jswithnode --test. A listed test with any other extension fails. PyYAML is installed only when a fork.pytest exists andimport yamlfails.<test> run by <file>: another job or script runs it. That file must name the test's path, and it can't be one of upstream's workflows, which are disabled here. Upstream's.github/scriptstests are listed this way.thread-transfer-report.test.cjswas run nowhere, because upstream runs it from aworkflow_runof its disabled CI. Test 1 now runs it with upstream's other release-check tests.The rule is the same on every event:
fork-follow.test.sh(fork: follow each stable upstream release with the patch series #4) andfork-schema-check.test.cjs(fork: CI check that refuses database schema and migration changes #3).So a test file that ever reached main can't disappear without failing the run, and the open PRs' files don't block this one. Once #3 and #4 merge, every listed file is present and required. Removing a test on purpose means also removing its line from the list, which shows up in the diff. "Named by its runner" is checked by text: the check can't tell whether that line actually executes.
Secret scan
.github/scripts/fork-secret-scan.shruns gitleaks on a full clone without file contents. gitleaks then fetches the contents of only the commits it scans.base.sha..head.sha, which is every commit the PR adds, including merges of main, whatever their dates. A squash merge never brings the PR's own commits to main, but they stay public underrefs/pull/N/head, so they are scanned here.before..after. After a force push, that is every commit the new tip has and the old tip didn't. Commits both tips share were scanned when they were first pushed.--remerge-diff, which shows only what the merge itself changed beyond an automatic merge (a conflict resolution, or a file added in the merge). Plaingit log -pshows a merge with no diff at all.fork-secret-scan.test.shbuilds these cases in local repositories. They include a key added and then deleted before a merge of main, and a key that only a merge commit adds. The Secret scan job runs it against the pinned gitleaks before it scans, and the workflows job runs it with a stand-in.Upstream drift
.github/scripts/fork-tests-drift.cjsdigests every job and step of upstream'sci.ymland the four test jobs ofrelease.yml, plus both files' top-levelenv,defaults,concurrencyandpermissions. It compares them with the digest in.github/fork-tests-upstream.json. It fails with lines likeci.yml: job test_server: step "Restore Vitest module cache" added, and ignores changes that touch only comments. It caught the newer upstream on main when this branch merged main: test-delta tests, Vitest's module cache on the server shards, and web split in two. All three are ported. After porting a change, runnode .github/scripts/fork-tests-drift.cjs --record. It refuses while a tracked job is missing upstream. Otherwise it saves the record and prints every upstream change the record now accepts.Limit: the check compares upstream with its own record, not with
fork-tests.yml. Recording without porting anything still passes. So a commit that changes the record must say, in its message and in the PR body, what was ported for each printed line or why that line doesn't apply. Reviewers check the record diff against that statement. The fix round 1 commit accepts the newly digested top-level keys this way. Upstream'sci.ymlpermissionsandconcurrencyalready have equivalents infork-tests.yml.release.yml'spermissionsandconcurrencyapply to upstream's release run, not its test jobs.Runner image:
ubuntu-24.04, notubuntu-latestubuntu-latestmoved to 26.04 while this was being built. The pin means an OS upgrade happens in a PR that says so, not silently in the middle of a run. A Chromium crash in the web job was first seen on the 26.04 image.Tuning
.github/scripts/fork-test-shards.cjs. A package with at least 100 test files and a plainvp test runscript gets a vitest--shardslice on every shard (desktop, mobile, client-runtime, shared). The rest are placed whole by test-file count, heaviest first onto the lightest shard. This is deterministic, so both shards compute the same split.actions/cachefor typecheck and the server shards. The key isvp-tasks-v1-<job>-<os>-<lockfile hash>-<sha>, restored by prefix. vp fingerprints each task's inputs, so a restored entry only replays a task whose inputs haven't changed.Lessons carried over from another project's CI
concurrencyper PR; main and release runs always finishfork-ci-resultchangescontents: read; onlyfork-ci-resultaddsactions: read(job list);pull_request, neverpull_request_target; no secretsfork-ci-resultfork-ci-resultsummaryfork-tests-drift.cjsagainstci.ymlandrelease.ymlCoverage
Covered: every job of upstream
ci.yml, plusrelease.yml'squality,test,test_webandtest_server. Given aref, every Linux job runs on that commit with no path filter, which is a superset of those four.Not covered, and why:
windows-tests.yml: upstream runs it only byworkflow_dispatch.agent-context-budget: a label-gated budget comparison of a PR against main.mobile-fingerprint-check,web-preview,mobile-eas: they need secrets or deploy somewhere.pr-size,pr-vouch: repository management throughpull_request_target.thread-transfer-report: triggered byworkflow_run.Timings
Wall time from the first job starting to the last job finishing, on GitHub-hosted 4-CPU
ubuntu-24.04runners:With warm caches, Test 1 sets the wall time: the desktop, mobile, client-runtime and shared slices plus the whole packages placed on shard 1. A third Test shard is the next step if this needs to be faster. Lint takes 93 s, Rust 54 s, Release Smoke 58 s, macOS lint 64 s, Secret scan 23 s and Workflows 12 s.
No test was flaky across the measurement runs. The one Chromium crash (connection closed) was on the 26.04 image, before the split.
Cost
All jobs run on GitHub-hosted runners. These are free for a public repository, including the one macOS job (
macos-26, gated to native mobile or workflow changes). No paid or larger runners are used. Peak concurrency is about 19 jobs, against the free plan's limit of 20.One fix to main in this PR
scripts: move release-repository.test.ts out of scripts/lib(two commits). #1 added that test underscripts/lib, whichapps/desktop's typecheck includes, and it importsscripts/build-desktop-artifact.ts, which that project doesn't list (TS6307). Main's Build and Typecheck therefore fails. Inscripts/the test is covered by the scripts project and still runs in the scripts package.Replaces #2's copy
#2 merged with its own
fork-tests.yml. This PR replaces that file and keeps itsworkflow_call/workflow_dispatchinputref, sofork-release.ymlcalls it unchanged.fork-release.ymlalready grants theactions: readthatfork-ci-resultuses.General vs project
Fully general: nothing here is specific to one deployment. Repository facts live in the workflow and its two data files (
.github/fork-ci-tests.txtand.github/fork-tests-upstream.json).🤖 Generated with Claude Code