mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 13:10:03 +00:00
Keep isolated issue-triage changes on their dedicated CI path (#3252)
The proposed-source triage suite already exercises the locked parser and silent-mutation consumers. Pure helper edits still queued unrelated application suites for more than twenty minutes. Recognise only the six exact triage inputs from a complete tree diff; mixed, missing and control-path changes retain full application checks. Keep workflow output outside the fixture source tree so real-diff controls do not stage it. Contract-Neutral: CI admission for isolated issue-event inputs only; locked triage tests, secret scanning, governance and mixed-source runtime checks remain enforced, with no product contract or permission change. --- Fleet candidate `20261025T085429Z-delivery-trust` from the `delivery-trust` lane. Reviewed tip `e404c2fe07096fcf9b733f307a5a834719a966c5` on `main`. [How Pulse handles triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
This commit is contained in:
@@ -119,6 +119,19 @@ jobs:
|
||||
echo "triage_helper=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
# The isolated issue parser is not application source. Its complete
|
||||
# proposed-source tests run in triage-helper instead of queuing the
|
||||
# unrelated frontend, backend and installer suites. The selector
|
||||
# consumes a complete NUL-delimited diff with an exact allowlist;
|
||||
# mixed changes and changes to this admission still get runtime CI.
|
||||
if [ "$(python3 scripts/ci_triage_only.py --base "$base" --head "${{ github.sha }}")" = true ]; then
|
||||
echo "Issue-triage-only tree diff; dedicated triage checks apply, application checks are not applicable."
|
||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||
echo "frontend_deps=false" >> "$GITHUB_OUTPUT"
|
||||
echo "go_compile=false" >> "$GITHUB_OUTPUT"
|
||||
echo "triage_helper=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
changed=$(git diff --name-only "$base" "${{ github.sha }}")
|
||||
printf 'Changed files:\n%s\n' "$changed"
|
||||
# The issue-event job tests before minting its App token, but that
|
||||
@@ -126,7 +139,7 @@ jobs:
|
||||
# policy on the proposed source too, including documentation-only
|
||||
# guidance and every workflow consumed by the policy tests.
|
||||
triage_helper=false
|
||||
if printf '%s\n' "$changed" | grep -E '^\.github/scripts/(issue-version-triage(\.test)?\.cjs|triage-comment-policy\.test\.cjs|package(-lock)?\.json)$|^\.github/ISSUE_TEMPLATE/|^\.github/workflows/|^docs/ISSUE_TRIAGE\.md$|^scripts/tests/test_ci_browser_proof_only\.py$' >/dev/null; then
|
||||
if printf '%s\n' "$changed" | grep -E '^\.github/scripts/(issue-version-triage(\.test)?\.cjs|triage-comment-policy\.test\.cjs|package(-lock)?\.json)$|^\.github/ISSUE_TEMPLATE/|^\.github/workflows/|^docs/ISSUE_TRIAGE\.md$|^scripts/(ci_triage_only\.py|tests/test_ci_browser_proof_only\.py)$' >/dev/null; then
|
||||
triage_helper=true
|
||||
fi
|
||||
echo "triage_helper=$triage_helper" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Select the dedicated issue-triage check for an isolated triage tree change.
|
||||
|
||||
These inputs run only in the issue-event workflow, not the Pulse application.
|
||||
Their locked parser and silent-mutation tests still run on the proposed source,
|
||||
as do the independent secret scan and governance. Mixed, empty or unavailable
|
||||
diffs retain the ordinary application checks. Changes to this selector or its
|
||||
workflow/tests cannot opt themselves out of those checks.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
COMMIT = re.compile(r"[0-9a-f]{40}")
|
||||
TRIAGE_INPUTS = frozenset({
|
||||
b".github/scripts/issue-version-triage.cjs",
|
||||
b".github/scripts/issue-version-triage.test.cjs",
|
||||
b".github/scripts/triage-comment-policy.test.cjs",
|
||||
b".github/scripts/package.json",
|
||||
b".github/scripts/package-lock.json",
|
||||
b"docs/ISSUE_TRIAGE.md",
|
||||
})
|
||||
|
||||
|
||||
def triage_only_paths(paths):
|
||||
return bool(paths) and all(path in TRIAGE_INPUTS for path in paths)
|
||||
|
||||
|
||||
def triage_only_diff(base, head):
|
||||
if not COMMIT.fullmatch(base) or not COMMIT.fullmatch(head):
|
||||
return False
|
||||
try:
|
||||
# Inspect the entire range, including deleted names. Neither a rename
|
||||
# into this allowlist nor Git's quoted display of a filename may hide
|
||||
# a product change. A missing/truncated diff always keeps runtime CI.
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--no-ext-diff", "--no-renames", "--name-only", "-z",
|
||||
base, head, "--"],
|
||||
capture_output=True, timeout=30, check=False,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
print("Triage tree diff unavailable; retaining application CI.", file=sys.stderr)
|
||||
return False
|
||||
if result.returncode != 0 or not result.stdout.endswith(b"\0"):
|
||||
return False
|
||||
return triage_only_paths(result.stdout[:-1].split(b"\0"))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--base", required=True)
|
||||
parser.add_argument("--head", required=True)
|
||||
args = parser.parse_args()
|
||||
print("true" if triage_only_diff(args.base, args.head) else "false")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute receipt-only diff admission and the workflows' actual verdict paths."""
|
||||
"""Execute receipt/triage-only admission and the workflows' actual verdict paths."""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
@@ -19,6 +19,9 @@ E2E = ROOT / ".github/workflows/test-e2e.yml"
|
||||
SPEC = importlib.util.spec_from_file_location("ci_browser_proof_only", SCRIPT)
|
||||
SCOPE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(SCOPE)
|
||||
TRIAGE_SPEC = importlib.util.spec_from_file_location("ci_triage_only", ROOT / "scripts/ci_triage_only.py")
|
||||
TRIAGE = importlib.util.module_from_spec(TRIAGE_SPEC)
|
||||
TRIAGE_SPEC.loader.exec_module(TRIAGE)
|
||||
RECEIPT = "frontend-modern/browser-verification/2026-10-14-proof.json"
|
||||
JOBS = ("tier-selection", "test-images", "e2e", "agent-registration")
|
||||
|
||||
@@ -84,13 +87,61 @@ class ReceiptScopeTest(unittest.TestCase):
|
||||
])
|
||||
|
||||
|
||||
class TriageScopeTest(unittest.TestCase):
|
||||
def test_only_complete_exact_triage_inputs_are_eligible(self):
|
||||
self.assertTrue(TRIAGE.triage_only_paths(list(TRIAGE.TRIAGE_INPUTS)))
|
||||
self.assertFalse(TRIAGE.triage_only_paths([]))
|
||||
for path in TRIAGE.TRIAGE_INPUTS:
|
||||
with self.subTest(path=path):
|
||||
self.assertTrue(TRIAGE.triage_only_paths([path]))
|
||||
for suffix in (b".extra", b"/extra", b"\ninternal/api/server.go"):
|
||||
self.assertFalse(TRIAGE.triage_only_paths([path + suffix]))
|
||||
|
||||
def test_product_tests_controls_templates_workflows_and_unknown_files_keep_runtime(self):
|
||||
for path in (
|
||||
b"frontend-modern/src/App.tsx", b"frontend-modern/package-lock.json",
|
||||
b"internal/api/server.go", b"internal/api/server_test.go", b"go.mod", b"go.sum",
|
||||
b"install.sh", b"scripts/tests/run.sh", b"tests/integration/playwright.config.ts",
|
||||
b".github/workflows/build-and-test.yml", b".github/workflows/issue-version-label-sync.yml",
|
||||
b".github/ISSUE_TEMPLATE/bug_report.yml", b".github/scripts/other.cjs", b"docs/other.md",
|
||||
b"scripts/ci_triage_only.py", b"scripts/tests/test_ci_browser_proof_only.py",
|
||||
RECEIPT.encode(), b"other/.github/scripts/package.json",
|
||||
):
|
||||
with self.subTest(path=path):
|
||||
self.assertFalse(TRIAGE.triage_only_paths([*TRIAGE.TRIAGE_INPUTS, path]))
|
||||
|
||||
def test_unknown_failed_incomplete_or_missing_diff_keeps_runtime(self):
|
||||
for base, head in (("", "a" * 40), ("HEAD^", "a" * 40), ("a" * 40, "main")):
|
||||
with patch.object(TRIAGE.subprocess, "run") as run:
|
||||
self.assertFalse(TRIAGE.triage_only_diff(base, head))
|
||||
run.assert_not_called()
|
||||
path = b".github/scripts/issue-version-triage.cjs"
|
||||
for output, code in ((b"", 0), (path, 0), (path + b"\0", 1)):
|
||||
with patch.object(TRIAGE.subprocess, "run", return_value=subprocess.CompletedProcess([], code, output)):
|
||||
self.assertFalse(TRIAGE.triage_only_diff("a" * 40, "b" * 40))
|
||||
for error in (OSError("fixture unavailable"), subprocess.TimeoutExpired("git", 30)):
|
||||
with patch.object(TRIAGE.subprocess, "run", side_effect=error):
|
||||
self.assertFalse(TRIAGE.triage_only_diff("a" * 40, "b" * 40))
|
||||
|
||||
def test_complete_null_delimited_diff_disables_rename_and_external_diff(self):
|
||||
path = b".github/scripts/issue-version-triage.cjs\0"
|
||||
for output, expected in ((path, True), (path * 4000 + b"internal/api/server.go\0", False)):
|
||||
with patch.object(TRIAGE.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, output)) as run:
|
||||
self.assertEqual(TRIAGE.triage_only_diff("a" * 40, "b" * 40), expected)
|
||||
self.assertEqual(run.call_args.args[0], [
|
||||
"git", "diff", "--no-ext-diff", "--no-renames", "--name-only", "-z",
|
||||
"a" * 40, "b" * 40, "--",
|
||||
])
|
||||
|
||||
|
||||
class GitTreeScopeTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.repo = Path(self.temp.name)
|
||||
self.git("init", "-q", "--initial-branch=main")
|
||||
for name in ("scripts/ci_browser_proof_only.py", "scripts/go-test-compile-required.sh"):
|
||||
for name in ("scripts/ci_browser_proof_only.py", "scripts/ci_triage_only.py",
|
||||
"scripts/go-test-compile-required.sh"):
|
||||
path = self.repo / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / name, path)
|
||||
@@ -160,13 +211,15 @@ class GitTreeScopeTest(unittest.TestCase):
|
||||
"github.event_name": "pull_request", "github.event.pull_request.base.sha": base,
|
||||
"github.event.before": base, "github.sha": head,
|
||||
})
|
||||
output = self.repo / "outputs"
|
||||
if output.exists():
|
||||
output.unlink()
|
||||
result = subprocess.run(["bash", "-c", script], cwd=self.repo, capture_output=True,
|
||||
text=True, timeout=10, env={**os.environ, "GITHUB_OUTPUT": str(output)})
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
return dict(line.split("=", 1) for line in output.read_text().splitlines())
|
||||
# Workflow output is runner evidence, not a source change. Keep it
|
||||
# outside the fixture repository so the next commit cannot stage it
|
||||
# and accidentally turn an isolated change into a mixed tree diff.
|
||||
with tempfile.TemporaryDirectory() as output_dir:
|
||||
output = Path(output_dir) / "outputs"
|
||||
result = subprocess.run(["bash", "-c", script], cwd=self.repo, capture_output=True,
|
||||
text=True, timeout=10, env={**os.environ, "GITHUB_OUTPUT": str(output)})
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
return dict(line.split("=", 1) for line in output.read_text().splitlines())
|
||||
|
||||
def test_actual_build_classifier_omits_only_receipt_runtime_checks(self):
|
||||
self.write(RECEIPT, "{}\n")
|
||||
@@ -201,7 +254,7 @@ class GitTreeScopeTest(unittest.TestCase):
|
||||
".github/ISSUE_TEMPLATE/bug_report.yml", ".github/ISSUE_TEMPLATE/config.yml",
|
||||
".github/workflows/issue-version-label-sync.yml",
|
||||
".github/workflows/build-and-test.yml", ".github/workflows/policy-consumer.yaml",
|
||||
"docs/ISSUE_TRIAGE.md", "scripts/tests/test_ci_browser_proof_only.py",
|
||||
"docs/ISSUE_TRIAGE.md", "scripts/ci_triage_only.py", "scripts/tests/test_ci_browser_proof_only.py",
|
||||
):
|
||||
with self.subTest(path=path):
|
||||
before = self.git("rev-parse", "HEAD")
|
||||
@@ -209,7 +262,7 @@ class GitTreeScopeTest(unittest.TestCase):
|
||||
head = self.commit()
|
||||
self.assertEqual(self.classify(before, head)["triage_helper"], "true")
|
||||
|
||||
def test_triage_admission_uses_complete_range_and_does_not_expand_runtime_skip(self):
|
||||
def test_triage_admission_uses_complete_range_and_keeps_mixed_runtime_checks(self):
|
||||
self.write("docs/ISSUE_TRIAGE.md", "Changed operator guidance\n")
|
||||
guidance = self.commit()
|
||||
self.assertEqual(self.classify(self.base, guidance), {
|
||||
@@ -223,7 +276,7 @@ class GitTreeScopeTest(unittest.TestCase):
|
||||
self.write(".github/scripts/issue-version-triage.cjs", "// changed parser\n")
|
||||
parser = self.commit()
|
||||
self.assertEqual(self.classify(proof, parser), {
|
||||
"code": "true", "frontend_deps": "false", "go_compile": "false",
|
||||
"code": "false", "frontend_deps": "false", "go_compile": "false",
|
||||
"triage_helper": "true",
|
||||
})
|
||||
self.write("frontend-modern/package-lock.json", "{}\n")
|
||||
@@ -233,6 +286,49 @@ class GitTreeScopeTest(unittest.TestCase):
|
||||
self.assertEqual(combined["code"], "true")
|
||||
self.assertEqual(combined["triage_helper"], "true")
|
||||
|
||||
def test_isolated_triage_inputs_use_the_dedicated_check(self):
|
||||
for path in sorted(TRIAGE.TRIAGE_INPUTS):
|
||||
with self.subTest(path=path):
|
||||
before = self.git("rev-parse", "HEAD")
|
||||
self.write(path.decode(), "triage fixture\n")
|
||||
head = self.commit()
|
||||
self.assertEqual(self.classify(before, head), {
|
||||
"code": "false", "frontend_deps": "false", "go_compile": "false",
|
||||
"triage_helper": "true",
|
||||
})
|
||||
|
||||
def test_triage_skip_cannot_hide_source_renames_or_quoted_paths(self):
|
||||
self.write("internal/api/fixture.go", "package fixture\nvar Changed = true\n")
|
||||
product = self.commit()
|
||||
self.write(".github/scripts/issue-version-triage.cjs", "// parser fixture\n")
|
||||
parser = self.commit()
|
||||
self.assertEqual(self.classify(product, parser)["code"], "false")
|
||||
self.assertEqual(self.classify(self.base, parser)["code"], "true")
|
||||
(self.repo / "internal/api/fixture.go").rename(self.repo / ".github/scripts/issue-version-triage.test.cjs")
|
||||
renamed = self.commit()
|
||||
self.assertEqual(self.classify(parser, renamed)["code"], "true")
|
||||
self.write(".github/scripts/issue-version-triage.cjs\ninternal/api/fixture.go", "// quoted filename\n")
|
||||
quoted = self.commit()
|
||||
self.assertEqual(self.classify(renamed, quoted)["code"], "true")
|
||||
|
||||
def test_selector_failure_cannot_be_mistaken_for_a_narrow_change(self):
|
||||
self.write(".github/scripts/issue-version-triage.cjs", "// parser fixture\n")
|
||||
self.write("scripts/ci_triage_only.py", "raise SystemExit(42)\n")
|
||||
head = self.commit()
|
||||
self.assertEqual(self.classify(self.base, head)["code"], "true")
|
||||
|
||||
def test_controls_and_workflow_edits_cannot_skip_their_own_runtime_tests(self):
|
||||
for path in ("scripts/ci_triage_only.py", "scripts/tests/test_ci_browser_proof_only.py",
|
||||
".github/workflows/build-and-test.yml"):
|
||||
with self.subTest(path=path):
|
||||
before = self.git("rev-parse", "HEAD")
|
||||
(self.repo / path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with (self.repo / path).open("a") as control:
|
||||
control.write("\n# Changed CI control\n")
|
||||
head = self.commit()
|
||||
self.assertEqual(self.classify(before, head)["code"], "true")
|
||||
self.assertEqual(self.classify(before, head)["triage_helper"], "true")
|
||||
|
||||
def test_triage_path_lookalikes_and_unrelated_code_do_not_select_it(self):
|
||||
for path in (
|
||||
".github/scripts/issue-version-triage.cjs.extra",
|
||||
|
||||
Reference in New Issue
Block a user