From 82d96177a0fdb845233026a63f1b28ace69d3ee5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 15:05:22 -0700 Subject: [PATCH 1/3] quest(tooling/check-scope): claim From fefa63cbeef1fa1984c3738e6d05d4847bd6061a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 15:30:57 -0700 Subject: [PATCH 2/3] ci: scope check.yml from the dispatch impact map check.yml's inline Scope steps kept a second path map. sh/dispatch.sh now answers scope-check/scope-test (just ci-scope) from the one impact map, and both jobs gate their build setup on it. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/check.yml | 39 +++++++++++++--------------- justfile | 4 +++ quest/m1/tooling/README.md | 1 - quest/m1/tooling/check-scope.md | 9 ------- sh/dispatch.sh | 45 ++++++++++++++++++++++++--------- 5 files changed, 54 insertions(+), 44 deletions(-) delete mode 100644 quest/m1/tooling/check-scope.md diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 52d79d7b98..5a8a125f9f 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -39,19 +39,17 @@ jobs: # Full history so `just ci check` can diff against origin/$GITHUB_BASE_REF. fetch-depth: 0 - # Quests, agent config, and root Markdown compile nothing, so a diff of only - # those skips the disk cleanup and Rust cache that exist for the build. + - uses: ./.github/actions/just + + # sh/dispatch.sh's impact map decides. A diff that reaches only lints, + # such as a quest-only one, skips the disk cleanup and Rust cache that + # exist for the build. - name: Scope id: scope - run: | - base=$(git merge-base "origin/$GITHUB_BASE_REF" HEAD) - files=$(git diff --name-only "$base") - if grep -qvE '^(quest/|\.claude/|[^/]+\.md$)' <<< "$files"; then - echo build=true >> "$GITHUB_OUTPUT" - fi + run: just ci-scope check >> "$GITHUB_OUTPUT" - name: Free disk space - if: steps.scope.outputs.build + if: steps.scope.outputs.build == 'true' uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # main with: tool-cache: false @@ -75,7 +73,7 @@ jobs: # scope such an entry to the PR's own branch anyway, where no later PR # could read it while it ate the repository's 10 GB budget. - name: Rust cache - if: steps.scope.outputs.build + if: steps.scope.outputs.build == 'true' uses: ./.github/actions/rust-cache # The same recipe a developer runs locally. It diffs against @@ -105,25 +103,22 @@ jobs: # Full history so `just ci test` can diff against origin/$GITHUB_BASE_REF. fetch-depth: 0 - # Same scope as the `check` job. No test covers those files either, so the - # job skips everything and reports green instead of spending a minute of + - uses: ./.github/actions/just + + # With no test in scope, the job skips everything and still reports + # green, so the required check passes instead of spending a minute of # setup to run nothing. - name: Scope id: scope - run: | - base=$(git merge-base "origin/$GITHUB_BASE_REF" HEAD) - files=$(git diff --name-only "$base") - if grep -qvE '^(quest/|\.claude/|[^/]+\.md$)' <<< "$files"; then - echo build=true >> "$GITHUB_OUTPUT" - fi + run: just ci-scope test >> "$GITHUB_OUTPUT" - name: Free disk space - if: steps.scope.outputs.build + if: steps.scope.outputs.build == 'true' uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # main with: tool-cache: false - - if: steps.scope.outputs.build + - if: steps.scope.outputs.build == 'true' uses: DeterminateSystems/nix-installer-action@1d87d45818068401a10cf16bdc5f00b24994a83f # main with: determinate: false @@ -139,13 +134,13 @@ jobs: # Restore only, same as the `check` job above. - name: Rust cache - if: steps.scope.outputs.build + if: steps.scope.outputs.build == 'true' uses: ./.github/actions/rust-cache # NEXTEST_PROFILE picks up the longer hang timeout in .config/nextest.toml; # without it a runner under load could trip the local one. - name: Test - if: steps.scope.outputs.build + if: steps.scope.outputs.build == 'true' run: nix develop --command just ci test env: MOQ_STRICT: 1 diff --git a/justfile b/justfile index c20feb7f2d..6cf89744ac 100644 --- a/justfile +++ b/justfile @@ -72,6 +72,10 @@ check $BASE="": ci $JOB $BASE="": sh/dispatch.sh "ci-$JOB" "$BASE" +# Print `build=true` when CI job JOB has more than lints in scope, so it sets up the build. +ci-scope $JOB $BASE="": + sh/dispatch.sh "scope-$JOB" "$BASE" + # Auto-fix lint and formatting for what the branch changed since BASE. fix $BASE="": sh/dispatch.sh fix "$BASE" diff --git a/quest/m1/tooling/README.md b/quest/m1/tooling/README.md index 3820b828aa..b8e485a625 100644 --- a/quest/m1/tooling/README.md +++ b/quest/m1/tooling/README.md @@ -25,4 +25,3 @@ inline; line count is not the test. - [Demo scripts](/quest/m1/tooling/demo-scripts.md) - the demo justfiles' inline bash moves into scripts, the last logic left inside recipes - [Forward arguments](/quest/m1/tooling/forward-args.md) - recipes pass a quoted nextest filterset through intact instead of re-splitting it -- [Check scope](/quest/m1/tooling/check-scope.md) - check.yml's build-or-skip decision comes from sh/dispatch.sh instead of an inline diff grep diff --git a/quest/m1/tooling/check-scope.md b/quest/m1/tooling/check-scope.md deleted file mode 100644 index 0b92cec0b9..0000000000 --- a/quest/m1/tooling/check-scope.md +++ /dev/null @@ -1,9 +0,0 @@ -# [S] check.yml scopes from the impact map - -## Goal - -`.github/workflows/check.yml` decides whether a PR needs the build and test jobs from `sh/dispatch.sh`, the line's single impact map. The two inline "Scope" steps that grep the diff for quest-only and doc-only changes go away, so there is one place that maps changed paths to work. - -## Plan - -Main's #4407 added the inline Scope steps (a quest, `.claude/`, or root-markdown-only diff skips the build) after this line had already moved CI scoping into `sh/dispatch.sh`. Teach the impact map that those paths need no build, have the Scope steps call it (or drop them if the dispatcher already makes the jobs no-ops), and keep the skipped-but-required checks reporting success so branch protection still passes on quest-only PRs. `just gh check` must keep enforcing that workflow steps run recipes. diff --git a/sh/dispatch.sh b/sh/dispatch.sh index 87a8c271bc..18a0324c2f 100755 --- a/sh/dispatch.sh +++ b/sh/dispatch.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Run `just check`, `just fix`, or `just ci check|test` over what the branch -# changed. +# changed, or tell CI whether a job needs its build setup (`just ci-scope`). # -# Usage: sh/dispatch.sh check|fix|ci-check|ci-test [BASE|--all] +# Usage: sh/dispatch.sh check|fix|ci-check|ci-test|scope-check|scope-test [BASE|--all] # # The branch diff is resolved once and matched against the impact map below, # the one place that says which paths put which module in scope. `check` is @@ -10,11 +10,11 @@ # so there is no second definition of "checked". set -euo pipefail -usage="usage: sh/dispatch.sh check|fix|ci-check|ci-test [BASE|--all]" +usage="usage: sh/dispatch.sh check|fix|ci-check|ci-test|scope-check|scope-test [BASE|--all]" action=${1:?$usage} base=${2:-} case "$action" in - check | fix | ci-check | ci-test) ;; + check | fix | ci-check | ci-test | scope-check | scope-test) ;; *) echo "$usage" >&2 exit 2 @@ -24,8 +24,9 @@ esac # A host toolchain fails deep in a build instead: its gcc keeps the # _FORTIFY_SOURCE the dev shell disables, which breaks jemalloc's -Werror # configure probes. This flake's dev shell sets MOQ_DEV_SHELL. Windows has no -# Nix, so it always runs on the host. -if [[ -z "${MOQ_DEV_SHELL:-}" && -z "${MOQ_ALLOW_HOST:-}" && "$OSTYPE" != msys && "$OSTYPE" != cygwin ]]; then +# Nix, so it always runs on the host. A scope query only reads git, and CI asks +# it before installing Nix. +if [[ "$action" != scope-* && -z "${MOQ_DEV_SHELL:-}" && -z "${MOQ_ALLOW_HOST:-}" && "$OSTYPE" != msys && "$OSTYPE" != cygwin ]]; then echo "error: run inside the Nix dev shell ('nix develop' or direnv) so tools match CI" >&2 echo " or set MOQ_ALLOW_HOST=1 to use the host toolchain anyway" >&2 exit 1 @@ -139,19 +140,39 @@ declare -A tools=( [gh]='actionlint bun' ) +# Modules that lint without compiling. A CI job whose scope holds nothing else +# skips its build setup: the disk cleanup, the Rust cache, and for `test`, Nix +# itself. A new module gets the build setup until it is listed here. +lint_only=(quest markdown shell toml nix justfile gh) + case "$action" in - check | ci-check) modules=(js workers drafts rs bench quest drill py kt swift go dart obs_compile obs flake markdown shell toml nix justfile gh) ;; + check | ci-check | scope-check) modules=(js workers drafts rs bench quest drill py kt swift go dart obs_compile obs flake markdown shell toml nix justfile gh) ;; fix) modules=(js rs py dart obs markdown shell toml nix justfile) ;; - ci-test) modules=(js rs py) ;; + ci-test | scope-test) modules=(js rs py) ;; esac -selected=() -missing=() +in_scope=() for module in "${modules[@]}"; do pattern=${scope[$module]} - if [[ -z "$all" && -n "$pattern" ]] && ! grep -qE "$pattern" "$changed"; then - continue + if [[ -n "$all" || -z "$pattern" ]] || grep -qE "$pattern" "$changed"; then + in_scope+=("$module") fi +done + +# key=value, which CI appends to $GITHUB_OUTPUT. +if [[ "$action" == scope-* ]]; then + build=false + for module in "${in_scope[@]}"; do + [[ " ${lint_only[*]} " == *" $module "* ]] || build=true + done + echo "$action: in scope: ${in_scope[*]:-nothing}" >&2 + echo "build=$build" + exit 0 +fi + +selected=() +missing=() +for module in "${in_scope[@]}"; do absent=() for tool in ${tools[$module]}; do command -v "$tool" >/dev/null 2>&1 || absent+=("$tool") From 657e0dc4e1c35846f5a45d1c2851754c808a2c8d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 16:25:38 -0700 Subject: [PATCH 3/3] ci: drop check.yml's Scope steps; always set up The impact map in sh/dispatch.sh already makes `just ci check|test` run only what a diff reaches, so a second path map in the workflow is not worth the minute of setup it saves on quest-only PRs. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/check.yml | 31 +++++-------------------- justfile | 4 ---- sh/dispatch.sh | 45 ++++++++++--------------------------- 3 files changed, 17 insertions(+), 63 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5a8a125f9f..834ac0fac6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -39,17 +39,7 @@ jobs: # Full history so `just ci check` can diff against origin/$GITHUB_BASE_REF. fetch-depth: 0 - - uses: ./.github/actions/just - - # sh/dispatch.sh's impact map decides. A diff that reaches only lints, - # such as a quest-only one, skips the disk cleanup and Rust cache that - # exist for the build. - - name: Scope - id: scope - run: just ci-scope check >> "$GITHUB_OUTPUT" - - name: Free disk space - if: steps.scope.outputs.build == 'true' uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # main with: tool-cache: false @@ -73,12 +63,14 @@ jobs: # scope such an entry to the PR's own branch anyway, where no later PR # could read it while it ate the repository's 10 GB budget. - name: Rust cache - if: steps.scope.outputs.build == 'true' uses: ./.github/actions/rust-cache # The same recipe a developer runs locally. It diffs against # origin/$GITHUB_BASE_REF and touches only the packages this PR changed - # plus their dependents, so a one-crate PR compiles one crate. + # plus their dependents, so a one-crate PR compiles one crate and a + # quest-only PR runs only the lints. sh/dispatch.sh's impact map is the one + # place that decides; the setup above always runs rather than keep a second + # path map here. # # MOQ_STRICT turns every "tool missing, skipping" guard into an error. # Locally an absent formatter means less gets checked; here it would mean @@ -103,23 +95,12 @@ jobs: # Full history so `just ci test` can diff against origin/$GITHUB_BASE_REF. fetch-depth: 0 - - uses: ./.github/actions/just - - # With no test in scope, the job skips everything and still reports - # green, so the required check passes instead of spending a minute of - # setup to run nothing. - - name: Scope - id: scope - run: just ci-scope test >> "$GITHUB_OUTPUT" - - name: Free disk space - if: steps.scope.outputs.build == 'true' uses: jlumbroso/free-disk-space@ceedf095f4ec1a097402bc6bd80831f2e1a6fde6 # main with: tool-cache: false - - if: steps.scope.outputs.build == 'true' - uses: DeterminateSystems/nix-installer-action@1d87d45818068401a10cf16bdc5f00b24994a83f # main + - uses: DeterminateSystems/nix-installer-action@1d87d45818068401a10cf16bdc5f00b24994a83f # main with: determinate: false # Trust the flake's cachix substituter. `nixConfig` in flake.nix is @@ -134,13 +115,11 @@ jobs: # Restore only, same as the `check` job above. - name: Rust cache - if: steps.scope.outputs.build == 'true' uses: ./.github/actions/rust-cache # NEXTEST_PROFILE picks up the longer hang timeout in .config/nextest.toml; # without it a runner under load could trip the local one. - name: Test - if: steps.scope.outputs.build == 'true' run: nix develop --command just ci test env: MOQ_STRICT: 1 diff --git a/justfile b/justfile index 6cf89744ac..c20feb7f2d 100644 --- a/justfile +++ b/justfile @@ -72,10 +72,6 @@ check $BASE="": ci $JOB $BASE="": sh/dispatch.sh "ci-$JOB" "$BASE" -# Print `build=true` when CI job JOB has more than lints in scope, so it sets up the build. -ci-scope $JOB $BASE="": - sh/dispatch.sh "scope-$JOB" "$BASE" - # Auto-fix lint and formatting for what the branch changed since BASE. fix $BASE="": sh/dispatch.sh fix "$BASE" diff --git a/sh/dispatch.sh b/sh/dispatch.sh index 18a0324c2f..87a8c271bc 100755 --- a/sh/dispatch.sh +++ b/sh/dispatch.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # Run `just check`, `just fix`, or `just ci check|test` over what the branch -# changed, or tell CI whether a job needs its build setup (`just ci-scope`). +# changed. # -# Usage: sh/dispatch.sh check|fix|ci-check|ci-test|scope-check|scope-test [BASE|--all] +# Usage: sh/dispatch.sh check|fix|ci-check|ci-test [BASE|--all] # # The branch diff is resolved once and matched against the impact map below, # the one place that says which paths put which module in scope. `check` is @@ -10,11 +10,11 @@ # so there is no second definition of "checked". set -euo pipefail -usage="usage: sh/dispatch.sh check|fix|ci-check|ci-test|scope-check|scope-test [BASE|--all]" +usage="usage: sh/dispatch.sh check|fix|ci-check|ci-test [BASE|--all]" action=${1:?$usage} base=${2:-} case "$action" in - check | fix | ci-check | ci-test | scope-check | scope-test) ;; + check | fix | ci-check | ci-test) ;; *) echo "$usage" >&2 exit 2 @@ -24,9 +24,8 @@ esac # A host toolchain fails deep in a build instead: its gcc keeps the # _FORTIFY_SOURCE the dev shell disables, which breaks jemalloc's -Werror # configure probes. This flake's dev shell sets MOQ_DEV_SHELL. Windows has no -# Nix, so it always runs on the host. A scope query only reads git, and CI asks -# it before installing Nix. -if [[ "$action" != scope-* && -z "${MOQ_DEV_SHELL:-}" && -z "${MOQ_ALLOW_HOST:-}" && "$OSTYPE" != msys && "$OSTYPE" != cygwin ]]; then +# Nix, so it always runs on the host. +if [[ -z "${MOQ_DEV_SHELL:-}" && -z "${MOQ_ALLOW_HOST:-}" && "$OSTYPE" != msys && "$OSTYPE" != cygwin ]]; then echo "error: run inside the Nix dev shell ('nix develop' or direnv) so tools match CI" >&2 echo " or set MOQ_ALLOW_HOST=1 to use the host toolchain anyway" >&2 exit 1 @@ -140,39 +139,19 @@ declare -A tools=( [gh]='actionlint bun' ) -# Modules that lint without compiling. A CI job whose scope holds nothing else -# skips its build setup: the disk cleanup, the Rust cache, and for `test`, Nix -# itself. A new module gets the build setup until it is listed here. -lint_only=(quest markdown shell toml nix justfile gh) - case "$action" in - check | ci-check | scope-check) modules=(js workers drafts rs bench quest drill py kt swift go dart obs_compile obs flake markdown shell toml nix justfile gh) ;; + check | ci-check) modules=(js workers drafts rs bench quest drill py kt swift go dart obs_compile obs flake markdown shell toml nix justfile gh) ;; fix) modules=(js rs py dart obs markdown shell toml nix justfile) ;; - ci-test | scope-test) modules=(js rs py) ;; + ci-test) modules=(js rs py) ;; esac -in_scope=() +selected=() +missing=() for module in "${modules[@]}"; do pattern=${scope[$module]} - if [[ -n "$all" || -z "$pattern" ]] || grep -qE "$pattern" "$changed"; then - in_scope+=("$module") + if [[ -z "$all" && -n "$pattern" ]] && ! grep -qE "$pattern" "$changed"; then + continue fi -done - -# key=value, which CI appends to $GITHUB_OUTPUT. -if [[ "$action" == scope-* ]]; then - build=false - for module in "${in_scope[@]}"; do - [[ " ${lint_only[*]} " == *" $module "* ]] || build=true - done - echo "$action: in scope: ${in_scope[*]:-nothing}" >&2 - echo "build=$build" - exit 0 -fi - -selected=() -missing=() -for module in "${in_scope[@]}"; do absent=() for tool in ${tools[$module]}; do command -v "$tool" >/dev/null 2>&1 || absent+=("$tool")