From d66afd16480122c5dcce2f11eb28403e90bbb7b3 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 18:10:41 +0100 Subject: [PATCH] Reject shipped documentation drift before CI fan-out PR2576's deterministic mirror failure admitted expensive frontend, backend and browser work that could not land. Run the existing whole-tree checker before dependency work in the build admission job and all three E2E entrypoints, retaining every existing check, secret scan and test gate. Exercise the actual workflow commands with synced, stale and orphaned guides. Change-source: pulse-maintainer --- .github/workflows/build-and-test.yml | 5 + .github/workflows/test-e2e.yml | 12 +++ .../subsystems/deployment-installability.md | 17 +++ .../installtests/build_release_assets_test.go | 100 ++++++++++++++++++ 4 files changed, 134 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index fa545bfde3..46bee9ebbe 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -79,6 +79,11 @@ jobs: persist-credentials: false fetch-depth: 0 + # Reject deterministic docs drift before queuing builds/tests. Keep the + # independent per-push secret scan and Public docs check unchanged. + - name: Check shipped docs before dependency work + run: python3 scripts/check_docs_mirror.py + - name: Classify changed files id: filter run: | diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index ec492a715d..03d1b1b11c 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -47,6 +47,10 @@ jobs: with: persist-credentials: false + # Fail before installs, Docker builds or the eight-shard fan-out. + - name: Check shipped docs before dependency work + run: python3 scripts/check_docs_mirror.py + - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -82,6 +86,10 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + # Fail before installs, Docker builds or the eight-shard fan-out. + - name: Check shipped docs before dependency work + run: python3 scripts/check_docs_mirror.py + - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -297,6 +305,10 @@ jobs: with: persist-credentials: false + # Fail before installs, Docker builds or the eight-shard fan-out. + - name: Check shipped docs before dependency work + run: python3 scripts/check_docs_mirror.py + - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index db7d27ba06..a8dbfbd5f0 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -15,6 +15,23 @@ ## Purpose +### Shipped documentation is checked before CI dependency work + +Build and Test checks every shipped Markdown mirror immediately after the +`changes` job's checkout, before classification can admit frontend, backend, +script/build or benchmark work. Core E2E does the same before dependency setup +in tier selection and both independent provisioning jobs; a failing tier check +does not admit the eight browser shards. Existing check names, secret scanning, +Public docs validation, test coverage and failure verdicts remain unchanged. + +The check is blocking and unconditional, including dispatches. It uses the +whole-tree mode of `scripts/check_docs_mirror.py`, not staged-only tolerance. +`TestCIDocsMirrorPreflight` parses all four actual workflow entrypoints and +executes their command with the real checker: stale or orphaned shipped guides +stop before a dependency-work marker, while identical copies allow it. This +prevents deterministic documentation faults from consuming expensive CI work; +local controls do not establish hosted queue-time improvement or publication. + ### Native helper verification follows helper-only changes Unified Agent Native Verification admits helper command, protocol, shared diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 438cfb1bb1..da492f2c72 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -3786,6 +3786,106 @@ func TestReleaseTrainCITriggersIncludeBuildAndE2E(t *testing.T) { } } +// Documentation drift is deterministic and cheap to reject. It must not wait +// behind dependency installation or fan out into builds that cannot land. +func TestCIDocsMirrorPreflight(t *testing.T) { + type step struct { + Name string `yaml:"name"` + Uses string `yaml:"uses"` + Run string `yaml:"run"` + If string `yaml:"if"` + ContinueOnError bool `yaml:"continue-on-error"` + Directory string `yaml:"working-directory"` + } + type job struct { + Steps []step `yaml:"steps"` + } + for _, site := range []struct{ workflow, job string }{ + {"build-and-test.yml", "changes"}, + {"test-e2e.yml", "tier-selection"}, + {"test-e2e.yml", "offline-org-provisioning"}, + {"test-e2e.yml", "agent-registration"}, + } { + t.Run(site.workflow+"/"+site.job, func(t *testing.T) { + content, err := os.ReadFile(repoFile(".github", "workflows", site.workflow)) + if err != nil { + t.Fatal(err) + } + var workflow struct { + Jobs map[string]job `yaml:"jobs"` + } + if err := yaml.Unmarshal(content, &workflow); err != nil { + t.Fatal(err) + } + steps := workflow.Jobs[site.job].Steps + if len(steps) < 2 || !strings.HasPrefix(steps[0].Uses, "actions/checkout@") { + t.Fatal("preflight must follow source checkout") + } + guard := steps[1] + if guard.Run != "python3 scripts/check_docs_mirror.py" || guard.If != "" || + guard.ContinueOnError || guard.Directory != "" || guard.Uses != "" { + t.Fatal("whole-tree mirror check must be an unconditional blocking step immediately after checkout") + } + checker, err := os.ReadFile(repoFile("scripts", "check_docs_mirror.py")) + if err != nil { + t.Fatal(err) + } + for _, fixture := range []struct { + name, source, mirror, diagnostic string + wantExit int + }{ + {"synced", "# Safe guidance\n", "# Safe guidance\n", "passed (1 shipped docs)", 0}, + {"stale-shipped-guide", "# Safe guidance\nDo not repeat a guest probe.\n", "# Safe guidance\n", "docs/GUIDE.md and frontend-modern/public/docs/GUIDE.md differ", 1}, + {"missing-source", "", "# Orphan guide\n", "shipped copy has no repo source docs/GUIDE.md", 1}, + } { + t.Run(fixture.name, func(t *testing.T) { + root := t.TempDir() + write := func(path string, data []byte) { + t.Helper() + full := filepath.Join(root, path) + if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, data, 0600); err != nil { + t.Fatal(err) + } + } + write("scripts/check_docs_mirror.py", checker) + write("frontend-modern/public/docs/GUIDE.md", []byte(fixture.mirror)) + if fixture.source != "" { + write("docs/GUIDE.md", []byte(fixture.source)) + } + // Run the workflow's actual command and a harmless stand-in for + // the next dependency step under GitHub's fail-fast shell mode. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, "bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", guard.Run+"\nprintf started > dependency-work") + cmd.Dir = root + output, err := cmd.CombinedOutput() + exit := 0 + if err != nil { + failure, ok := err.(*exec.ExitError) + if !ok { + t.Fatal(err) + } + exit = failure.ExitCode() + } + if exit != fixture.wantExit || !strings.Contains(string(output), fixture.diagnostic) { + t.Fatalf("exit=%d, want=%d, output=%s", exit, fixture.wantExit, output) + } + _, markerErr := os.Stat(filepath.Join(root, "dependency-work")) + if fixture.wantExit == 0 && markerErr != nil { + t.Fatal("synced documentation must allow later dependency work") + } + if fixture.wantExit != 0 && !os.IsNotExist(markerErr) { + t.Fatal("failed preflight must stop before dependency work") + } + }) + } + }) + } +} + func TestProviderPairDockerProofRunsBeforeFreezeAndOnExactCandidate(t *testing.T) { const testName = "TestIntegrationProviderPairNetworkIsolation" const helperImage = "alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6"