Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ jobs:
checks:
# 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
# A run takes 3 to 5 minutes (install, typecheck, the platform-free and registry suites). 30 minutes leaves six times that,
# and stays well under the landing driver's CI wait (LAND_CI_WAIT, 90 min), so a run that hangs ends as a failed check, a
# verdict on the tree, rather than a wait the driver gives up on as a CI outage.
timeout-minutes: 30
steps:
- name: Check out
uses: actions/checkout@v4
Expand Down Expand Up @@ -50,6 +54,10 @@ jobs:
with:
node-version: '24'
cache: pnpm
# land.yml holds the landing token and land-checks.yml runs a tree's commands for it; actionlint (with shellcheck) checks
# their expressions and shell steps. Only these two: the others use custom runner labels actionlint does not know.
- name: Lint the landing workflows (actionlint 1.7.12)
run: docker run --rm -v "$PWD:/repo" --workdir /repo rhysd/actionlint:1.7.12 -color .github/workflows/land.yml .github/workflows/land-checks.yml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
Expand Down
144 changes: 144 additions & 0 deletions .github/workflows/land-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# The landing driver's tree checks (LAND_TRUSTED, scripts/land-devices-ci.ts checksWorkflow): the commands of a landing tree that
# the token-holding land.yml job must never run itself. Dispatched on master by the driver for a tree's commit, pushed apart to
# land-checks/c-<sha>. With no secrets and contents read only, it installs the tree, merges a device run's outcomes into its lane
# records (devices_run), and runs the asked checks: typecheck, evidence:stamp --compare <prev>, parity:lanes. Every command's exit
# code and output go into result.json, and the merged records into outputs.patch; the driver judges them as data.
# Each tree command runs under `timeout`, so a hang is that command's recorded failure (exit 124), not a run that never ends:
# install 30 min, the device merge 15 min, typecheck 40 min, evidence stamp 20 min, parity:lanes 20 min (each several times its
# usual run; a SIGKILL follows 60 s after the SIGTERM). The job's 150 min covers all of them with setup. A failed "Land checks"
# step, or a completed run whose artifact is missing, malformed or refused, is the tree's failure (the driver blames the PR);
# only setup steps and the runner are CI trouble. Each command runs in a session of its own, killed whole (pkill -s) once the
# command ends, so nothing it left behind in its session rewrites the results. A process that leaves the session (its own
# setsid) can still write them: whatever it does, the run's artifact is the tree's, and a bad one fails the PR, never CI.
name: land-checks
run-name: ${{ format('land checks of {0}', inputs.sha) }}
on:
workflow_dispatch:
inputs:
sha:
description: The landing tree's commit
required: true
type: string
prev:
description: The previous position (evidence:stamp --compare)
required: true
type: string
checks:
description: Comma-separated checks to run after the install (typecheck, stamp, lanes)
required: false
default: ''
type: string
devices_run:
description: A device-lanes.yml run whose device-outcomes are merged into the tree's lane records first; empty for none
required: false
default: ''
type: string
permissions:
contents: read
actions: read
concurrency:
group: ${{ format('land-checks-{0}', inputs.sha) }}
cancel-in-progress: false
jobs:
checks:
runs-on: ubuntu-latest
timeout-minutes: 150
steps:
- name: Check the inputs
env:
SHA: ${{ inputs.sha }}
PREV: ${{ inputs.prev }}
CHECKS: ${{ inputs.checks }}
DEVICES_RUN: ${{ inputs.devices_run }}
run: |
[[ "$SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::sha '$SHA' is not a full sha"; exit 1; }
[[ "$PREV" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::prev '$PREV' is not a full sha"; exit 1; }
[[ "$CHECKS" =~ ^((typecheck|stamp|lanes)(,(typecheck|stamp|lanes))*)?$ ]] || { echo "::error::checks '$CHECKS' is not a list of typecheck, stamp, lanes"; exit 1; }
[[ "$DEVICES_RUN" =~ ^([1-9][0-9]*)?$ ]] || { echo "::error::devices_run '$DEVICES_RUN' is not a run id"; exit 1; }
- name: Check out the tree
uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
fetch-depth: 0
persist-credentials: false
- name: Set up pnpm 10.33.2
uses: pnpm/action-setup@v4
with:
version: 10.33.2
# No dependency cache: an unreviewed tree must not save one at master's scope.
- name: Set up Node 24.15.0
uses: actions/setup-node@v4
with:
node-version: '24.15.0'
- name: Download the device outcomes
if: inputs.devices_run != ''
env:
GH_TOKEN: ${{ github.token }}
DEVICES_RUN: ${{ inputs.devices_run }}
run: gh run download "$DEVICES_RUN" --repo "$GITHUB_REPOSITORY" -n device-outcomes -D "$RUNNER_TEMP/outcomes"
- name: Land checks
env:
SHA: ${{ inputs.sha }}
PREV: ${{ inputs.prev }}
CHECKS: ${{ inputs.checks }}
DEVICES_RUN: ${{ inputs.devices_run }}
run: |
# Every command's failure is a result, not this step's: no -e.
set +e -u -o pipefail
out="$RUNNER_TEMP/land-checks"
parts="$RUNNER_TEMP/land-checks-parts"
mkdir -p "$out" "$parts"
[ "$(git rev-parse HEAD)" = "$SHA" ] || { echo "::error::checked out $(git rev-parse HEAD), not $SHA"; exit 1; }
# One command: its exit code, and the last 200 KB of its stdout and stderr.
# Each command runs in a session of its own (setsid, with no job control, does not fork, so the session id is its pid);
# once it ends, whatever it left in that session is killed, so no process of it rewrites the results.
record() {
local name=$1 limit=$2; shift 2
setsid timeout --kill-after=60s "$limit" "$@" > "$parts/$name.out" 2> "$parts/$name.err" &
local sid=$!
wait "$sid"
local status=$?
pkill -KILL -s "$sid" 2> /dev/null || true
tail -c 200000 "$parts/$name.out" > "$parts/$name.out.tail"
tail -c 200000 "$parts/$name.err" > "$parts/$name.err.tail"
jq -n --arg name "$name" --argjson status "$status" --rawfile stdout "$parts/$name.out.tail" --rawfile stderr "$parts/$name.err.tail" \
'{($name): {status: $status, stdout: $stdout, stderr: $stderr}}' > "$parts/$name.json"
echo "$name: exit $status"
return "$status"
}
jq -n --arg sha "$SHA" '{sha: $sha}' > "$parts/sha.json"
: > "$out/outputs.patch"
ok=1
merged=0
record install 30m pnpm install --frozen-lockfile || ok=0
if [ "$ok" = 1 ] && [ -n "$DEVICES_RUN" ]; then
record merge 15m node --conditions=dragon-internal packages/parity/src/cli/device-ci.ts merge "$RUNNER_TEMP/outcomes"
st=$?
if [ "$st" = 0 ] || [ "$st" = 1 ]; then merged=1; else ok=0; fi
fi
if [ "$ok" = 1 ]; then
for c in ${CHECKS//,/ }; do
case "$c" in
typecheck) record typecheck 40m pnpm typecheck ;;
stamp) record stamp 20m pnpm -s evidence:stamp --compare "$PREV" ;;
lanes) record lanes 20m pnpm -s run parity:lanes ;;
esac
done
fi
if [ "$merged" = 1 ]; then
git add -A packages/parity/out
git diff --cached --binary HEAD -- packages/parity/out > "$out/outputs.patch"
fi
jq -s add "$parts"/*.json > "$out/result.json"
echo "result.json: $(jq -c 'with_entries(if .key == "sha" then . else .value |= .status end)' "$out/result.json")"
# Whatever the step left, so a step the tree's commands ended still hands back what it has.
- name: Upload the results
if: always()
uses: actions/upload-artifact@v4
with:
name: land-checks
path: |
${{ runner.temp }}/land-checks/result.json
${{ runner.temp }}/land-checks/outputs.patch
if-no-files-found: warn
retention-days: 7
189 changes: 189 additions & 0 deletions .github/workflows/land.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# The landing driver (scripts/land.ts, AGENTS.md step 7) on a GitHub runner (cloud migration item 9).
#
# How the PM dispatches: `pnpm land:dispatch <branch>:<pr>:<clean-head> ...` (scripts/land-actions.ts). It waits until no land.yml
# run waits to start, dispatches on master, and checks no racing dispatch replaced its run: GitHub keeps one pending run per
# concurrency group and cancels the older, which would drop that run's queue. Never `gh workflow run land.yml` by hand while a
# land run is queued. To stop after the current batch, add the land-stop label to issue vars.LAND_STOP_ISSUE (the PM clears it).
#
# The job that holds LAND_TOKEN runs no code of a PR or a merged tree (LAND_TRUSTED, scripts/land-lib.ts treeCodeRefusal):
# - its checkout is this workflow's own commit (master at dispatch time), without persisted credentials, and only that checkout
# is installed and run;
# - the driver's merges use the trusted checkout's merge drivers by absolute path, and the driver never pulls what merges into it;
# - every command of a tree (install, typecheck, evidence stamp, lane judgement, device-outcome merge, regen, full test, device
# lanes) runs in a workflow the driver dispatches on master for a scratch ref (land-checks.yml, regen-on-ci.yml, full-test.yml,
# device-lanes.yml), with no secrets; the driver reads their results as data;
# - LAND_TOKEN is only in the driver step's environment; the driver takes it out of its own and hands it only to git push and to
# its own gh calls, per call. GITHUB_TOKEN (read-only here) serves every other read.
# LAND_TOKEN lives in the `land` environment, which the owner restricts to master. LAND_GLOBAL_LOCK takes refs/tags/land-lock, so a
# Mac driver (run with LAND_GLOBAL_LOCK=1) and this one never land at once.
# One batch per job (LAND_MAX_BATCHES=1), for the 6-hour job limit: the redispatch job dispatches the rest of the queue when the
# batch ended normally and no stop is set. A new run reconciles from GitHub (skips merged PRs, proves an unproved master first).
name: land
run-name: ${{ inputs.queue == '' && 'land (empty queue)' || 'land' }}
on:
workflow_dispatch:
inputs:
queue:
description: 'Queue entries <branch>:<pr>:<clean-head>, separated by newlines or spaces'
required: false
default: ''
type: string
permissions:
contents: read
jobs:
land:
runs-on: ubuntu-latest
environment: land
timeout-minutes: 350
# On the job, not the run: the redispatch job below runs after this one ends, while the next run's land job may start.
concurrency:
group: land
cancel-in-progress: false
permissions:
contents: read
issues: read
pull-requests: read
statuses: read
checks: read
actions: read
outputs:
count: ${{ steps.queue.outputs.count }}
env:
GH_TOKEN: ${{ github.token }}
LAND_CI: only
LAND_TRUSTED: '1'
LAND_GLOBAL_LOCK: '1'
LAND_MAX_BATCHES: '1'
LAND_PIPELINE: '0'
LAND_QUEUE_EMPTY_OK: '1'
LAND_REVIEW_SOURCE: comment
LAND_REVIEWERS: ${{ vars.LAND_REVIEWERS }}
LAND_STOP_ISSUE: ${{ vars.LAND_STOP_ISSUE }}
LAND_TOKEN_USER_ID: ${{ vars.LAND_TOKEN_USER_ID }}
LAND_PROOF_WRITERS: ${{ vars.LAND_PROOF_WRITERS }}
steps:
# Fail fast: a queue needs LAND_TOKEN and lands only from master; an empty queue only proves the start-up.
- name: Check the token and the ref
env:
QUEUE: ${{ inputs.queue }}
HAS_LAND_TOKEN: ${{ secrets.LAND_TOKEN != '' }}
STOP_ISSUE: ${{ vars.LAND_STOP_ISSUE }}
run: |
if [ -z "$(printf '%s' "$QUEUE" | tr -d '[:space:]')" ]; then
[ "$HAS_LAND_TOKEN" = true ] || echo "::warning::LAND_TOKEN is not set in the land environment; the queue is empty, so this run only proves the start-up. A queue would fail here."
exit 0
fi
if [ "$HAS_LAND_TOKEN" != true ]; then
echo "::error::LAND_TOKEN is not set in the land environment. Add a GitHub App token or fine-grained PAT with contents, pull-requests, actions and statuses write: pushes and merges made with GITHUB_TOKEN start no CI, so the driver would wait forever."
exit 1
fi
if [ "$GITHUB_REF_NAME" != master ]; then
echo "::error::land.yml lands a queue only from master (dispatched on $GITHUB_REF_NAME); dispatch it with scripts/land-actions.ts dispatch (the land:dispatch script)."
exit 1
fi
# The land-stop label is the only way to stop a run here after its batch (the Mac's stop file cannot reach it).
if ! [[ "$STOP_ISSUE" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::The repository variable LAND_STOP_ISSUE is '$STOP_ISSUE', not an issue number. Set it to the tracking issue whose land-stop label stops the driver after its batch: without it nothing can stop the chain of runs but cancelling one mid-merge."
exit 1
fi
- name: Free disk space
run: |
{
echo "LAND_LOG_DIR=$RUNNER_TEMP/land-logs"
echo "LAND_HANDOFF=$RUNNER_TEMP/land-logs/handoff.json"
echo "LAND_QUEUE_FILE=$RUNNER_TEMP/land-queue.txt"
} >> "$GITHUB_ENV"
df -h /
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL /usr/local/share/boost /usr/share/swift
sudo docker image prune --all --force > /dev/null || true
df -h /
- name: Check out this workflow's commit (trusted)
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
fetch-depth: 0
persist-credentials: false
# No package manager runs in this job: its package.json commands are not reviewed as code, and the driver's scripts import
# only node: built-ins (land-trusted.test.ts checks their import closure), so nothing is installed.
- name: Set up Node 24.15.0
uses: actions/setup-node@v4
with:
node-version: '24.15.0'
# What pnpm setup:git sets, with the merge drivers as the trusted checkout's scripts by absolute path (the driver checks
# the same, trustedGitConfig), and a tree's symlinks checked out as plain files.
- name: Set up git
run: |
git config rerere.enabled false
git config core.symlinks false
git config merge.dragon-generated.name 'keep this side; pnpm regen rebuilds it'
git config merge.dragon-generated.driver true
git config merge.dragon-floor.name 'structural merge of floors and pins (scripts/floor-merge.ts)'
git config merge.dragon-floor.driver "node '$GITHUB_WORKSPACE/scripts/floor-merge.ts' %O %A %B %P"
git config merge.dragon-sorted.name 'interleave additions to the sorted registries (scripts/sorted-merge.ts)'
git config merge.dragon-sorted.driver "node '$GITHUB_WORKSPACE/scripts/sorted-merge.ts' %O %A %B %P"
git config --global user.name 'dragon landing driver'
git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
# Parallel position worktrees take a few GB each; they stay on only when the disk keeps the driver's floor (40 GB).
- name: Choose parallel builds by free disk
run: |
free=$(df -BG --output=avail /tmp | tail -1 | tr -dc '0-9')
if [ "$free" -ge 40 ]; then parallel=1; else parallel=0; fi
echo "free disk ${free} GB: LAND_PARALLEL=$parallel"
echo "LAND_PARALLEL=$parallel" >> "$GITHUB_ENV"
- name: Build the queue file
id: queue
env:
LAND_QUEUE_INPUT: ${{ inputs.queue }}
run: |
mkdir -p "$LAND_LOG_DIR"
node scripts/land-actions.ts queue "$LAND_QUEUE_FILE"
# The trusted checkout read-only from here: a write the driver is misled into (a tree's symlink) cannot change its scripts.
# Only .git stays writable: the driver's worktrees, fetches, refs, config and merges live there.
- name: Make the trusted checkout read-only
run: |
chmod -R a-w "$GITHUB_WORKSPACE"
chmod -R u+w "$GITHUB_WORKSPACE/.git"
writable=$(find "$GITHUB_WORKSPACE" -path "$GITHUB_WORKSPACE/.git" -prune -o -perm -u=w -print | head -5)
[ -z "$writable" ] || { echo "::error::the trusted checkout is still writable: $writable"; exit 1; }
- name: Land one batch
env:
LAND_TOKEN: ${{ secrets.LAND_TOKEN }}
run: node scripts/land.ts "$LAND_QUEUE_FILE"
- name: Upload the driver's logs and status
if: always()
uses: actions/upload-artifact@v4
with:
name: land-logs
path: ${{ runner.temp }}/land-logs
if-no-files-found: warn
retention-days: 30
redispatch:
needs: land
if: ${{ !cancelled() && needs.land.outputs.count != '' && needs.land.outputs.count != '0' }}
runs-on: ubuntu-latest
timeout-minutes: 350
# GITHUB_TOKEN may dispatch a workflow (workflow_dispatch is the exception to its no-new-runs rule); no secret is needed here.
permissions:
contents: read
issues: read
actions: write
env:
GH_TOKEN: ${{ github.token }}
LAND_STOP_ISSUE: ${{ vars.LAND_STOP_ISSUE }}
steps:
- name: Check out this workflow's commit (trusted)
uses: actions/checkout@v4
with:
ref: ${{ github.sha }}
persist-credentials: false
- name: Set up Node 24.15.0
uses: actions/setup-node@v4
with:
node-version: '24.15.0'
- name: Download the handoff
uses: actions/download-artifact@v4
with:
name: land-logs
path: ${{ runner.temp }}/land-logs
- name: Re-dispatch the rest of the queue
run: node scripts/land-actions.ts redispatch "$RUNNER_TEMP/land-logs/handoff.json"
2 changes: 1 addition & 1 deletion .macroscope/ignore.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ out/**
**/*.gen.ts

# === Macroscope defaults: package manager and lock files ===
**/package.json
# package.json is reviewed: its scripts are commands (the landing driver's among them), not data.
**/Package.swift
**/Package.resolved
**/*.pbxproj
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"land": "node scripts/land.ts",
"land:review-lookup": "node --conditions=dragon-internal scripts/land-review-lookup.ts",
"land:post-review": "node --conditions=dragon-internal scripts/land-post-review.ts",
"land:dispatch": "node scripts/land-actions.ts dispatch",
"land:clear-outage": "node scripts/land-actions.ts clear-outage",
"evidence:stamp": "node --conditions=dragon-internal scripts/evidence-stamp.ts",
"layout:subset": "node packages/translate/src/cli/subset.ts",
"linebreak:gen": "node scripts/gen-linebreak-data.ts",
Expand Down
Loading
Loading