authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-04 12:43:04-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-04 19:59:06-07:00
log407dfe55dbe42cb807256f899650e1042bc2c8e4
tree2aa91617a06be1e158fb58b97ec0a0fafffebb99
parentd0b4d9575d4f0d808a7c00d9ff90846b5721ee3f
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

chore: CI checks out in workspaces/ci inside the checkout that holds the repo

A nested workspace finds that checkout through its `.jj/repo` file, so ci.py from any workspace shares one CI workspace and the main checkout's toolchains. Assisted-by: claude-opus-5.5

3 files changed, 17 insertions(+), 11 deletions(-)

.gitignore+2
...@@ -10,6 +10,8 @@ __pycache__/...@@ -10,6 +10,8 @@ __pycache__/
10/soak/10/soak/
11/tools/w7/payload/vendor/11/tools/w7/payload/vendor/
12/resources/12/resources/
13# jj workspaces for CI, releases and agents, inside the checkout that holds the repo.
14/workspaces/
13/.env15/.env
1416
15# The global ignore excludes AGENTS.md; this repo keeps its own.17# The global ignore excludes AGENTS.md; this repo keeps its own.
tools/TESTING.md+6-6
...@@ -3,11 +3,11 @@...@@ -3,11 +3,11 @@
3## The CI gate3## The CI gate
44
5`python3 tools/ci.py` gates `main` (or `--rev REV`, or `--working-copy` for this5`python3 tools/ci.py` gates `main` (or `--rev REV`, or `--working-copy` for this
6checkout's `@`) in the jj workspace `../snowbound-ci`: it points that6checkout's `@`) in the jj workspace `workspaces/ci`: it points that
7workspace's own commit, a child of `main`, at the revision's files, so other7workspace's own commit, a child of `main`, at the revision's files, so other
8checkouts' edits in progress never reach the result, and a working copy is8checkouts' edits in progress never reach the result, and a working copy is
9frozen as it was when the run started. Its build cache is9frozen as it was when the run started. Its build cache is
10`../snowbound-ci/target`, apart from every agent's `target/`. Runs wait for one10`workspaces/ci/target`, apart from every agent's `target/`. Runs wait for one
11another, and the exit status is the result.11another, and the exit status is the result.
1212
13| Lane | Runs |13| Lane | Runs |
...@@ -31,17 +31,17 @@ python3 tools/ci.py --rev xyz --lanes test windows # `windows` names both wind...@@ -31,17 +31,17 @@ python3 tools/ci.py --rev xyz --lanes test windows # `windows` names both wind
31Lanes run four at a time (`--jobs`), each under its own time limit31Lanes run four at a time (`--jobs`), each under its own time limit
32(`--timeout MINUTES` overrides them all). The table it prints names each32(`--timeout MINUTES` overrides them all). The table it prints names each
33failure's first errors with their files and lines, to tell whose edit broke33failure's first errors with their files and lines, to tell whose edit broke
34it; `../snowbound-ci/target/ci/runs/TIME/` keeps every lane's log and34it; `workspaces/ci/target/ci/runs/TIME/` keeps every lane's log and
35`summary.json` (status, seconds, errors with files, each test executable's35`summary.json` (status, seconds, errors with files, each test executable's
36time), for the last 20 runs. After a run over `--budget` (80 GB), it deletes36time), for the last 20 runs. After a run over `--budget` (40 GB), it deletes
37the build units this run didn't use, least recently used first, which keeps37the build units this run didn't use, least recently used first, which keeps
38`deps/` small for the font tests that scan it. Windows needs llvm-mingw from38`deps/` small for the font tests that scan it. Windows needs llvm-mingw from
39`platform/windows/toolchain.sh` in this checkout's `target/windows`, or39`platform/windows/toolchain.sh` in the main checkout's `target/windows`, or
40`LLVM_MINGW`; Linux needs `zig`. `release.py` runs the gate on the commit it40`LLVM_MINGW`; Linux needs `zig`. `release.py` runs the gate on the commit it
41publishes.41publishes.
4242
43The workspace is made on first use; to drop it, `jj workspace forget ci` and43The workspace is made on first use; to drop it, `jj workspace forget ci` and
44delete `../snowbound-ci`.44delete `workspaces/ci`.
4545
46## Public fixtures46## Public fixtures
4747
tools/ci.py+9-5
...@@ -1,6 +1,6 @@...@@ -1,6 +1,6 @@
1#!/usr/bin/env python31#!/usr/bin/env python3
2"""Gates a revision (main by default) on formatting, Clippy, the tests, the Python suite and2"""Gates a revision (main by default) on formatting, Clippy, the tests, the Python suite and
3every platform's build, checked out in the jj workspace ../snowbound-ci so that edits in3every platform's build, checked out in the jj workspace workspaces/ci so that edits in
4progress elsewhere never reach it. See tools/TESTING.md."""4progress elsewhere never reach it. See tools/TESTING.md."""
5import argparse5import argparse
6from concurrent.futures import ThreadPoolExecutor6from concurrent.futures import ThreadPoolExecutor
...@@ -18,7 +18,10 @@ import threading...@@ -18,7 +18,10 @@ import threading
18import time18import time
1919
20ROOT = Path(__file__).resolve().parents[1]20ROOT = Path(__file__).resolve().parents[1]
21CI = ROOT.parent / 'snowbound-ci'21# The checkout holding the jj repository, which a nested workspace's `.jj/repo` file names.
22STORE = ROOT / '.jj/repo'
23MAIN = (STORE.parent / STORE.read_text()).resolve().parents[1] if STORE.is_file() else ROOT
24CI = MAIN / 'workspaces/ci'
22TARGET = CI / 'target'25TARGET = CI / 'target'
23RUNS = TARGET / 'ci'26RUNS = TARGET / 'ci'
24# Changes here rebuild every package.27# Changes here rebuild every package.
...@@ -43,8 +46,8 @@ def lanes():...@@ -43,8 +46,8 @@ def lanes():
43 '--', '-D', 'warnings']46 '--', '-D', 'warnings']
44 windows = CI / 'platform/windows/cargo.sh'47 windows = CI / 'platform/windows/cargo.sh'
45 linux = ROOT / 'platform/linux/cargo.sh'48 linux = ROOT / 'platform/linux/cargo.sh'
46 mingw = os.environ.get('LLVM_MINGW') or ROOT / 'target/windows/llvm-mingw'49 mingw = os.environ.get('LLVM_MINGW') or MAIN / 'target/windows/llvm-mingw'
47 sdk = Path(os.environ.get('SNOW_LEOPARD_SDK') or ROOT / 'target/snow-leopard/MacOSX10.6.sdk')50 sdk = Path(os.environ.get('SNOW_LEOPARD_SDK') or MAIN / 'target/snow-leopard/MacOSX10.6.sdk')
48 nightly = subprocess.run(['rustup', 'component', 'list', '--installed', '--toolchain', 'nightly'],51 nightly = subprocess.run(['rustup', 'component', 'list', '--installed', '--toolchain', 'nightly'],
49 capture_output=True, text=True).stdout.split()52 capture_output=True, text=True).stdout.split()
50 result = [53 result = [
...@@ -122,6 +125,7 @@ def checkout(rev):...@@ -122,6 +125,7 @@ def checkout(rev):
122 if len(commits) != 1:125 if len(commits) != 1:
123 sys.exit(f'{rev} names {len(commits)} revisions, not one.')126 sys.exit(f'{rev} names {len(commits)} revisions, not one.')
124 if not CI.exists():127 if not CI.exists():
128 CI.parent.mkdir(exist_ok=True)
125 jj('workspace', 'add', '--name', 'ci', '-r', 'main', str(CI))129 jj('workspace', 'add', '--name', 'ci', '-r', 'main', str(CI))
126 jj('workspace', 'update-stale', cwd=CI)130 jj('workspace', 'update-stale', cwd=CI)
127 jj('rebase', '-r', '@', '-o', 'main', cwd=CI)131 jj('rebase', '-r', '@', '-o', 'main', cwd=CI)
...@@ -376,7 +380,7 @@ def main():...@@ -376,7 +380,7 @@ def main():
376 parser.add_argument('--test-jobs', type=int, default=4, help='Test executables at once (default 4)')380 parser.add_argument('--test-jobs', type=int, default=4, help='Test executables at once (default 4)')
377 parser.add_argument('--timeout', type=float, metavar='MINUTES', help="Each lane's limit, overriding its own")381 parser.add_argument('--timeout', type=float, metavar='MINUTES', help="Each lane's limit, overriding its own")
378 parser.add_argument('--budget', type=float, default=40, metavar='GB',382 parser.add_argument('--budget', type=float, default=40, metavar='GB',
379 help='Prune ../snowbound-ci/target to this size after the run (default 40)')383 help='Prune workspaces/ci/target to this size after the run (default 40)')
380 args = parser.parse_args()384 args = parser.parse_args()
381 if ROOT == CI:385 if ROOT == CI:
382 sys.exit(f'Run ci.py from another checkout; {CI} is its own.')386 sys.exit(f'Run ci.py from another checkout; {CI} is its own.')