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
14 changes: 13 additions & 1 deletion .github/agent-guides/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,19 @@ rename destinations keep full coverage. The authenticated caller's original
event/author admission remains outside mandatory execution selection.
Shared helpers, other workflows, tests,
fixtures, root configuration and unknown paths retain full coverage; missing Git
evidence fails the selector. Classification runs from the immutable trusted base
evidence fails the selector. Root documents are the only exception and need ordinary
regular-file modifications: the root `README.md` selects exactly the gated jobs whose
executed scripts read it, and the always-on documentation gates still run. Root
`AGENTS.md` has no gated reader, so it adds nothing beside a recognized non-Markdown
change; alone, with the README or with only Markdown it keeps full coverage. There is
no general Markdown exemption. `ci-selection-document-readers.sh` (run by
`test-ci-selection.sh`) derives the reader set from the workflow and every script,
action and Go module a gated job executes, and fails when the inventory's `README.md`
owners differ from it, when a gated job reads `AGENTS.md`, or when one copies or walks
the whole checkout. A new README-consuming check joins routing by adding `README.md`
to its inventory owners and its job id to the selector's reviewed reader list; a line
that only mentions a document (fixture data, a message or a comment) joins that
script's reviewed non-reading list with its exact text. Classification runs from the immutable trusted base
before candidate checkout and selection on the same read-only runner; there is
no separate prerequisite runner assignment. A successful trusted-base scope decision
for a deployment-only PR omits only the explicitly inventoried smoke jobs; the
Expand Down
4 changes: 4 additions & 0 deletions .github/scripts/ci-selection/dotnet_workflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func dotnetSelectionFixture(t *testing.T) (inventory, workflow) {
w.Jobs["test-go-cache-native-observer"] = job{If: selectionGuard("test-go-cache-native-observer"), Needs: []any{"select-ci-tests", "test-validate-go-postgresql-fixture"}}
i.Jobs["test-go-workflow-guards"] = []string{"actions/upload-coverage/"}
w.Jobs["test-go-workflow-guards"] = job{If: selectionGuard("test-go-workflow-guards"), Needs: []any{"select-ci-tests"}}
for id := range readmeReaders {
i.Jobs[id] = []string{rootReadme}
w.Jobs[id] = job{If: selectionGuard(id), Needs: []any{"select-ci-tests"}}
}
return i, w
}

Expand Down
8 changes: 4 additions & 4 deletions .github/scripts/ci-selection/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@
"test-enable-auto-merge": [],
"test-enable-auto-merge-actor-trust": [],
"test-enable-auto-merge-queued": [],
"test-enable-auto-merge-author-gate": [],
"test-enable-auto-merge-pentad-gate": [],
"test-enable-auto-merge-author-gate": ["README.md"],
"test-enable-auto-merge-pentad-gate": ["README.md"],
"test-enable-auto-merge-queue": [],
"test-create-release": [],
"test-create-release-no-issue-side-effects": [],
Expand Down Expand Up @@ -141,10 +141,10 @@
"test-validate-go-lint-blocks": ["actions/upload-coverage/"],
"test-validate-go-test-blocks": ["actions/upload-coverage/"],
"test-validate-go-project-concurrency-key": ["actions/upload-coverage/"],
"test-validate-go-gate-lockstep": ["actions/upload-coverage/", "actions/dependency-review/"],
"test-validate-go-gate-lockstep": ["actions/upload-coverage/", "actions/dependency-review/", "README.md"],
"test-lint-blocks": [],
"test-prepare-fixes": [],
"test-lint-gate-lockstep": ["actions/upload-coverage/"],
"test-lint-gate-lockstep": ["actions/upload-coverage/", "README.md"],
"test-apply-signed-fixes-suppresses-bots": [],
"test-apply-signed-fixes-verifies-without-a-patch": [],
"test-check-ci-run-budget": ["actions/check-ci-run-budget/"]
Expand Down
57 changes: 56 additions & 1 deletion .github/scripts/ci-selection/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,25 @@ const goCacheObserver = "test-go-cache-native-observer"
const goCacheFixture = "test-validate-go-postgresql-fixture"
const goWorkflowGuards = "test-go-workflow-guards"

// Root documents admitted only as ordinary regular-file modifications.
const rootReadme = "README.md"
const rootAgents = "AGENTS.md"

var ordinarySelectiveFiles = map[string]bool{
dotnetFixtureAction: true,
rootReadme: true,
rootAgents: true,
}

// Every gated job whose executed tests read the root README. The tracked-reader
// guard (.github/tests/ci-selection-document-readers.sh) derives this set from
// the workflow and its scripts; the inventory must own README.md for exactly
// these jobs. Always-on and preserved jobs read it without needing selection.
var readmeReaders = map[string]bool{
"test-enable-auto-merge-author-gate": true,
"test-enable-auto-merge-pentad-gate": true,
"test-lint-gate-lockstep": true,
"test-validate-go-gate-lockstep": true,
}

// Native fixture jobs and their hosted/credential controls share this local action.
Expand Down Expand Up @@ -64,6 +81,9 @@ func validateInventory(i inventory, w workflow) ([]string, error) {
if err := validateGoWorkflowGuards(i, w); err != nil {
return nil, err
}
if err := validateReadmeReaders(i, w); err != nil {
return nil, err
}
seen := map[string]bool{"select-ci-tests": true, "ci-required-checks": true}
for _, id := range i.Always {
if seen[id] {
Expand All @@ -87,7 +107,8 @@ func validateInventory(i inventory, w workflow) ([]string, error) {
seen[id] = true
for _, p := range paths {
if !regexp.MustCompile(`^actions/[a-z0-9-]+/$`).MatchString(p) &&
(p != dotnetFixtureAction || !hasDotnetOwner(id)) {
(p != dotnetFixtureAction || !hasDotnetOwner(id)) &&
(p != rootReadme || !readmeReaders[id]) {
return nil, fmt.Errorf("invalid selective owner %q", p)
}
}
Expand Down Expand Up @@ -171,6 +192,24 @@ func validateGoCacheOwners(i inventory, w workflow) error {
return nil
}

// Each README reader must own the file exactly once and stay selection-gated, so
// a README change cannot pass without the tests that read it.
func validateReadmeReaders(i inventory, w workflow) error {
for id := range readmeReaders {
count := 0
for _, p := range i.Jobs[id] {
if p == rootReadme {
count++
}
}
_, preserved := i.Preserved[id]
if _, ok := w.Jobs[id]; !ok || count != 1 || preserved {
return fmt.Errorf("incomplete or unbound README reader %s", id)
}
}
return nil
}

func hasDotnetOwner(id string) bool {
_, ok := dotnetOwners[id]
return ok
Expand Down Expand Up @@ -207,7 +246,14 @@ func selectJobs(i inventory, event string, paths []string) []string {
return all
}
selected := map[string]bool{}
// Root AGENTS.md has no gated reader. It may only accompany a recognized
// non-documentation change; alone or with other documents it stays full.
companion, source := false, false
for _, path := range paths {
if path == rootAgents {
companion = true
continue
}
known := false
for id, owners := range i.Jobs {
for _, owner := range owners {
Expand All @@ -219,6 +265,10 @@ func selectJobs(i inventory, event string, paths []string) []string {
if !known {
return all
}
source = source || !isDocumentation(path)
}
if companion && !source {
return all
}
result := []string{}
for id := range selected {
Expand All @@ -228,6 +278,11 @@ func selectJobs(i inventory, event string, paths []string) []string {
return result
}

func isDocumentation(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
return ext == ".md" || ext == ".markdown" || ext == ".mdx"
}

func changedPaths(root, base, head string) ([]string, error) {
sha := regexp.MustCompile(`^[a-f0-9]{40}$`)
if !sha.MatchString(base) || !sha.MatchString(head) {
Expand Down
6 changes: 3 additions & 3 deletions .github/scripts/ci-selection/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func TestEntrypointBindsGitEvidenceAndSelectedOutput(t *testing.T) {
fails bool
}{
{"selective PR", "pull_request", "true", head, "selected=[\"test-one\",\"test-one-wrapper\"]\n", false},
{"main retains complete inventory", "push", "true", "", "selected=[\"test-go-cache-native-observer\",\"test-go-workflow-guards\",\"test-one\",\"test-one-wrapper\",\"test-run-dotnet-tests-gate-lockstep\",\"test-run-dotnet-tests-mtp\",\"test-run-dotnet-tests-workflow\",\"test-two\",\"test-validate-go-postgresql-fixture\",\"test-workflow\"]\n", false},
{"main retains complete inventory", "push", "true", "", "selected=[\"test-enable-auto-merge-author-gate\",\"test-enable-auto-merge-pentad-gate\",\"test-go-cache-native-observer\",\"test-go-workflow-guards\",\"test-lint-gate-lockstep\",\"test-one\",\"test-one-wrapper\",\"test-run-dotnet-tests-gate-lockstep\",\"test-run-dotnet-tests-mtp\",\"test-run-dotnet-tests-workflow\",\"test-two\",\"test-validate-go-gate-lockstep\",\"test-validate-go-postgresql-fixture\",\"test-workflow\"]\n", false},
{"excluded event", "merge_group", "false", "", "selected=[]\n", false},
{"missing immutable head", "pull_request", "true", strings.Repeat("a", 40), "", true},
{"unknown eligibility", "push", "", "", "", true},
Expand Down Expand Up @@ -370,8 +370,8 @@ func TestDeploymentCatalogueScopeComposesWithSelection(t *testing.T) {
name, event, eligible, scope, want string
fails bool
}{
{"deployment smoke omission only", "pull_request", "true", "false", "selected=[\"test-go-cache-native-observer\",\"test-go-workflow-guards\",\"test-one-wrapper\",\"test-run-dotnet-tests-gate-lockstep\",\"test-run-dotnet-tests-mtp\",\"test-run-dotnet-tests-workflow\",\"test-validate-go-postgresql-fixture\",\"test-workflow\"]\n", false},
{"full scope", "pull_request", "true", "true", "selected=[\"test-go-cache-native-observer\",\"test-go-workflow-guards\",\"test-one\",\"test-one-wrapper\",\"test-run-dotnet-tests-gate-lockstep\",\"test-run-dotnet-tests-mtp\",\"test-run-dotnet-tests-workflow\",\"test-two\",\"test-validate-go-postgresql-fixture\",\"test-workflow\"]\n", false},
{"deployment smoke omission only", "pull_request", "true", "false", "selected=[\"test-enable-auto-merge-author-gate\",\"test-enable-auto-merge-pentad-gate\",\"test-go-cache-native-observer\",\"test-go-workflow-guards\",\"test-lint-gate-lockstep\",\"test-one-wrapper\",\"test-run-dotnet-tests-gate-lockstep\",\"test-run-dotnet-tests-mtp\",\"test-run-dotnet-tests-workflow\",\"test-validate-go-gate-lockstep\",\"test-validate-go-postgresql-fixture\",\"test-workflow\"]\n", false},
{"full scope", "pull_request", "true", "true", "selected=[\"test-enable-auto-merge-author-gate\",\"test-enable-auto-merge-pentad-gate\",\"test-go-cache-native-observer\",\"test-go-workflow-guards\",\"test-lint-gate-lockstep\",\"test-one\",\"test-one-wrapper\",\"test-run-dotnet-tests-gate-lockstep\",\"test-run-dotnet-tests-mtp\",\"test-run-dotnet-tests-workflow\",\"test-two\",\"test-validate-go-gate-lockstep\",\"test-validate-go-postgresql-fixture\",\"test-workflow\"]\n", false},
{"missing scope", "pull_request", "true", "", "", true},
{"malformed scope", "pull_request", "true", "unknown", "", true},
{"push cannot omit smoke", "push", "true", "false", "", true},
Expand Down
Loading
Loading