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