From 4592a4d8301572ee2c7a04f29c99cb1e50c44834 Mon Sep 17 00:00:00 2001 From: hongming-codex-laptop Date: Wed, 13 May 2026 17:11:12 -0700 Subject: [PATCH] fix(ci): avoid heavy fanout for workflow-only PRs --- .gitea/workflows/ci.yml | 29 +++++++---- tests/test_lint_workflow_yaml.py | 87 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 10 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 2703f0f7..16560e92 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -107,16 +107,25 @@ jobs: echo "scripts=true" >> "$GITHUB_OUTPUT" exit 0 fi - # Both .github/workflows/ci.yml AND .gitea/workflows/ci.yml count - # as "this workflow changed" — either edit should force-run every - # downstream job. The Gitea port follows the same shape as the - # GitHub original so behavior matches when triggered on either - # platform. - DIFF=$(git diff --name-only "$BASE" HEAD 2>/dev/null || echo ".gitea/workflows/ci.yml") - echo "platform=$(echo "$DIFF" | grep -qE '^workspace-server/|^\.gitea/workflows/ci\.yml$|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT" - echo "canvas=$(echo "$DIFF" | grep -qE '^canvas/|^\.gitea/workflows/ci\.yml$|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT" - echo "python=$(echo "$DIFF" | grep -qE '^workspace/|^\.gitea/workflows/ci\.yml$|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT" - echo "scripts=$(echo "$DIFF" | grep -qE '^tests/e2e/|^scripts/|^infra/scripts/|^\.gitea/workflows/ci\.yml$|^\.github/workflows/ci\.yml$' && echo true || echo false)" >> "$GITHUB_OUTPUT" + # Workflow-only edits are covered by the workflow lint family + # and by this workflow's always-present required jobs. Do not fan + # those edits out into Go/Canvas/Python/shellcheck work; the + # downstream jobs still emit their required contexts via no-op + # steps when their surface flag is false. + # + # If the diff itself cannot be trusted, fail open by running every + # surface instead of silently under-testing the PR. + if ! DIFF=$(git diff --name-only "$BASE" HEAD 2>/dev/null); then + echo "platform=true" >> "$GITHUB_OUTPUT" + echo "canvas=true" >> "$GITHUB_OUTPUT" + echo "python=true" >> "$GITHUB_OUTPUT" + echo "scripts=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "platform=$(echo "$DIFF" | grep -qE '^workspace-server/' && echo true || echo false)" >> "$GITHUB_OUTPUT" + echo "canvas=$(echo "$DIFF" | grep -qE '^canvas/' && echo true || echo false)" >> "$GITHUB_OUTPUT" + echo "python=$(echo "$DIFF" | grep -qE '^workspace/' && echo true || echo false)" >> "$GITHUB_OUTPUT" + echo "scripts=$(echo "$DIFF" | grep -qE '^tests/e2e/|^scripts/|^infra/scripts/' && echo true || echo false)" >> "$GITHUB_OUTPUT" # Platform (Go) — Go build/vet/test/lint + coverage gates. The always-run # + per-step gating shape preserves the GitHub-side required-check name diff --git a/tests/test_lint_workflow_yaml.py b/tests/test_lint_workflow_yaml.py index 55835235..4cd4b151 100644 --- a/tests/test_lint_workflow_yaml.py +++ b/tests/test_lint_workflow_yaml.py @@ -22,6 +22,7 @@ Cross-links: """ from __future__ import annotations +import re import subprocess import sys import textwrap @@ -542,3 +543,89 @@ def test_rule9_prod_manual_deploy_allows_rollback_control(tmp_path): _write(tmp_path, "ok.yml", PROD_ROLLBACK_OK) r = _run_lint(tmp_path) assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}" + + +# --------------------------------------------------------------------------- +# CI change detector fanout — workflow-only PRs keep required contexts without +# running Go/Canvas/Python/shellcheck heavy steps. +# --------------------------------------------------------------------------- + +CI_WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "ci.yml" +CI_SURFACES = ("platform", "canvas", "python", "scripts") + + +def _ci_change_patterns() -> dict[str, re.Pattern[str]]: + text = CI_WORKFLOW.read_text(encoding="utf-8") + patterns: dict[str, re.Pattern[str]] = {} + for surface, pattern in re.findall( + r'echo "(platform|canvas|python|scripts)=.*?grep -qE \'([^\']+)\'', + text, + ): + patterns[surface] = re.compile(pattern) + assert set(patterns) == set(CI_SURFACES) + return patterns + + +def _classify_ci_change(*paths: str) -> dict[str, bool]: + patterns = _ci_change_patterns() + return { + surface: any(pattern.search(path) for path in paths) + for surface, pattern in patterns.items() + } + + +def test_ci_change_detector_workflow_only_edits_do_not_trigger_heavy_surfaces(): + assert _classify_ci_change(".gitea/workflows/ci.yml") == { + "platform": False, + "canvas": False, + "python": False, + "scripts": False, + } + assert _classify_ci_change(".github/workflows/ci.yml") == { + "platform": False, + "canvas": False, + "python": False, + "scripts": False, + } + + +def test_ci_change_detector_narrow_surface_edits_only_trigger_their_surface(): + assert _classify_ci_change("workspace-server/internal/handlers/foo.go") == { + "platform": True, + "canvas": False, + "python": False, + "scripts": False, + } + assert _classify_ci_change("canvas/app/page.tsx") == { + "platform": False, + "canvas": True, + "python": False, + "scripts": False, + } + assert _classify_ci_change("workspace/a2a_mcp_server.py") == { + "platform": False, + "canvas": False, + "python": True, + "scripts": False, + } + assert _classify_ci_change("tests/e2e/test_model_slug.sh") == { + "platform": False, + "canvas": False, + "python": False, + "scripts": True, + } + + +def test_ci_change_detector_docs_and_meta_scripts_do_not_trigger_surfaces(): + assert _classify_ci_change("README.md") == { + "platform": False, + "canvas": False, + "python": False, + "scripts": False, + } + assert _classify_ci_change(".gitea/scripts/lint-workflow-yaml.py") == { + "platform": False, + "canvas": False, + "python": False, + "scripts": False, + }