From ecce09b355db971ec19fbbb7e09c76cb8b33c7d2 Mon Sep 17 00:00:00 2001 From: chichuan Date: Sat, 15 Aug 2026 22:10:28 +0800 Subject: [PATCH 1/3] fix(ci): shard the focused test job instead of one long-lived run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The focused path tested every impacted package in a single job with a plain `go test -race`, so internal/app ran inside one long-lived process alongside all of its reverse dependencies. That is exactly the shape scripts/ci/run-app-race-tests.sh exists to avoid: a single app test process retains every constructed command tree in framework registries, so the run grows to 900s and the job stays alive long enough to be reclaimed by the runner. Recent focused runs failed with SIGTERM after 9-10 minutes without a single test failure, and one earlier run failed at `internal/app 902.651s`, 2.65s past the package timeout. Fan the same package plan across the shard matrix test-race already uses, and run each shard the way test-race runs it: internal/app through the process-isolating helper, cli/smoke with their wider package budget, release-scripts without race and with archive tooling. changed-test-packages.sh gains `list-shard`, which intersects the impacted set with scripts/ci/test-packages.sh shard membership so shard definitions stay single-sourced — and so an unknown shard name aborts there rather than reporting an empty selection, which would let a mistyped shard skip every test while reporting success. release-scripts is in the matrix on purpose: its dedicated job only runs at full-suite or release-sensitive scope, so omitting it here would stop testing test/scripts changes altogether. A test pins that the shard selections partition the impacted set exactly, so shard-plan drift cannot silently shrink focused coverage. --- .github/workflows/ci.yml | 81 ++++++- scripts/ci/changed-test-packages.sh | 55 ++++- test/scripts/changed_test_packages_test.go | 239 +++++++++++++++++++++ 3 files changed, 357 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02e72637..9d3fa1e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -513,11 +513,28 @@ jobs: run: node .github/reviewer-routing.test.js test-focused: - name: Test (changed packages) + name: "Test (focused: ${{ matrix.shard }})" needs: lint if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite != 'true' }} runs-on: ubuntu-latest + # Each shard owns one bounded slice of the impacted set, so no single job + # carries internal/app together with every reverse dependency. The shard + # list and per-shard execution below mirror test-race, which runs the same + # shards at full-suite scope; release-scripts is included because its + # dedicated job only runs at full-suite or release-sensitive scope, and + # dropping it here would stop testing test/scripts changes entirely. timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + shard: + - app + - generators + - helpers + - cli + - smoke + - remaining + - release-scripts steps: - name: Check out repository uses: actions/checkout@v4 @@ -546,23 +563,69 @@ jobs: with: go-version-file: go.mod - - name: Test changed packages and reverse dependencies + - name: Select impacted packages for shard + id: select shell: bash env: - DWS_PACKAGE_VERSION: 0.0.0-test + TEST_SHARD: ${{ matrix.shard }} run: | set -euo pipefail package_output="$( ./scripts/ci/changed-test-packages.sh \ - list "$TEST_BASE_REF" "$TEST_HEAD_REF" + list-shard "$TEST_SHARD" "$TEST_BASE_REF" "$TEST_HEAD_REF" )" if [ -z "$package_output" ]; then - echo "No buildable Go package is affected by this revision." \ + echo "No buildable Go package in shard $TEST_SHARD is affected by this revision." \ >> "$GITHUB_STEP_SUMMARY" exit 0 fi - mapfile -t packages <<< "$package_output" - go test -v -race -count=1 -timeout=15m "${packages[@]}" + { + echo "packages<> "$GITHUB_OUTPUT" + + - name: Build + if: ${{ matrix.shard == 'remaining' && steps.select.outputs.packages != '' }} + run: make build + + - name: Install archive tooling + if: ${{ matrix.shard == 'release-scripts' && steps.select.outputs.packages != '' }} + run: sudo apt-get update && sudo apt-get install -y zip unzip + + - name: Test shard with Race Detection + if: ${{ steps.select.outputs.packages != '' }} + shell: bash + env: + DWS_PACKAGE_VERSION: 0.0.0-test + TEST_SHARD: ${{ matrix.shard }} + SHARD_PACKAGES: ${{ steps.select.outputs.packages }} + run: | + set -euo pipefail + mapfile -t packages <<< "$SHARD_PACKAGES" + test "${#packages[@]}" -gt 0 + if [ "$TEST_SHARD" = app ]; then + # A single long-lived app test process retains every constructed + # command tree in framework registries. Isolate Schema assembly and + # bounded name ranges so each process releases that state on exit. + test "${#packages[@]}" -eq 1 + ./scripts/ci/run-app-race-tests.sh run "${packages[0]}" + exit 0 + fi + if [ "$TEST_SHARD" = release-scripts ]; then + # Mirror the dedicated release-contract job: these suites shell out + # to archive tooling and are not race-instrumented there. + go test -v -count=1 -timeout=10m "${packages[@]}" + exit 0 + fi + # cli/smoke own heavy NewRootCommand / Schema assembly under -race; + # give them a dedicated package timeout on slower hosted runners. + timeout_budget=12m + if [ "$TEST_SHARD" = cli ] || + [ "$TEST_SHARD" = smoke ]; then + timeout_budget=15m + fi + go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}" test-race: name: "Test (race: ${{ matrix.shard }})" @@ -715,7 +778,7 @@ jobs: failed=0 if [ "$CHANGELOG_ONLY" = true ] || [ "$DOCS_ONLY" = true ]; then for shard in \ - "changed packages:$FOCUSED_RESULT" \ + "focused shards:$FOCUSED_RESULT" \ "race shards:$RACE_RESULT" \ "release scripts:$RELEASE_SCRIPTS_RESULT" \ "cross-platform compile:$CROSS_PLATFORM_RESULT" \ @@ -744,7 +807,7 @@ jobs: release_expected=success fi for shard in \ - "changed packages:$FOCUSED_RESULT:$focused_expected" \ + "focused shards:$FOCUSED_RESULT:$focused_expected" \ "race shards:$RACE_RESULT:$race_expected" \ "release scripts:$RELEASE_SCRIPTS_RESULT:$release_expected" \ "cross-platform compile:$CROSS_PLATFORM_RESULT:success" diff --git a/scripts/ci/changed-test-packages.sh b/scripts/ci/changed-test-packages.sh index 3558b689..37d31e1c 100755 --- a/scripts/ci/changed-test-packages.sh +++ b/scripts/ci/changed-test-packages.sh @@ -7,16 +7,32 @@ set -eu ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)" usage() { - printf '%s\n' "usage: $0 BASE_REF HEAD_REF" >&2 + printf '%s\n' \ + "usage: $0 BASE_REF HEAD_REF" \ + " $0 list-shard SHARD BASE_REF HEAD_REF" >&2 exit 2 } -[ "${1:-}" = changed ] || [ "${1:-}" = list ] || usage -[ "$#" -eq 3 ] || usage - -MODE="$1" -BASE_REF="$2" -HEAD_REF="$3" +# list-shard narrows the impacted set to one test shard so focused CI runs can +# fan the same package plan across the shard matrix instead of testing every +# impacted package in a single long-lived job. +SHARD="" +case "${1:-}" in + changed|list) + [ "$#" -eq 3 ] || usage + MODE="$1" + BASE_REF="$2" + HEAD_REF="$3" + ;; + list-shard) + [ "$#" -eq 4 ] || usage + MODE="$1" + SHARD="$2" + BASE_REF="$3" + HEAD_REF="$4" + ;; + *) usage ;; +esac cd "$ROOT" git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || { @@ -116,9 +132,29 @@ while IFS= read -r file; do [ "$embedded_owner_found" = true ] || continue done < "$files" +# emit_selection prints the planned packages, restricted to SHARD when the +# caller asked for one. Restriction reuses scripts/ci/test-packages.sh as the +# single source of shard membership, which also means an unknown shard name +# aborts there instead of being reported as an empty selection: a silently empty +# shard would let a mistyped CI shard name skip every test and still succeed. +emit_selection() { + selection="$1" + if [ -z "$SHARD" ]; then + cat "$selection" + return 0 + fi + shard_packages="$workdir/shard-packages" + "$ROOT/scripts/ci/test-packages.sh" list "$SHARD" > "$shard_packages" + LC_ALL=C sort -u "$shard_packages" -o "$shard_packages" + LC_ALL=C sort -u "$selection" -o "$selection" + # An empty intersection is a normal outcome for a shard no change touched, so + # comm's silence must not be treated as an error. + LC_ALL=C comm -12 "$selection" "$shard_packages" +} + LC_ALL=C sort -u "$changed_packages" -o "$changed_packages" if [ "$MODE" = changed ] || [ ! -s "$changed_packages" ]; then - cat "$changed_packages" + emit_selection "$changed_packages" exit 0 fi @@ -133,4 +169,5 @@ while IFS= read -r package; do done < "$all_packages" > "$impacted_packages" cat "$changed_packages" >> "$impacted_packages" -LC_ALL=C sort -u "$impacted_packages" +LC_ALL=C sort -u "$impacted_packages" -o "$impacted_packages" +emit_selection "$impacted_packages" diff --git a/test/scripts/changed_test_packages_test.go b/test/scripts/changed_test_packages_test.go index 3f228c0a..f696e569 100644 --- a/test/scripts/changed_test_packages_test.go +++ b/test/scripts/changed_test_packages_test.go @@ -129,6 +129,245 @@ func Value() string { } } +// shardChangedPackagesShards mirrors the shard names scripts/ci/test-packages.sh +// asserts complete coverage for in its verify mode. Keeping the list here in +// full is deliberate: the union assertion below fails if a shard is added +// upstream without being selected, and an unknown-shard error fails if one is +// removed, so shard-plan drift cannot silently shrink focused-test coverage. +var shardChangedPackagesShards = []string{ + "app", + "generators", + "helpers", + "cli", + "smoke", + "remaining", + "release-scripts", +} + +func TestChangedTestPackagesShardSelectionCoversEveryImpactedPackageExactlyOnce(t *testing.T) { + repository := newShardChangedPackagesRepository(t) + baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture") + + writeChangedTestPackagesFixture(t, repository, "core/core.go", `package core + +func Value() string { + return "changed" +} +`) + headRef := commitChangedTestPackagesFixture(t, repository, "change core") + + impacted := runChangedTestPackages(t, repository, "list", baseRef, headRef) + if len(impacted) == 0 { + t.Fatal("shard fixture produced no impacted packages") + } + + owner := map[string]string{} + union := make([]string, 0, len(impacted)) + for _, shard := range shardChangedPackagesShards { + for _, pkg := range runChangedTestPackagesShard(t, repository, shard, baseRef, headRef) { + if previous, ok := owner[pkg]; ok { + t.Fatalf("package %q selected by both shard %q and %q", pkg, previous, shard) + } + owner[pkg] = shard + union = append(union, pkg) + } + } + + slices.Sort(union) + if !slices.Equal(union, impacted) { + t.Fatalf("shard selection union = %q, want every impacted package %q", union, impacted) + } +} + +func TestChangedTestPackagesShardSelectionFailsClosedOnUnknownShard(t *testing.T) { + repository := newShardChangedPackagesRepository(t) + baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture") + + writeChangedTestPackagesFixture(t, repository, "core/core.go", `package core + +func Value() string { + return "changed" +} +`) + headRef := commitChangedTestPackagesFixture(t, repository, "change core") + + script := filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh") + command := exec.Command(script, "list-shard", "nonexistent", baseRef, headRef) + command.Dir = repository + output, err := command.CombinedOutput() + if err == nil { + t.Fatalf("unknown shard unexpectedly succeeded:\n%s", output) + } + // An unknown shard must abort rather than report an empty selection: a + // silently empty shard would let a mistyped CI shard name skip every test + // while still reporting success. + if !strings.Contains(string(output), "unknown test package shard") { + t.Fatalf("unknown shard failure = %q, want fail-closed diagnostic", output) + } + if strings.TrimSpace(string(output)) == "" { + t.Fatal("unknown shard produced empty output instead of a diagnostic") + } +} + +func TestChangedTestPackagesShardSelectionIsEmptyForUnaffectedShard(t *testing.T) { + repository := newShardChangedPackagesRepository(t) + baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture") + + writeChangedTestPackagesFixture(t, repository, "internal/helpers/helpers.go", `package helpers + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " helpers changed" +} +`) + headRef := commitChangedTestPackagesFixture(t, repository, "change helpers only") + + if selected := runChangedTestPackagesShard(t, repository, "helpers", baseRef, headRef); len(selected) == 0 { + t.Fatal("helpers shard selected nothing for a helpers-only change") + } + // An unaffected shard is a normal outcome and must exit zero with no + // output so the CI step can no-op instead of failing the job. + if selected := runChangedTestPackagesShard(t, repository, "app", baseRef, headRef); len(selected) != 0 { + t.Fatalf("app shard = %q, want empty for a helpers-only change", selected) + } +} + +func TestChangedTestPackagesShardSelectionRejectsMissingShardArgument(t *testing.T) { + repository := newShardChangedPackagesRepository(t) + baseRef := commitChangedTestPackagesFixture(t, repository, "initial shard fixture") + + script := filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh") + command := exec.Command(script, "list-shard", baseRef, baseRef) + command.Dir = repository + output, err := command.CombinedOutput() + if err == nil { + t.Fatalf("list-shard without a shard argument unexpectedly succeeded:\n%s", output) + } + if !strings.Contains(string(output), "usage:") { + t.Fatalf("missing shard argument failure = %q, want usage diagnostic", output) + } +} + +func runChangedTestPackagesShard(t *testing.T, repository, shard, baseRef, headRef string) []string { + t.Helper() + + output := runChangedTestPackagesCommand( + t, + repository, + filepath.Join(repository, "scripts", "ci", "changed-test-packages.sh"), + "list-shard", + shard, + baseRef, + headRef, + ) + return strings.Fields(output) +} + +// newShardChangedPackagesRepository builds a fixture whose directory layout +// matches every shard scripts/ci/test-packages.sh knows about, so shard +// selection can be exercised without depending on the real repository's working +// tree state. Every package imports core, letting a single core edit mark the +// whole module impacted. +func newShardChangedPackagesRepository(t *testing.T) string { + t.Helper() + + repository := t.TempDir() + for path, contents := range map[string]string{ + "go.mod": `module example.com/shardfixture + +go 1.22 +`, + "core/core.go": `package core + +func Value() string { + return "core" +} +`, + "internal/app/app.go": `package app + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " app" +} +`, + "internal/generator/gen/gen.go": `package gen + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " gen" +} +`, + "internal/helpers/helpers.go": `package helpers + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " helpers" +} +`, + "internal/cli/cli.go": `package cli + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " cli" +} +`, + "test/smoke/smoke.go": `package smoke + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " smoke" +} +`, + "test/scripts/scripts.go": `package scripts + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " scripts" +} +`, + "pkg/extra/extra.go": `package extra + +import "example.com/shardfixture/core" + +func Value() string { + return core.Value() + " extra" +} +`, + } { + writeChangedTestPackagesFixture(t, repository, path, contents) + } + + projectRoot, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("resolve project root: %v", err) + } + for _, name := range []string{"changed-test-packages.sh", "test-packages.sh"} { + script, err := os.ReadFile(filepath.Join(projectRoot, "scripts", "ci", name)) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + scriptPath := filepath.Join(repository, "scripts", "ci", name) + if err := os.MkdirAll(filepath.Dir(scriptPath), 0o755); err != nil { + t.Fatalf("create script directory: %v", err) + } + if err := os.WriteFile(scriptPath, script, 0o755); err != nil { + t.Fatalf("copy %s: %v", name, err) + } + } + + runChangedTestPackagesCommand(t, repository, "git", "init", "-q", "-b", "main") + runChangedTestPackagesCommand(t, repository, "git", "config", "user.name", "DWS CI") + runChangedTestPackagesCommand(t, repository, "git", "config", "user.email", "dws-ci@example.invalid") + return repository +} + func newChangedTestPackagesRepository(t *testing.T) string { t.Helper() From 308e71c7833bc577e3efcf8ba002419715d66b6a Mon Sep 17 00:00:00 2001 From: chichuan Date: Sat, 15 Aug 2026 22:32:39 +0800 Subject: [PATCH 2/3] fix(ci): pass focused shard packages through a file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading the package list with `mapfile < file` has unambiguous line semantics. Routing it through a step output and a here-string instead would append an extra empty array element if the value ever carried a trailing newline, and that element would reach go test as an empty package argument. The step output now carries only a single-line boolean, and the list travels through RUNNER_TEMP. An explicit empty-entry guard fails closed if the file is ever malformed. This job cannot execute on its own pull request — editing a workflow routes the revision to full_suite, which skips the focused path — so the implementation deliberately avoids depending on platform-specific trailing-newline behavior that local verification cannot observe. --- .github/workflows/ci.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d3fa1e3..ed9fe4f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -577,33 +577,41 @@ jobs: if [ -z "$package_output" ]; then echo "No buildable Go package in shard $TEST_SHARD is affected by this revision." \ >> "$GITHUB_STEP_SUMMARY" + echo "affected=false" >> "$GITHUB_OUTPUT" exit 0 fi - { - echo "packages<> "$GITHUB_OUTPUT" + # The package list travels through a file rather than a step output: + # reading it with `mapfile < file` has unambiguous line semantics, + # whereas a here-string over a multi-line output would append an extra + # empty element if the value ever carried a trailing newline, and an + # empty element would reach go test as an empty package argument. + printf '%s\n' "$package_output" > "$RUNNER_TEMP/focused-shard-packages.txt" + echo "affected=true" >> "$GITHUB_OUTPUT" - name: Build - if: ${{ matrix.shard == 'remaining' && steps.select.outputs.packages != '' }} + if: ${{ matrix.shard == 'remaining' && steps.select.outputs.affected == 'true' }} run: make build - name: Install archive tooling - if: ${{ matrix.shard == 'release-scripts' && steps.select.outputs.packages != '' }} + if: ${{ matrix.shard == 'release-scripts' && steps.select.outputs.affected == 'true' }} run: sudo apt-get update && sudo apt-get install -y zip unzip - name: Test shard with Race Detection - if: ${{ steps.select.outputs.packages != '' }} + if: ${{ steps.select.outputs.affected == 'true' }} shell: bash env: DWS_PACKAGE_VERSION: 0.0.0-test TEST_SHARD: ${{ matrix.shard }} - SHARD_PACKAGES: ${{ steps.select.outputs.packages }} run: | set -euo pipefail - mapfile -t packages <<< "$SHARD_PACKAGES" + mapfile -t packages < "$RUNNER_TEMP/focused-shard-packages.txt" test "${#packages[@]}" -gt 0 + for package in "${packages[@]}"; do + test -n "$package" || { + echo "shard package list contains an empty entry" >&2 + exit 1 + } + done if [ "$TEST_SHARD" = app ]; then # A single long-lived app test process retains every constructed # command tree in framework registries. Isolate Schema assembly and From c4f2ab631b4965674dbfee03e891ee00db4e8cd6 Mon Sep 17 00:00:00 2001 From: chichuan Date: Sat, 15 Aug 2026 22:58:30 +0800 Subject: [PATCH 3/3] fix(ci): assert the focused path's shard shape in the workflow contract The workflow contract pinned the focused path by literal: the job name `Test (changed packages)`, the unsharded `list "$TEST_BASE_REF" "$TEST_HEAD_REF"` call, and a single `go test -timeout=15m` line standing in for internal/app's package-level headroom. Sharding the job changed all three literals, so `Test (workflow and release contracts)` failed on this branch even though every shard selection test passed. Each invariant the contract guarded still holds, so the assertions are updated to the new shape rather than relaxed: - the focused job must still exist, now as the matrix job, named the way the contract already names `Test (race: ${{ matrix.shard }})`; - package selection must still derive from the authoritative synthetic merge base/head, now with an explicit shard argument, so pointing it at any other ref still fails the contract; - internal/app's headroom is asserted through the process-isolating helper and the per-shard budgets, mirroring the assertions already applied to test-race. That is stronger than the old single -timeout: it pins the mechanism that keeps the suite inside its budget rather than the number alone. release-scripts membership is asserted too, because its dedicated job only runs at full-suite or release-sensitive scope, so losing it here would silently stop testing test/scripts changes. The shard comparisons in the focused job are quoted so that job reads verbatim like test-race's. Ablating the implementation one change at a time turns the contract red in all five cases: removing the app helper call, dropping release-scripts from the matrix, selecting from HEAD~1, collapsing the matrix back to a single unsharded job, and dropping the cli/smoke timeout budget. --- .github/workflows/ci.yml | 8 +++---- test/scripts/changelog_pr_gate_test.go | 30 ++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed9fe4f5..a4d30325 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -612,7 +612,7 @@ jobs: exit 1 } done - if [ "$TEST_SHARD" = app ]; then + if [ "$TEST_SHARD" = "app" ]; then # A single long-lived app test process retains every constructed # command tree in framework registries. Isolate Schema assembly and # bounded name ranges so each process releases that state on exit. @@ -620,7 +620,7 @@ jobs: ./scripts/ci/run-app-race-tests.sh run "${packages[0]}" exit 0 fi - if [ "$TEST_SHARD" = release-scripts ]; then + if [ "$TEST_SHARD" = "release-scripts" ]; then # Mirror the dedicated release-contract job: these suites shell out # to archive tooling and are not race-instrumented there. go test -v -count=1 -timeout=10m "${packages[@]}" @@ -629,8 +629,8 @@ jobs: # cli/smoke own heavy NewRootCommand / Schema assembly under -race; # give them a dedicated package timeout on slower hosted runners. timeout_budget=12m - if [ "$TEST_SHARD" = cli ] || - [ "$TEST_SHARD" = smoke ]; then + if [ "$TEST_SHARD" = "cli" ] || + [ "$TEST_SHARD" = "smoke" ]; then timeout_budget=15m fi go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}" diff --git a/test/scripts/changelog_pr_gate_test.go b/test/scripts/changelog_pr_gate_test.go index 8f6be15a..849d9191 100644 --- a/test/scripts/changelog_pr_gate_test.go +++ b/test/scripts/changelog_pr_gate_test.go @@ -1151,13 +1151,13 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { "filename.startsWith('pkg/cmdutil/')", "filename === 'skills_embed.go'", "filename.startsWith('test/mock_mcp/')", - "name: Test (changed packages)", + `name: "Test (focused: ${{ matrix.shard }})"`, "changed-test-packages.sh", "Verify authoritative synthetic merge", `test "$(git rev-parse HEAD^1)" = "$PR_BASE_SHA"`, `test "$(git rev-parse HEAD^2)" = "$PR_HEAD_SHA"`, `echo "TEST_HEAD_REF=$(git rev-parse HEAD)"`, - "list \"$TEST_BASE_REF\" \"$TEST_HEAD_REF\"", + `list-shard "$TEST_SHARD" "$TEST_BASE_REF" "$TEST_HEAD_REF"`, "needs.lint.outputs.full_suite != 'true'", `name: "Test (race: ${{ matrix.shard }})"`, "name: Test (workflow and release contracts)", @@ -1217,8 +1217,30 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) { if !strings.Contains(focusedJob, "timeout-minutes: 20") { t.Error("focused test job must allow the scoped race suite up to 20 minutes") } - if !strings.Contains(focusedJob, `go test -v -race -count=1 -timeout=15m "${packages[@]}"`) { - t.Error("focused race tests must retain enough package-level time for internal/app") + // The focused path fans the impacted set across the same shards as test-race + // and runs each shard the way test-race runs it, so no single job carries + // internal/app together with its reverse dependencies. internal/app keeps its + // package-level headroom through the process-isolating helper instead of one + // long -timeout, which is strictly stronger: every process releases the + // framework registries it populated. release-scripts is asserted because its + // dedicated job only runs at full-suite or release-sensitive scope, so losing + // it here would silently stop testing test/scripts changes. + for _, want := range []string{ + `if [ "$TEST_SHARD" = "app" ]; then`, + `test "${#packages[@]}" -eq 1`, + `./scripts/ci/run-app-race-tests.sh run "${packages[0]}"`, + `if [ "$TEST_SHARD" = "release-scripts" ]; then`, + `go test -v -count=1 -timeout=10m "${packages[@]}"`, + "timeout_budget=12m", + `if [ "$TEST_SHARD" = "cli" ] ||`, + `[ "$TEST_SHARD" = "smoke" ]; then`, + "timeout_budget=15m", + `go test -v -race -count=1 -timeout="$timeout_budget" "${packages[@]}"`, + "- release-scripts", + } { + if !strings.Contains(focusedJob, want) { + t.Errorf("focused test job missing shard contract %q", want) + } } raceStart := focusedEnd