Skip to content
39 changes: 39 additions & 0 deletions .github/required-checks.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# TIER 1 (#1264) — the checks that block a merge to main. THE source of truth:
# the "Protection" ruleset (id 17713865) is synced FROM this file after merge,
# never edited by hand. Required checks are evaluated in the MERGE QUEUE
# (merge_group), which runs the full main lane before anything lands.
# tools/ci_tier_check.sh gates this file against the workflows (each name is
# produced by exactly one job that reports on pull_request AND merge_group;
# no required path runs work only on push); `--live` diffs it against the
# ruleset.
#
# Format: one exact check-run name per line. A line starting with `#` is a
# comment (give the reason for a non-obvious entry). No trailing blanks.
#
# ci.yml
scope
# Every `container:` job runs inside this image; a failed prerequisite SKIPS the
# required jobs that need it, and GitHub counts a skipped required check as
# passing — so it is required like the jobs that need it (#1264).
build dev/ci image
werror audit ([99i], cached)
gate self-tests (section plan + audit cache key)
linux / gcc
# The clang leg: -Werror at compile time + clang codegen (#1264: tier 1 is
# "Linux gcc/clang"). Core smoke on a PR, the full suite on main.
linux / clang
macos / macos-latest
extensions (http+model+gfx suite; embed/lsp/jit-smoke)
asan + ubsan (full suite)
db extension (postgres service)
jit differential (interpreter oracle, tape-replayed)
replay differential (same-binary tape fidelity)
freestanding profile (symbol gate + smoke)
tsan (concurrency race gate)
install.sh (interpreter + eigenlsp on PATH)
bench (instruction-count regression gate)
valgrind (memcheck smoke, JIT off)
# codeql.yml (workflow "CodeQL")
Analyze C
# pages.yml (workflow "Docs site") — the aggregator over the real emcc build
playground (real emcc wasm32 build)
104 changes: 75 additions & 29 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ on:
branches: [main]
pull_request:
branches: [main]
# The merge queue (#1264, Rust's model): the FULL main lane runs on the
# queue's candidate (current main + the PRs queued ahead + this one) before
# anything lands, so main is green by construction. Every step below that
# is main-lane-only is gated `github.event_name != 'pull_request'`, which is
# true on push AND on merge_group — never `== 'push'`.
merge_group:
types: [checks_requested]

permissions:
contents: read
Expand Down Expand Up @@ -43,15 +50,28 @@ concurrency:
# probe gates (never a hand-written list). [99i] runs ONCE, in the
# `werror audit` job, cached on the audit's inputs. macos-15-intel is
# not on this lane.
# main lane (push to main) — the full matrix except macos-15-intel, with
# [99i] owned by the one `werror audit` job instead of being repeated ten
# times. The merge-er waits for this; contributors do not.
# main lane (merge_group AND push to main) — the full matrix except
# macos-15-intel. It runs in the MERGE QUEUE (#1264, Rust's model) on the
# candidate commit (main + the PRs queued ahead + this one) and nothing
# lands unless it is green, so main is green by construction and nobody
# rebases a PR to "update" it. Main-lane-only steps are gated
# `github.event_name != 'pull_request'` (true on push and merge_group),
# never `== 'push'`. The post-merge push run re-tests the commit the queue
# already tested: it is kept because the README badge reads it and it
# publishes the rolling `ci-main` dev image that fork PRs run in.
# TIERS (#1264): every job in this file is TIER 1 — listed in
# .github/required-checks.txt, or a worker of an aggregator listed there.
# A job that is neither must go to nightly.yml; tools/ci_tier_check.sh
# (the `gate self-tests` job) fails otherwise, and also fails on a step
# whose condition would run it on push but not in the queue. Adding a job
# here means adding its check name to that file (the ruleset is synced
# from it).
# nightly (.github/workflows/nightly.yml) — macos-15-intel (ONLY here, #1264)
# and the full valgrind corpus, with a tracking issue on failure.
#
# The risk this accepts, stated: a variant-specific regression in a
# NON-variant section reaches main before it is caught. Main still runs the
# full matrix before anything is released.
# NON-variant section passes the PR lane and is caught in the queue, before
# it lands; the contributor waits only for the fast lane.

jobs:
# Is this PR docs-only? A change touching nothing but *.md cannot alter C or
Expand Down Expand Up @@ -143,13 +163,15 @@ jobs:
env:
OWNER: ${{ github.repository_owner }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }}
EVENT: ${{ github.event_name }}
# The event is read from the runner's GITHUB_EVENT_NAME, not an
# expression: tools/ci_tier_check.sh reds event-derived expression
# values on a required path (#1264). Only push advances ci-main.
run: |
BASE="ghcr.io/${OWNER,,}/eigenscript-dev"
if [ "$IS_FORK" = "true" ]; then
echo "image=$BASE:ci-main" >> "$GITHUB_OUTPUT"
echo "tags=" >> "$GITHUB_OUTPUT"
elif [ "$EVENT" = "push" ]; then
elif [ "$GITHUB_EVENT_NAME" = "push" ]; then
echo "image=$BASE:ci-${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "tags=$BASE:ci-${{ github.sha }},$BASE:ci-main" >> "$GITHUB_OUTPUT"
else
Expand Down Expand Up @@ -348,6 +370,17 @@ jobs:
apt-get update && apt-get install -y --no-install-recommends python3-yaml
fi
python3 -c 'import yaml; print("yaml", yaml.__version__)'
# Platform tiers (#1264): every name in .github/required-checks.txt is
# produced by one job that reports on pull_request AND merge_group, no
# required path runs work only on push, and every job in THIS file is
# required or a worker of a required aggregator. ~1 s + ~10 s of planted
# faults. Needs the PyYAML installed above; a missing loader is exit 2
# (instrument error), never a pass.
- if: needs.scope.outputs.code == 'true'
name: Platform tiers — required-checks.txt vs the workflows (#1264)
run: |
bash tools/ci_tier_check.sh
bash tools/ci_tier_check.sh --selftest
- if: needs.scope.outputs.code == 'true'
name: Consumer-acceptance harness self-test (~7 min)
run: bash tools/consumer_acceptance.sh --self-test
Expand Down Expand Up @@ -444,7 +477,7 @@ jobs:
# only thing it no longer runs is [99i], which the `werror audit` job
# owns for this run and which prints a SKIP naming that job.
- name: Run test suite (full)
if: matrix.cc == 'gcc' || github.event_name == 'push'
if: matrix.cc == 'gcc' || github.event_name != 'pull_request'
env:
EIGS_SKIP_WERROR_AUDIT: 1
# [99zd]'s live arms. Without this the roadmap gate's milestone and
Expand All @@ -457,10 +490,10 @@ jobs:
# clang on a PR: the value of this leg is the BUILD (-Werror fires at
# compile time, and clang's codegen differs), not a tenth execution of
# the same ~263 sections the gcc leg just ran on the same commit. It
# runs the derived core-smoke plan instead. On a push to main it runs
# runs the derived core-smoke plan instead. On the main lane (queue, push) it runs
# the full suite like every other leg.
- name: Run test suite (derived core-smoke plan)
if: matrix.cc == 'clang' && github.event_name != 'push'
if: matrix.cc == 'clang' && github.event_name == 'pull_request'
env:
EIGS_SUITE_SECTIONS: core
EIGS_SKIP_WERROR_AUDIT: 1
Expand Down Expand Up @@ -687,15 +720,15 @@ jobs:
# self-test (11 min) — the single largest block of this leg's 15 min.
# Its verdict cannot differ by platform anyway: the only conditional in
# the Makefile is LDFLAGS, a LINK flag, and the audit reads COMPILE
# invocations. On a push to main it still runs here in full, so the
# invocations. On the main lane (queue, push) it still runs here in full, so the
# gate's BSD-userland portability keeps an exercise at merge time.
- if: needs.scope.outputs.code == 'true' && github.event_name != 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name == 'pull_request'
name: Run test suite (PR lane; [99i] owned by the werror audit job)
env:
EIGS_SKIP_WERROR_AUDIT: 1
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true' && github.event_name == 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name != 'pull_request'
name: Run test suite (main lane, [99i] included)
run: cd tests && bash run_all_tests.sh

Expand Down Expand Up @@ -817,14 +850,14 @@ jobs:
# decide whether to skip — plus a fixed core smoke, and the job fails if
# the http binary unlocks fewer probe-gated chunks than its floor (a
# broken registration otherwise collapses the plan silently).
- if: needs.scope.outputs.code == 'true' && github.event_name != 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name == 'pull_request'
name: Run derived http+model section plan
env:
EIGS_SUITE_SECTIONS: http
EIGS_SKIP_WERROR_AUDIT: 1
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true' && github.event_name == 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name != 'pull_request'
name: Run full suite against http+model build
env:
EIGS_SKIP_WERROR_AUDIT: 1
Expand Down Expand Up @@ -865,14 +898,14 @@ jobs:
name: Build gfx variant
run: make gfx

- if: needs.scope.outputs.code == 'true' && github.event_name != 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name == 'pull_request'
name: Run derived gfx section plan (audio [62], [120b], [132], [133], [134])
env:
EIGS_SUITE_SECTIONS: gfx
EIGS_SKIP_WERROR_AUDIT: 1
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true' && github.event_name == 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name != 'pull_request'
name: Run full suite against gfx build (audio [62], containment [132], gfx examples [97])
env:
EIGS_SKIP_WERROR_AUDIT: 1
Expand All @@ -899,14 +932,14 @@ jobs:
name: Build zlib variant
run: make zlib

- if: needs.scope.outputs.code == 'true' && github.event_name != 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name == 'pull_request'
name: Run derived zlib section plan (executes DEFLATE section [124b])
env:
EIGS_SUITE_SECTIONS: zlib
EIGS_SKIP_WERROR_AUDIT: 1
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true' && github.event_name == 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name != 'pull_request'
name: Run full suite against zlib build (executes DEFLATE section [124])
env:
EIGS_SKIP_WERROR_AUDIT: 1
Expand All @@ -933,14 +966,14 @@ jobs:
name: Build net variant
run: make net

- if: needs.scope.outputs.code == 'true' && github.event_name != 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name == 'pull_request'
name: Run derived net section plan (executes network section [125])
env:
EIGS_SUITE_SECTIONS: net
EIGS_SKIP_WERROR_AUDIT: 1
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true' && github.event_name == 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name != 'pull_request'
name: Run full suite against net build (executes network section [125])
env:
EIGS_SKIP_WERROR_AUDIT: 1
Expand Down Expand Up @@ -986,15 +1019,15 @@ jobs:
name: Build full variant (http+model+db)
run: make full

- if: needs.scope.outputs.code == 'true' && github.event_name != 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name == 'pull_request'
name: Run derived full-variant section plan with live DATABASE_URL
env:
DATABASE_URL: postgres://eigs:eigs_test@db:5432/eigs_test
EIGS_SUITE_SECTIONS: full
EIGS_SKIP_WERROR_AUDIT: 1
run: cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true' && github.event_name == 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name != 'pull_request'
name: Run full suite with live DATABASE_URL
env:
DATABASE_URL: postgres://eigs:eigs_test@db:5432/eigs_test
Expand Down Expand Up @@ -1286,7 +1319,7 @@ jobs:
# sanitizers. This job was 26 min on #1158 — the second-longest on the
# board — and the core sections it shared with `asan + ubsan / core and
# LSP` are already sanitized there on the same commit.
- if: needs.scope.outputs.code == 'true' && github.event_name != 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name == 'pull_request'
name: Run derived asan-http section plan under sanitizers
env:
ASAN_OPTIONS: detect_leaks=1
Expand All @@ -1295,7 +1328,7 @@ jobs:
EIGS_SKIP_WERROR_AUDIT: 1
run: make asan-http && cd tests && bash run_all_tests.sh

- if: needs.scope.outputs.code == 'true' && github.event_name == 'push'
- if: needs.scope.outputs.code == 'true' && github.event_name != 'pull_request'
name: Run suite under sanitizers with the HTTP+model extensions
env:
ASAN_OPTIONS: detect_leaks=1
Expand Down Expand Up @@ -1544,16 +1577,29 @@ jobs:
run: ./build.sh

- if: needs.scope.outputs.code == 'true'
name: Build origin/main in a worktree (same environment)
name: Build the baseline in a worktree (same environment)
# In the merge queue the candidate is main + the PRs queued AHEAD + this
# one, so the baseline is the candidate's base (merge_group.base_sha),
# not origin/main — otherwise a PR is charged for the Ir cost of the PRs
# ahead of it and falsely rejected (#1273 code review). Read from the
# event payload inside the script, never via ${{ }} (tier gate rule).
run: |
git fetch --no-tags --depth=1 origin main
git worktree add /tmp/main-ref origin/main
base=origin/main
if [ "$GITHUB_EVENT_NAME" = merge_group ]; then
base=$(jq -r '.merge_group.base_sha' "$GITHUB_EVENT_PATH")
[ -n "$base" ] && [ "$base" != null ] || { echo "merge_group event without base_sha"; exit 1; }
git fetch --no-tags --depth=1 origin "$base"
else
git fetch --no-tags --depth=1 origin main
fi
echo "baseline: $base"
git worktree add /tmp/main-ref "$base"
( cd /tmp/main-ref && ./build.sh )

- if: needs.scope.outputs.code == 'true'
name: Gate self-test (a 2x pessimization must fail the gate)
run: EIGENSCRIPT="$PWD/src/eigenscript" bash bench/check_regression.sh --selftest

- if: needs.scope.outputs.code == 'true'
name: Regression gate — Ir of this commit vs origin/main
name: Regression gate — Ir of this commit vs its baseline
run: EIGENSCRIPT="$PWD/src/eigenscript" bash bench/check_regression.sh --vs /tmp/main-ref/src/eigenscript
2 changes: 2 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
branches: [main]
pull_request:
branches: [main]
merge_group: # the merge queue (#1264): a required check must report there
types: [checks_requested]
schedule:
- cron: '23 7 * * 1'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ jobs:

Run: $RUN_URL

These lanes moved off the PR path in #1160. \`macos-15-intel\` also runs on every push to \`main\`; the FULL valgrind corpus runs HERE ONLY (the PR lane and \`main\` both run the smoke spread), so a red \`valgrind-full\` is a finding nothing else will report. This thread exists so a nightly failure between main pushes is not silent."
These lanes moved off the PR path in #1160 and are tier 2 (#1264): they never colour \`main\`. \`macos-15-intel\` runs HERE ONLY; the FULL valgrind corpus runs HERE ONLY (the PR lane and \`main\` both run the smoke spread), so a red \`valgrind-full\` is a finding nothing else will report. This thread exists so a nightly failure between main pushes is not silent."

if [ -n "$num" ]; then
# Reopen first: a closed thread that starts failing again is the
Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,30 @@
# check NAME independently of the worker's. THIS is the check
# to require on main.
#
# Deploy is unchanged: Configure Pages, the artifact upload and the `deploy`
# job run only on push/dispatch, never on a pull request, and deploy needs
# the aggregator.
# Deploy: Configure Pages and the artifact upload run on every non-PR event
# (push, dispatch, and the merge queue, so the queue exercises exactly what a
# push will); the `deploy` job runs only on push/dispatch — never from a PR or
# a queue candidate — and needs the aggregator.
name: Docs site

on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group: # the merge queue (#1264): a required check must report there
types: [checks_requested]
workflow_dispatch:

permissions:
contents: read

# Deploys from main still serialize on ONE group, so a newer main push
# supersedes an older deploy. A pull request gets a group of its OWN ref:
# sharing `pages` would let every PR push cancel an in-flight main deploy.
# supersedes an older deploy. A pull request or a merge-queue candidate gets a
# group of its OWN ref: sharing `pages` would let it cancel an in-flight main
# deploy.
concurrency:
group: ${{ github.event_name == 'pull_request' && format('pages-pr-{0}', github.ref) || 'pages' }}
group: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && format('pages-pr-{0}', github.ref) || 'pages' }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -132,7 +136,9 @@ jobs:
echo "OK: the real emcc build of web/build.sh passed"

deploy:
if: github.event_name != 'pull_request'
# Publish only what LANDED: never from a pull request, and never from a
# merge-queue candidate that may still be rejected (#1264).
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
needs: playground
runs-on: ubuntu-latest
permissions:
Expand Down
Loading
Loading