Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9d14bb2
iOS simulators boot pinned to Full Screen Apps: iPadOS 26's Windowed …
thejackshelton Oct 6, 2026
7d39d93
Merge remote-tracking branch 'origin/master' into ios-sim-fullscreen
thejackshelton Oct 6, 2026
79c141d
Regenerate: pnpm regen --jobs 1 (after merging origin/master; the hos…
thejackshelton Oct 6, 2026
aeda780
test-files.yml: run chosen test files on their full-test runners, wit…
thejackshelton Oct 8, 2026
5abc810
Cloud session bootstrap and DRAGON_REQUIRE_NATIVE (cloud migration it…
thejackshelton Oct 8, 2026
8c8808f
Land: LAND_REGEN=ci runs the driver's regens on CI (regen-on-ci.yml p…
thejackshelton Oct 8, 2026
42c5c7e
test-files.yml: a summary job checks every requested file ran in one …
thejackshelton Oct 8, 2026
7413904
test-files.yml: the ref input is a full sha or a branch
thejackshelton Oct 8, 2026
892a687
setup:git and the landing driver share SETUP_GIT_CONFIG (rerere off, …
thejackshelton Oct 8, 2026
dc99465
ci:test-files: say where a dispatched run's checks are filed
thejackshelton Oct 8, 2026
f7747df
Land: regen-on-ci review fixes (#220 Claude review)
thejackshelton Oct 8, 2026
579bce9
Cloud setup: hook on every SessionStart source, export DRAGON_REQUIRE…
thejackshelton Oct 8, 2026
4e5d626
Land: ci-only mode (LAND_CI=only), queued CI runs waited for, CI Andr…
thejackshelton Oct 8, 2026
57a223e
Merge remote-tracking branch 'origin/land-regen-ci' into land-ci-only
thejackshelton Oct 8, 2026
01028d3
Land: R3 tests build both ABIs' records from this tree, so they hold …
thejackshelton Oct 8, 2026
d9736b9
Land: merge ios-sim-fullscreen (#217)
thejackshelton Oct 8, 2026
7e7f8fe
test-files: refuse a file the ref's own test-shards.ts groups elsewhe…
thejackshelton Oct 8, 2026
ce4bc52
Land: merge land-regen-ci (#220)
thejackshelton Oct 8, 2026
70307c2
Land: regenerate after merging ios-sim-fullscreen (#217)
thejackshelton Oct 8, 2026
9bde9b1
Land: regenerate after merging land-regen-ci (#220)
thejackshelton Oct 8, 2026
0104a55
Land: merge cloud-bootstrap (#219)
thejackshelton Oct 8, 2026
e3b21ae
Lane contract and AGENTS.md steps 2-4 for cloud lanes (cloud migratio…
thejackshelton Oct 8, 2026
27adbb5
Land: judge each CI job's queue and run time on its own (#222 Claude …
thejackshelton Oct 8, 2026
7c71160
Land: regenerate after merging cloud-bootstrap (#219)
thejackshelton Oct 8, 2026
42a11b6
Lane contract review fixes: direct-push full test, label-only regen t…
thejackshelton Oct 8, 2026
0347610
Lane contract: READY needs regen, typecheck and ci:test-files verdict…
thejackshelton Oct 8, 2026
48e091e
Merge pull request #217 from compiled-run/ios-sim-fullscreen
thejackshelton Oct 8, 2026
1af63f1
Merge pull request #220 from compiled-run/land-regen-ci
thejackshelton Oct 8, 2026
3099f7b
Merge pull request #219 from compiled-run/cloud-bootstrap
thejackshelton Oct 8, 2026
28b45e4
Land: merge ci-test-files (#221)
thejackshelton Oct 8, 2026
5e77488
Land: regenerate after merging ci-test-files (#221)
thejackshelton Oct 8, 2026
a01848d
Land: merge land-ci-only (#222)
thejackshelton Oct 8, 2026
40f816a
Land: regenerate after merging land-ci-only (#222)
thejackshelton Oct 8, 2026
9dced02
Land: merge lane-contract-cloud (#223)
thejackshelton Oct 8, 2026
e2457b0
Land: regenerate after merging lane-contract-cloud (#223)
thejackshelton Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/scripts/cloud-setup.sh",
"timeout": 900
}
]
}
]
}
}
26 changes: 25 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,34 @@ permissions:
contents: read
jobs:
checks:
runs-on: ubuntu-latest
# ubuntu-24.04 x86_64, the image of Claude Code cloud sessions, so the cloud-setup steps below prove their bootstrap.
runs-on: ubuntu-24.04
steps:
- name: Check out
uses: actions/checkout@v4
# Before setup-node, so the runner has only its image's Node: scripts/cloud-setup.sh must install the pinned Node and pnpm.
- name: Cloud session bootstrap (scripts/cloud-setup.sh), run twice
run: |
set -euo pipefail
test "$(uname -s)-$(uname -m)" = Linux-x86_64
for round in 1 2; do
: > "$RUNNER_TEMP/env"
CLAUDE_CODE_REMOTE=true CLAUDE_ENV_FILE="$RUNNER_TEMP/env" scripts/cloud-setup.sh > "$RUNNER_TEMP/out-$round" 2> "$RUNNER_TEMP/err-$round" || { cat "$RUNNER_TEMP/err-$round"; exit 1; }
cat "$RUNNER_TEMP/err-$round" "$RUNNER_TEMP/out-$round"
# Stdout reaches Claude's context: one summary line only.
test "$(wc -l < "$RUNNER_TEMP/out-$round")" = 1 || { echo "::error::cloud-setup printed more than its summary line on stdout"; exit 1; }
(
# shellcheck disable=SC1091
. "$RUNNER_TEMP/env"
test "$(node -v)" = "v$(sed -n 's/^node //p' <(scripts/cloud-setup.sh --print-versions))" || { echo "::error::node is $(node -v) after sourcing CLAUDE_ENV_FILE"; exit 1; }
test "$(pnpm --version)" = "$(sed -n 's/^pnpm //p' <(scripts/cloud-setup.sh --print-versions))" || { echo "::error::pnpm is $(pnpm --version)"; exit 1; }
test "${DRAGON_REQUIRE_NATIVE:-}" = 1 || { echo "::error::DRAGON_REQUIRE_NATIVE is not exported as 1"; exit 1; }
pnpm typecheck
)
done
# The second run found Node and pnpm in place and installed nothing.
if grep -E 'installing (Node|pnpm)' "$RUNNER_TEMP/err-2"; then echo "::error::the second cloud-setup run reinstalled"; exit 1; fi
grep -q 'installing Node' "$RUNNER_TEMP/err-1" || { echo "::error::the first run did not install Node, so the install path is unproven"; exit 1; }
- name: Set up pnpm 10.33.2
uses: pnpm/action-setup@v4
with:
Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/regen-on-ci-round.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# side chrome (macos-26, where the Chrome captures are byte-identical to the local Mac's): every step except lanes-host.
# side host (xcode-27, Xcode 27.0 and Swift 6.4 like the local Mac): lanes-host only. The host toolchains must equal the ones
# the committed lanes.json records, or the round fails before running anything.
# save_cache false (regen-on-ci.yml's patch mode): nothing is saved, as in full-test.yml; the regen and WPT caches are restored
# only and the pnpm store is not cached (setup-node's cache always saves).
name: regen-on-ci-round
on:
workflow_call:
Expand All @@ -13,6 +15,7 @@ on:
sha: { type: string, required: true }
prev: { type: string, required: false, default: '' }
name: { type: string, required: true }
save_cache: { type: boolean, required: false, default: true }
outputs:
changed:
description: "'true' when this round changed any file"
Expand Down Expand Up @@ -45,7 +48,8 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: '24.15.0'
cache: pnpm
# setup-node's pnpm cache saves in a post step, so a round that must not save (patch mode) does not use it.
cache: ${{ inputs.save_cache && 'pnpm' || '' }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Restore the regen cache (first round)
Expand Down Expand Up @@ -73,11 +77,18 @@ jobs:
test "$(pnpm --filter @dragon/parity exec playwright --version)" = "Version 1.58.2"
pnpm --filter @dragon/parity exec playwright install chromium
- name: Chrome side - cache the WPT copy
if: inputs.side == 'chrome'
if: inputs.side == 'chrome' && inputs.save_cache
uses: actions/cache@v4
with:
path: /Users/runner/wpt
key: wpt-${{ hashFiles('packages/wpt/wpt.lock') }}
# Restored only: a patch-mode run regenerates an unreviewed tree at master's cache scope, so it never saves into it.
- name: Chrome side - restore the WPT copy (patch mode)
if: inputs.side == 'chrome' && !inputs.save_cache
uses: actions/cache/restore@v4
with:
path: /Users/runner/wpt
key: wpt-${{ hashFiles('packages/wpt/wpt.lock') }}
- name: Chrome side - fetch the WPT copy when the cache missed
if: inputs.side == 'chrome'
run: bash scripts/fetch-wpt.sh
Expand Down Expand Up @@ -130,7 +141,7 @@ jobs:
path: ${{ runner.temp }}/out
if-no-files-found: warn
- name: Save the regen cache
if: success()
if: success() && inputs.save_cache
uses: actions/cache/save@v4
with:
path: .git/dragon-regen/v2
Expand Down
72 changes: 61 additions & 11 deletions .github/workflows/regen-on-ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CI-OFFLOAD step 2: `pnpm regen` for a branch on GitHub-hosted runners, pushed back to the branch as its own commit.
# Trigger: workflow_dispatch with a branch, or the `regen` label on a pull request from this repository.
# Patch mode (the landing driver, LAND_REGEN=ci): workflow_dispatch with a commit `sha` instead of a branch. Nothing is pushed:
# the final combined outputs.patch (against that commit, the tw-sweep round's) is the artifact regen-patch, and no cache is saved
# (the regen and WPT caches are restored only, the pnpm store is not cached), since a dispatched run regenerates an unreviewed
# tree at master's cache scope.
# The chain is split by machine (regen-on-ci-round.yml): the Chrome steps on macos-26, whose captures are byte-identical to the
# local Mac's (capture-proof.yml), and lanes-host on xcode-27, the image with the local Mac's Xcode 27.0. Rounds alternate until
# one changes nothing (at most 5). tw-sweep, which no other step reads and which takes about 50 min on a 3-core runner, runs
Expand All @@ -9,12 +13,17 @@
# Every push to master runs the same chain without pushing ("warm"): Actions caches made in a pull request are visible only to
# that pull request, while master's are visible to every branch, so master's run keeps the regen cache warm for all of them.
name: regen-on-ci
run-name: ${{ inputs.sha && format('regen of {0}', inputs.sha) || (github.event_name == 'pull_request' && github.event.pull_request.title) || (github.event_name == 'push' && github.event.head_commit.message) || 'regen-on-ci' }}
on:
workflow_dispatch:
inputs:
branch:
description: Branch to regenerate (never master)
required: true
description: Branch to regenerate and push to (never master); leave empty to give a sha
required: false
type: string
sha:
description: Commit to regenerate without pushing (patch mode, the landing driver); the artifact regen-patch is the result
required: false
type: string
pull_request:
types: [labeled]
Expand All @@ -25,7 +34,7 @@ permissions:
# Master's cache runs share one group and cancel each other: only the newest master matters. A branch's runs have their own
# group, per branch and per label, so a label event that is not `regen` never takes a queued regen's place.
concurrency:
group: ${{ github.event_name == 'push' && 'regen-on-ci-master-warm' || format('regen-on-ci-{0}-{1}', inputs.branch || github.head_ref, github.event.label.name || 'dispatch') }}
group: ${{ github.event_name == 'push' && 'regen-on-ci-master-warm' || format('regen-on-ci-{0}-{1}', inputs.sha || inputs.branch || github.head_ref, github.event.label.name || 'dispatch') }}
cancel-in-progress: ${{ github.event_name == 'push' }}
jobs:
resolve:
Expand All @@ -45,17 +54,20 @@ jobs:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
DISPATCH_BRANCH: ${{ inputs.branch }}
DISPATCH_SHA: ${{ inputs.sha }}
PR_BRANCH: ${{ github.head_ref }}
PR_REPO: ${{ github.event.pull_request.head.repo.full_name }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" = push ]; then
# Branch regens come first: the macOS runners are few (5 at once), so a master cache run yields to any branch regen
# queued or running, and the next master push warms the cache instead.
# queued or running, and the next master push warms the cache instead. Patch-mode runs ("regen of <sha>", the landing
# driver's) never make it yield: they are nearly always in flight during a landing, save no cache themselves, and
# restore master's, so master's run must keep saving it.
busy=0
for st in queued in_progress waiting pending; do
n=$(gh api "repos/$REPO/actions/workflows/regen-on-ci.yml/runs?status=$st&per_page=100" -q '[.workflow_runs[] | select(.event != "push")] | length')
n=$(gh api "repos/$REPO/actions/workflows/regen-on-ci.yml/runs?status=$st&per_page=100" -q '[.workflow_runs[] | select(.event != "push" and (.event != "workflow_dispatch" or ((.display_title // "") | startswith("regen of ") | not)))] | length')
busy=$((busy + n))
done
if [ "$busy" -gt 0 ]; then
Expand All @@ -67,6 +79,13 @@ jobs:
echo "regen-on-ci: warming the regen cache on master at $GITHUB_SHA (nothing is pushed)" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ] && [ -n "$DISPATCH_SHA" ]; then
[ -z "$DISPATCH_BRANCH" ] || { echo "::error::give a branch or a sha, not both"; exit 1; }
[[ "$DISPATCH_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::not a full commit sha: $DISPATCH_SHA"; exit 1; }
{ echo "branch="; echo "sha=$DISPATCH_SHA"; echo "mode=patch"; } >> "$GITHUB_OUTPUT"
echo "regen-on-ci: regen of $DISPATCH_SHA (patch mode: nothing is pushed, caches are restored only)" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then branch=$DISPATCH_BRANCH; else branch=$PR_BRANCH
[ "$PR_REPO" = "$REPO" ] || { echo "::error::the pull request comes from $PR_REPO, not $REPO; regen-on-ci pushes only to this repository"; exit 1; }
fi
Expand All @@ -83,26 +102,26 @@ jobs:
needs: resolve
if: needs.resolve.outputs.mode != 'skip'
uses: ./.github/workflows/regen-on-ci-round.yml
with: { runner: macos-26, side: chrome, args: '--skip lanes-host --skip tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', name: chrome-1 }
with: { runner: macos-26, side: chrome, args: '--skip lanes-host --skip tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', name: chrome-1, save_cache: "${{ needs.resolve.outputs.mode != 'patch' }}" }
host-1:
needs: [resolve, chrome-1]
uses: ./.github/workflows/regen-on-ci-round.yml
with: { runner: xcode-27, side: host, args: '--only lanes-host', sha: '${{ needs.resolve.outputs.sha }}', prev: chrome-1, name: host-1 }
with: { runner: xcode-27, side: host, args: '--only lanes-host', sha: '${{ needs.resolve.outputs.sha }}', prev: chrome-1, name: host-1, save_cache: "${{ needs.resolve.outputs.mode != 'patch' }}" }
chrome-2:
needs: [resolve, host-1]
if: needs.host-1.outputs.changed == 'true'
uses: ./.github/workflows/regen-on-ci-round.yml
with: { runner: macos-26, side: chrome, args: '--skip lanes-host --skip tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', prev: host-1, name: chrome-2 }
with: { runner: macos-26, side: chrome, args: '--skip lanes-host --skip tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', prev: host-1, name: chrome-2, save_cache: "${{ needs.resolve.outputs.mode != 'patch' }}" }
host-2:
needs: [resolve, chrome-2]
if: needs.chrome-2.outputs.changed == 'true'
uses: ./.github/workflows/regen-on-ci-round.yml
with: { runner: xcode-27, side: host, args: '--only lanes-host', sha: '${{ needs.resolve.outputs.sha }}', prev: chrome-2, name: host-2 }
with: { runner: xcode-27, side: host, args: '--only lanes-host', sha: '${{ needs.resolve.outputs.sha }}', prev: chrome-2, name: host-2, save_cache: "${{ needs.resolve.outputs.mode != 'patch' }}" }
chrome-3:
needs: [resolve, host-2]
if: needs.host-2.outputs.changed == 'true'
uses: ./.github/workflows/regen-on-ci-round.yml
with: { runner: macos-26, side: chrome, args: '--skip lanes-host --skip tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', prev: host-2, name: chrome-3 }
with: { runner: macos-26, side: chrome, args: '--skip lanes-host --skip tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', prev: host-2, name: chrome-3, save_cache: "${{ needs.resolve.outputs.mode != 'patch' }}" }
converge:
needs: [resolve, chrome-1, host-1, chrome-2, host-2, chrome-3]
if: always() && needs.resolve.result == 'success' && needs.resolve.outputs.mode != 'skip' && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
Expand Down Expand Up @@ -131,7 +150,7 @@ jobs:
needs: [resolve, converge]
if: always() && needs.converge.result == 'success'
uses: ./.github/workflows/regen-on-ci-round.yml
with: { runner: macos-26, side: chrome, args: '--only tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', prev: '${{ needs.converge.outputs.last }}', name: sweep }
with: { runner: macos-26, side: chrome, args: '--only tw-sweep', sha: '${{ needs.resolve.outputs.sha }}', prev: '${{ needs.converge.outputs.last }}', name: sweep, save_cache: "${{ needs.resolve.outputs.mode != 'patch' }}" }
warm-check:
needs: [resolve, sweep]
if: always() && needs.sweep.result == 'success' && needs.resolve.outputs.mode == 'warm'
Expand All @@ -149,6 +168,37 @@ jobs:
exit 1
fi
echo "regen-on-ci: master at ${{ needs.resolve.outputs.sha }} is at a fixed point; the regen cache is warm" >> "$GITHUB_STEP_SUMMARY"
# Patch mode: the final combined patch alone (the sweep artifact also holds the regen cache and logs), for the landing driver to
# apply with git apply --binary --index. Empty when the commit is already at its fixed point.
patch:
needs: [resolve, sweep]
if: always() && needs.sweep.result == 'success' && needs.resolve.outputs.mode == 'patch'
runs-on: ubuntu-latest
steps:
- name: Download the sweep round (the converged tree with tw-sweep run)
uses: actions/download-artifact@v4
with:
name: sweep
path: ${{ runner.temp }}/last
- name: The combined outputs.patch
env:
SHA: ${{ needs.resolve.outputs.sha }}
run: |
set -euo pipefail
test -f "$RUNNER_TEMP/last/outputs.patch" || { echo "::error::the sweep round has no outputs.patch"; exit 1; }
mkdir -p "$RUNNER_TEMP/patch"
cp "$RUNNER_TEMP/last/outputs.patch" "$RUNNER_TEMP/patch/outputs.patch"
if [ -s "$RUNNER_TEMP/patch/outputs.patch" ]; then
echo "regen-on-ci: regen of $SHA changes files: $(git apply --stat "$RUNNER_TEMP/patch/outputs.patch" | tail -1)" | tee -a "$GITHUB_STEP_SUMMARY"
else
echo "regen-on-ci: $SHA is at a fixed point; the patch is empty" | tee -a "$GITHUB_STEP_SUMMARY"
fi
- name: Upload the patch
uses: actions/upload-artifact@v4
with:
name: regen-patch
path: ${{ runner.temp }}/patch/outputs.patch
if-no-files-found: error
push:
needs: [resolve, sweep]
if: always() && needs.sweep.result == 'success' && needs.resolve.outputs.mode == 'push'
Expand Down
Loading
Loading