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: 7 additions & 7 deletions .github/sync-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ version: 1
workflows:
# Core CI/CD
- source: .github/workflows/pr-00-gate.yml
description: "Gate workflow - runs tests, lint, type checking before merge. Bootstrap note: the expanded template Gate is not yet fresh-consumer deployable; see issue #2158 before seeding it into new repos."
sync_mode: create_only # Don't overwrite existing consumer Gate files; fresh repos can still receive this file, so follow #2158 before first seeding.
description: "Gate workflow - runs tests, lint, type checking before merge. It remains create-only and not yet fresh-consumer deployable under the documented workaround (resolved: #2158) because consumers own their coverage and runtime policy."
sync_mode: create_only # Do not overwrite consumer-specific Gate coverage, runtime, or policy.
overwrite_repos:
- stranske/Template
skip_repos:
Expand Down Expand Up @@ -63,7 +63,7 @@ workflows:
description: "Gate followups hub - consolidates keepalive and autofix followups"

- source: .github/workflows/agents-keepalive-sweep.yml
description: "Keepalive sweep (#2267) - periodic level-based resync; dispatches the loop for open agent PRs so silent zero-commit stalls resurface"
description: "Keepalive sweep - periodic level-based resync; dispatches the loop for open agent PRs so silent zero-commit stalls resurface"

# Agent system - execution
- source: .github/workflows/agents-keepalive-loop-reporter.yml
Expand Down Expand Up @@ -558,7 +558,7 @@ scripts:

# Agent helper scripts and copy-delivered LangChain workflow support
#
# delivery: channel annotation (issue #2347)
# delivery: channel annotation (resolved: issue #2347)
# copy = physically copy-synced into the consumer tree by maint-68 and
# read from the consumer's own working directory at runtime.
# runtime = NOT copy-synced; the consuming consumer-template workflow does a
Expand Down Expand Up @@ -658,7 +658,7 @@ scripts:
description: "PR verifier - validates PR changes against acceptance criteria"
delivery: copy

# NOTE (issue #2347): scripts/langchain/followup_issue_generator.py was MOVED
# NOTE (resolved: issue #2347): scripts/langchain/followup_issue_generator.py was MOVED
# out of this copy-sync set into the `runtime_fetched:` section below. It is
# delivered to consumers ONLY via runtime sparse-checkout (see that section).

Expand Down Expand Up @@ -886,7 +886,7 @@ docs:

- source: docs/SETUP_CHECKLIST.md
target: docs/SETUP_CHECKLIST.md
description: "Consumer repo setup checklist. Includes the default_workflow_permissions=write step in section 3.3.1 that prevents a Gate startup_failure on fresh consumers (see #2157). Synced so it no longer drifts across the fleet."
description: "Consumer repo setup checklist. Includes the default_workflow_permissions=write step in section 3.3.1 that prevents a Gate startup_failure on fresh consumers (resolved: #2157). Synced so it no longer drifts across the fleet."

# ---- Shared design system (fleet UX consistency) ----
# CSS tokens/components + a Streamlit adapter kit + the presentation-patterns
Expand Down Expand Up @@ -933,7 +933,7 @@ git_config:
description: "CodeRabbit auto-review config - skips maintenance-bot PRs (renovate/dependabot/github-actions/keepalive) and generated sync/draft PRs, while keeping substantial agent-authored PR reviews enabled. Advisory/non-gating per fleet rule."


# Workflows-owned runtime-fetched scripts (issue #2347)
# Workflows-owned runtime-fetched scripts (resolved: issue #2347)
#
# These files are intentionally NOT copy-synced into consumer repos. The
# consumer-template workflows that use them perform a runtime sparse-checkout of
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/pr-00-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,12 @@ jobs:
--head HEAD
env:
PR_BODY: ${{ github.event.pull_request.body || '' }}
- name: Verify open sync-manifest issue citations
if: ${{ hashFiles('tests/scripts/test_sync_manifest_docs.py') != '' }}
env:
GH_TOKEN: ${{ github.token }}
GITHUB_TOKEN: ${{ github.token }}
run: python -m pytest tests/scripts/test_sync_manifest_docs.py -q
- name: Apply runtime acceptance-criteria label
if: ${{ steps.deliberate_break.outputs.has_marker == 'true' }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
Expand Down
82 changes: 61 additions & 21 deletions config/template-drift-allowlist.txt

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion scripts/check_template_drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,11 @@ def read_allowlist(path: Path) -> TemplateDriftAllowlist:
template_path=parser.get(section, "template"),
main_sha256=parser.get(section, "main_sha256"),
template_sha256=parser.get(section, "template_sha256"),
reason=parser.get(section, "reason", fallback=""),
reason=parser.get(
section,
"divergence",
fallback=parser.get(section, "reason", fallback=""),
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
)
return TemplateDriftAllowlist(tuple(entries))
Expand Down
19 changes: 10 additions & 9 deletions scripts/cleanup_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@
import os
import sys
from collections.abc import Iterable
from pathlib import Path
from typing import NamedTuple

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

from scripts.list_registered_consumer_repos import extract_repos # noqa: E402

# Try to import github, fall back to instructions
try:
from github import Github
Expand Down Expand Up @@ -225,15 +232,9 @@ def normalize_label_name(label_name: str) -> str:
NORMALIZED_BLOAT_LABELS = {normalize_label_name(label) for label in BLOAT_LABELS}

# Consumer repos to audit
CONSUMER_REPOS = [
"stranske/Manager-Database",
"stranske/Template",
"stranske/trip-planner",
"stranske/Travel-Plan-Permission",
"stranske/Portable-Alpha-Extension-Model",
"stranske/Trend_Model_Project",
"stranske/Collab-Admin",
]
CONSUMER_REPOS = extract_repos(
Path(__file__).resolve().parents[1] / ".github/workflows/maint-68-sync-consumer-repos.yml"
)


def get_github_client() -> Github:
Expand Down
27 changes: 11 additions & 16 deletions scripts/langsmith_fleet.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

from jsonschema import Draft202012Validator

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))

from scripts.list_registered_consumer_repos import extract_repos # noqa: E402

SCHEMA_VERSION = "langsmith-fleet/v1"
SCHEMA_PATH = Path("docs/contracts/schemas/langsmith-fleet-v1.schema.json")
REGISTRY_SCHEMA_VERSION = "langsmith-fleet-registry/v1"
Expand All @@ -29,22 +35,11 @@
"resume_condition",
"review_by",
)
MANAGED_CONSUMER_REPOS = {
"stranske/Travel-Plan-Permission",
"stranske/Template",
"stranske/Counter_Risk",
"stranske/Pension-Data",
"stranske/Inv-Man-Intake",
"stranske/Ready",
"stranske/trip-planner",
"stranske/Manager-Database",
"stranske/Portable-Alpha-Extension-Model",
"stranske/Trend_Model_Project",
"stranske/Collab-Admin",
"stranske/learning-management-system",
"stranske/Fine-Art-Archive",
"stranske/Orchestrator",
}
MANAGED_CONSUMER_REPOS = set(
extract_repos(
Path(__file__).resolve().parents[1] / ".github/workflows/maint-68-sync-consumer-repos.yml"
)
)
REQUIRED_ACTIVE_REPO_ISSUES = {
"stranske/trip-planner": 1208,
"stranske/Pension-Data": 445,
Expand Down
6 changes: 1 addition & 5 deletions scripts/validate_template_completeness.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,10 @@
# Maintenance workflows specific to Workflows repo
"maint-52-sync-dev-versions.yml",
"maint-68-sync-consumer-repos.yml",
"maint-post-ci.yml",
# Health checks specific to Workflows repo
"health-68-consumer-sync-drift.yml",
"health-70-validate-sync-manifest.yml",
"health-71-sync-health-check.yml",
"health-72-template-lint.yml",
"health-75-api-rate-diagnostic.yml",
"health-76-codex-cli-freshness.yml",
# Debug/testing workflows
Expand All @@ -41,12 +39,11 @@
"agents-keepalive-branch-sync.yml",
"agents-keepalive-dispatch-handler.yml",
# Workflows repo specific features
"agents-weekly-metrics.yml",
"agents-moderate-connector.yml",
# Older versions superseded in consumer repos
"agents-63-issue-intake.yml", # consumers have agents-issue-intake.yml
"agents-64-verify-agent-assignment.yml", # verification is different
"agents-70-orchestrator.yml", # consumers have agents-orchestrator.yml
"agents-70-orchestrator.yml", # retired from consumers; agents-80/81 replace it
"agents-pr-meta-v4.yml", # consumers have agents-80-pr-event-hub.yml
# First-party workflows retained in Workflows; consumer templates use
# agents-80-pr-event-hub.yml and agents-81-gate-followups.yml instead.
Expand All @@ -59,7 +56,6 @@
"reusable-codex-run.yml",
"reusable-10-ci-python.yml",
"reusable-18-autofix.yml",
"reusable-pr-context.yml",
}


Expand Down
48 changes: 48 additions & 0 deletions tests/scripts/test_fleet_list_single_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

import ast
from pathlib import Path

from scripts import cleanup_labels, langsmith_fleet
from scripts.list_registered_consumer_repos import extract_repos

ROOT = Path(__file__).resolve().parents[2]
MANIFEST = ROOT / ".github/workflows/maint-68-sync-consumer-repos.yml"


def _assignment_uses_extract_repos(path: Path, target_name: str) -> bool:
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in tree.body:
value: ast.expr | None = None
if (
isinstance(node, ast.Assign)
and any(
isinstance(target, ast.Name) and target.id == target_name for target in node.targets
)
) or (
isinstance(node, ast.AnnAssign)
and isinstance(node.target, ast.Name)
and node.target.id == target_name
):
value = node.value
if value is not None and any(
isinstance(child, ast.Call)
and isinstance(child.func, ast.Name)
and child.func.id == "extract_repos"
for child in ast.walk(value)
):
return True
return False


def test_no_second_consumer_repo_literal() -> None:
registered = extract_repos(MANIFEST)

assert MANIFEST.name == "maint-68-sync-consumer-repos.yml"
assert len(registered) >= 10
assert registered == cleanup_labels.CONSUMER_REPOS
assert set(registered) == langsmith_fleet.MANAGED_CONSUMER_REPOS
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert _assignment_uses_extract_repos(ROOT / "scripts/cleanup_labels.py", "CONSUMER_REPOS")
assert _assignment_uses_extract_repos(
ROOT / "scripts/langsmith_fleet.py", "MANAGED_CONSUMER_REPOS"
)
84 changes: 84 additions & 0 deletions tests/scripts/test_sync_manifest_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from __future__ import annotations

import os
import re
import subprocess
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[2]
MANIFEST = ROOT / ".github" / "sync-manifest.yml"
ISSUE_REFERENCE = re.compile(r"#(?P<number>\d+)")
ISSUE_STATE_PAIR = re.compile(
r"\b(?P<state>open|resolved):\s*(?:issue\s*)?#(?P<number>\d+)",
re.IGNORECASE,
)


def _issue_state_pairs(line: str) -> list[tuple[str, str]]:
return [
(match.group("state").lower(), match.group("number"))
for match in ISSUE_STATE_PAIR.finditer(line)
]


def _unpaired_issue_references(line: str) -> list[str]:
residue = ISSUE_STATE_PAIR.sub("", line)
return [match.group(0) for match in ISSUE_REFERENCE.finditer(residue)]


def test_manifest_issue_citations_are_explicitly_stateful() -> None:
"""Manifest citations name whether the referenced issue is open or resolved."""
offenders = [
line.strip()
for line in MANIFEST.read_text(encoding="utf-8").splitlines()
if _unpaired_issue_references(line)
]
assert offenders == []


def test_issue_state_parser_associates_each_citation() -> None:
line = "open: #2158; resolved: issue #2157"

assert _issue_state_pairs(line) == [("open", "2158"), ("resolved", "2157")]
assert _unpaired_issue_references(line) == []
assert _unpaired_issue_references("open: #2158; also #2157") == ["#2157"]


def test_manifest_issue_references_are_open() -> None:
"""Live guard for manifest citations explicitly marked as open."""
if not (os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN")):
pytest.skip("GH_TOKEN or GITHUB_TOKEN is required to verify manifest issue references")
Comment thread
stranske marked this conversation as resolved.

for line in MANIFEST.read_text(encoding="utf-8").splitlines():
for state, issue_number in _issue_state_pairs(line):
if state != "open":
continue
try:
result = subprocess.run(
[
"gh",
"issue",
"view",
issue_number,
"--repo",
"stranske/Workflows",
"--json",
"state",
"--jq",
".state",
],
check=True,
capture_output=True,
text=True,
timeout=15,
)
except subprocess.TimeoutExpired:
pytest.fail(
f"timed out checking manifest issue #{issue_number}: {line.strip()}",
pytrace=False,
)
assert (
result.stdout.strip() == "OPEN"
), f"manifest references closed issue #{issue_number}: {line.strip()}"
52 changes: 52 additions & 0 deletions tests/scripts/test_template_drift_allowlist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

import configparser
from pathlib import Path

from scripts.check_template_drift import read_allowlist

ROOT = Path(__file__).resolve().parents[2]
ALLOWLIST = ROOT / "config/template-drift-allowlist.txt"


def test_every_pair_states_its_divergence() -> None:
parser = configparser.ConfigParser(interpolation=None)
parser.read(ALLOWLIST, encoding="utf-8")

assert ALLOWLIST.name == "template-drift-allowlist.txt"
assert len(parser.sections()) >= 1

for section in parser.sections():
divergence = parser.get(section, "divergence", fallback="").strip()
reviewed = parser.get(section, "divergence_reviewed", fallback="").strip()
refreshed = parser.get(section, "fingerprint_refreshed", fallback="").strip()
assert divergence and "Existing reviewed baseline drift" not in divergence
assert reviewed
assert refreshed


def test_read_allowlist_prefers_divergence_and_supports_legacy_reason(tmp_path: Path) -> None:
allowlist_path = tmp_path / "allowlist.txt"
allowlist_path.write_text(
"""
[pair.current]
main = root.yml
template = template.yml
main_sha256 = main-current
template_sha256 = template-current
divergence = current rationale
reason = superseded legacy rationale

[pair.legacy]
main = legacy-root.yml
template = legacy-template.yml
main_sha256 = main-legacy
template_sha256 = template-legacy
reason = legacy rationale
""".strip() + "\n",
encoding="utf-8",
)

entries = read_allowlist(allowlist_path).entries

assert [entry.reason for entry in entries] == ["current rationale", "legacy rationale"]
11 changes: 11 additions & 0 deletions tests/scripts/test_validate_template_completeness.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ def test_get_workflows_returns_top_level_yml_files_only(tmp_path: Path) -> None:
assert validator.get_workflows(tmp_path / "missing") == set()


def test_workflows_only_is_live_and_consistent() -> None:
root = Path(__file__).resolve().parents[2]
root_workflows = validator.get_workflows(root / ".github/workflows")
template_workflows = validator.get_workflows(root / "templates/consumer-repo/.github/workflows")
manifest_workflows = validator.get_manifest_workflows(root / ".github/sync-manifest.yml")

assert root_workflows >= validator.WORKFLOWS_ONLY
assert not validator.WORKFLOWS_ONLY & template_workflows
assert not validator.WORKFLOWS_ONLY & manifest_workflows


def test_get_manifest_workflows_extracts_consumer_workflow_targets(tmp_path: Path) -> None:
manifest_path = tmp_path / "sync-manifest.yml"
manifest_path.write_text(
Expand Down
Loading