diff --git a/config/template-drift-allowlist.txt b/config/template-drift-allowlist.txt index 5f47f033e..b4a4752b3 100644 --- a/config/template-drift-allowlist.txt +++ b/config/template-drift-allowlist.txt @@ -109,8 +109,8 @@ reason = Intentional divergence re-baselined 2026-06-30: root and consumer guard main = .github/workflows/agents-issue-optimizer.yml template = templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml main_sha256 = 77eecd57e1a7637628bc16faedb6ef50f8cb5a810f94e286eaffd8dc50e02409 -template_sha256 = a93f1a86ee6396740647fc4e923297aa3d6224bcec3f791592258d2710776d6b -reason = Intentional divergence re-baselined 2026-07-07: root and consumer issue-optimizer workflows keep different auth plumbing/action pin surfaces, and the consumer template installs LLM deps from checked-out workflows-scripts/tools/requirements-llm.txt with python -m pip. Do not align wholesale because that would strip consumer action pins/token setup. +template_sha256 = a5f02fc1c8909466a254e4ec4ab85819644dd8a9008522e4a232f426c67e6435 +reason = Intentional divergence re-baselined 2026-07-31: root and consumer issue-optimizer workflows keep different auth plumbing/action pin surfaces, and the consumer template installs LLM deps from checked-out workflows-scripts/tools/requirements-llm.txt with python -m pip. The template additionally sparse-checks out `config` alongside `scripts/langchain` and `tools`, which the root workflow does not need because it runs in-tree where config/ is already present; without it tools/llm_registry.py resolves no model registry in the vendored tree (see #2850, #2861). Do not align wholesale because that would strip consumer action pins/token setup. [pair.12] main = .github/workflows/agents-keepalive-loop-reporter.yml diff --git a/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml b/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml index 085c381eb..f7f76fada 100644 --- a/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml +++ b/templates/consumer-repo/.github/workflows/agents-80-pr-event-hub.yml @@ -319,6 +319,7 @@ jobs: repository: stranske/Workflows token: ${{ secrets.SERVICE_BOT_PAT || github.token }} sparse-checkout: | + config scripts/langchain tools .github/scripts/error_classifier.js diff --git a/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml b/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml index 95650693d..dd95677e1 100644 --- a/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml +++ b/templates/consumer-repo/.github/workflows/agents-issue-optimizer.yml @@ -126,6 +126,7 @@ jobs: repository: stranske/Workflows path: workflows-scripts sparse-checkout: | + config scripts/langchain tools sparse-checkout-cone-mode: false diff --git a/tests/workflows/test_workflow_llm_installs.py b/tests/workflows/test_workflow_llm_installs.py index 70966b9a4..08b108927 100644 --- a/tests/workflows/test_workflow_llm_installs.py +++ b/tests/workflows/test_workflow_llm_installs.py @@ -31,7 +31,19 @@ LLM_CONFIG_PATHS = (Path("config/llm_slots.json"), Path("config/model_registry.json")) LANGCHAIN_ENTRYPOINT_DIR = "scripts/langchain" VERIFY_TO_NEW_PR = WORKFLOWS_DIR / "agents-verify-to-new-pr.yml" -KNOWN_LLM_CLIENT_WORKFLOWS = (VERIFIER, VERIFY_TO_ISSUE, VERIFY_TO_NEW_PR) +CONSUMER_WORKFLOWS_DIR = Path("templates/consumer-repo/.github/workflows") +CONSUMER_VERIFY_TO_NEW_PR = CONSUMER_WORKFLOWS_DIR / "agents-verify-to-new-pr.yml" +CONSUMER_ISSUE_OPTIMIZER = CONSUMER_WORKFLOWS_DIR / "agents-issue-optimizer.yml" +CONSUMER_PR_EVENT_HUB = CONSUMER_WORKFLOWS_DIR / "agents-80-pr-event-hub.yml" +LLM_CLIENT_WORKFLOW_DIRS = (WORKFLOWS_DIR, CONSUMER_WORKFLOWS_DIR) +KNOWN_LLM_CLIENT_WORKFLOWS = ( + VERIFIER, + VERIFY_TO_ISSUE, + VERIFY_TO_NEW_PR, + CONSUMER_VERIFY_TO_NEW_PR, + CONSUMER_ISSUE_OPTIMIZER, + CONSUMER_PR_EVENT_HUB, +) def _load_text(path: Path) -> str: @@ -114,10 +126,17 @@ def _discover_llm_client_workflows() -> list[Path]: """Workflows that vendor `tools` and then run the LangChain client from it. Discovery rather than an explicit list: any future workflow that vendors the - client inherits the config-vendoring requirement automatically. + client inherits the config-vendoring requirement automatically. The consumer + template directory is scanned too, because those workflows are synced into + the consumer repos and vendor the same client from the same remote tree. """ + candidates: list[Path] = [] + for directory in LLM_CLIENT_WORKFLOW_DIRS: + candidates.extend(sorted(directory.glob("*.yml"))) + candidates.extend(sorted(directory.glob("*.yaml"))) + matches = [] - for path in sorted(WORKFLOWS_DIR.glob("*.yml")) + sorted(WORKFLOWS_DIR.glob("*.yaml")): + for path in candidates: text = path.read_text(encoding="utf-8") if LANGCHAIN_ENTRYPOINT_DIR not in text: continue @@ -285,7 +304,7 @@ def test_reusable_agents_verifier_pip_cache_is_configured() -> None: def test_llm_client_workflow_discovery_covers_the_known_verifier_surfaces() -> None: discovered = set(_discover_llm_client_workflows()) - missing = [path.name for path in KNOWN_LLM_CLIENT_WORKFLOWS if path not in discovered] + missing = [path.as_posix() for path in KNOWN_LLM_CLIENT_WORKFLOWS if path not in discovered] assert not missing, ( f"Discovery no longer sees {missing}; the config-vendoring guard below would " "silently stop covering them." @@ -293,12 +312,12 @@ def test_llm_client_workflow_discovery_covers_the_known_verifier_surfaces() -> N @pytest.mark.parametrize( - "workflow_path", _discover_llm_client_workflows(), ids=lambda path: path.name + "workflow_path", _discover_llm_client_workflows(), ids=lambda path: path.as_posix() ) def test_llm_workflows_vendor_the_model_registry_config(workflow_path: Path) -> None: workflow = _load_workflow(workflow_path) checkouts = _workflows_library_checkout_steps(workflow) - assert checkouts, f"{workflow_path.name} must sparse-checkout stranske/Workflows" + assert checkouts, f"{workflow_path.as_posix()} must sparse-checkout stranske/Workflows" vendors_tools = False for step in checkouts: @@ -308,13 +327,13 @@ def test_llm_workflows_vendor_the_model_registry_config(workflow_path: Path) -> vendors_tools = True missing = [entry for entry in ("config",) if entry not in paths] assert not missing, ( - f"{workflow_path.name} vendors `tools` but not {missing}; " + f"{workflow_path.as_posix()} vendors `tools` but not {missing}; " f"{LLM_REGISTRY_MODULE} resolves {', '.join(str(p) for p in LLM_CONFIG_PATHS)} " "relative to the vendored tree, so every judge slot resolves to no model and " 'compare mode reports "available families: none".' ) - assert vendors_tools, f"{workflow_path.name} must vendor `tools` for the LLM client" + assert vendors_tools, f"{workflow_path.as_posix()} must vendor `tools` for the LLM client" def test_bundled_llm_config_resolves_two_cross_family_judges(