ci: improve frontend verification and review timeouts (#3634)
This commit is contained in:
@@ -31,6 +31,16 @@ def workflow_step(workflow: str, name: str) -> str:
|
||||
return remainder.partition("\n - name:")[0]
|
||||
|
||||
|
||||
def job_block(workflow: str, job: str, next_job: str | None) -> str:
|
||||
marker = f" {job}:\n"
|
||||
_, separator, remainder = workflow.partition(marker)
|
||||
if not separator:
|
||||
raise AssertionError(f"Workflow has no {job} job")
|
||||
if next_job is None:
|
||||
return remainder
|
||||
return remainder.partition(f"\n {next_job}:\n")[0]
|
||||
|
||||
|
||||
class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
|
||||
def test_review_workflow_ignores_pr_updates(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
@@ -67,6 +77,27 @@ class ClaudeCodeReviewWorkflowContractTest(unittest.TestCase):
|
||||
workflow,
|
||||
)
|
||||
|
||||
def test_claude_execution_jobs_have_a_thirty_minute_budget(self) -> None:
|
||||
review_workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
for job, next_job in (
|
||||
("claude-review-same-repo", "label-allowlisted-fork"),
|
||||
("claude-review-fork", None),
|
||||
):
|
||||
block = job_block(review_workflow, job, next_job)
|
||||
self.assertRegex(
|
||||
block,
|
||||
r"runs-on: ubuntu-latest\n timeout-minutes: 30\n",
|
||||
f"{job} must bound Claude execution at 30 minutes",
|
||||
)
|
||||
|
||||
mention_workflow = MENTION_WORKFLOW.read_text(encoding="utf-8")
|
||||
block = job_block(mention_workflow, "claude", None)
|
||||
self.assertRegex(
|
||||
block,
|
||||
r"runs-on: ubuntu-latest\n timeout-minutes: 30\n",
|
||||
"the shared interactive Claude job must bound execution at 30 minutes",
|
||||
)
|
||||
|
||||
def test_manual_pr_review_does_not_checkout_untrusted_content(self) -> None:
|
||||
workflow = MENTION_WORKFLOW.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ class ExternalRunnerWorkflowContractTest(unittest.TestCase):
|
||||
self.assertIn("runs-on: ubuntu-latest" if job == "postgres-boot" else "runs-on: windows-latest", protected)
|
||||
self.assertNotIn("runner_plan", protected)
|
||||
|
||||
def test_frontend_uses_planner_for_test_and_gate(self) -> None:
|
||||
def test_frontend_uses_planner_for_unsharded_tests_and_gate(self) -> None:
|
||||
workflow = self.assert_planner(
|
||||
"frontend-tests.yml",
|
||||
"frontend",
|
||||
@@ -121,6 +121,9 @@ class ExternalRunnerWorkflowContractTest(unittest.TestCase):
|
||||
"runs-on: ${{ fromJSON(needs.runner_plan.outputs.plan).frontend_runner }}",
|
||||
job_block(workflow, "frontend", "frontend-gate"),
|
||||
)
|
||||
frontend = job_block(workflow, "frontend", "frontend-gate")
|
||||
self.assertNotIn("matrix:", frontend)
|
||||
self.assertNotIn("frontend_tests", workflow)
|
||||
self.assertIn(
|
||||
"runs-on: ${{ fromJSON(needs.runner_plan.outputs.plan).frontend_gate_runner }}",
|
||||
job_block(workflow, "frontend-gate", None),
|
||||
|
||||
@@ -40,6 +40,22 @@ def run_tests_step(workflow: str) -> str:
|
||||
return remainder.partition(NEXT_STEP_MARKER)[0]
|
||||
|
||||
|
||||
def frontend_job(workflow: str) -> str:
|
||||
"""Return the frontend job body, up to the required gate."""
|
||||
_, separator, remainder = workflow.partition(" frontend:\n")
|
||||
if not separator:
|
||||
raise AssertionError("frontend-tests.yml has no frontend job")
|
||||
return remainder.partition("\n frontend-gate:")[0]
|
||||
|
||||
|
||||
def frontend_gate_job(workflow: str) -> str:
|
||||
"""Return the required frontend gate body."""
|
||||
_, separator, remainder = workflow.partition(" frontend-gate:\n")
|
||||
if not separator:
|
||||
raise AssertionError("frontend-tests.yml has no frontend-gate job")
|
||||
return remainder
|
||||
|
||||
|
||||
def trigger_block(workflow: str, trigger: str) -> str:
|
||||
"""Return `trigger`'s block under `on:`, up to the next top-level key."""
|
||||
block = re.search(rf"(?m)^ {trigger}:\n(?:^ {{4}}.*\n|^\n)*", workflow)
|
||||
@@ -49,6 +65,78 @@ def trigger_block(workflow: str, trigger: str) -> str:
|
||||
|
||||
|
||||
class FrontendTestsWorkflowContractTest(unittest.TestCase):
|
||||
def test_cache_resolves_the_container_pnpm_store_before_restore(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
job = frontend_job(workflow)
|
||||
resolve_index = job.find("- name: Resolve pnpm store path")
|
||||
cache_index = job.find("- name: Cache pnpm store")
|
||||
install_index = job.find("- name: Install dependencies")
|
||||
|
||||
self.assertGreaterEqual(resolve_index, 0, "store resolution step is missing")
|
||||
self.assertGreaterEqual(cache_index, 0, "cache step is missing")
|
||||
self.assertGreaterEqual(install_index, 0, "dependency installation step is missing")
|
||||
self.assertLess(resolve_index, cache_index)
|
||||
self.assertLess(cache_index, install_index)
|
||||
resolve_step = job[resolve_index:cache_index]
|
||||
cache_step = job[cache_index:install_index]
|
||||
|
||||
self.assertIn("id: pnpm-store", resolve_step)
|
||||
self.assertIn("pnpm store path --silent", resolve_step)
|
||||
self.assertIn('pnpm --version', resolve_step)
|
||||
self.assertIn('mkdir -p "${STORE_PATH}"', resolve_step)
|
||||
self.assertIn('printf \'path=%s\\n\' "${STORE_PATH}"', resolve_step)
|
||||
self.assertIn("path: ${{ steps.pnpm-store.outputs.path }}", cache_step)
|
||||
self.assertIn("runner.os", cache_step)
|
||||
self.assertIn("runner.arch", cache_step)
|
||||
self.assertIn("steps.pnpm-store.outputs.version", cache_step)
|
||||
self.assertIn("hashFiles('apps/pnpm-lock.yaml')", cache_step)
|
||||
self.assertIn(
|
||||
"restore-keys: pnpm-${{ runner.os }}-${{ runner.arch }}-${{ steps.pnpm-store.outputs.version }}-",
|
||||
cache_step,
|
||||
)
|
||||
self.assertIn("continue-on-error: true", cache_step)
|
||||
|
||||
def test_cache_failure_does_not_replace_frozen_installation(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
job = frontend_job(workflow)
|
||||
cache_index = job.index("- name: Cache pnpm store")
|
||||
install_index = job.index("- name: Install dependencies")
|
||||
install_step = job[install_index:].partition(NEXT_STEP_MARKER)[0]
|
||||
|
||||
self.assertLess(cache_index, install_index)
|
||||
self.assertIn("pnpm install --frozen-lockfile", install_step)
|
||||
|
||||
def test_frontend_job_keeps_unsharded_unit_tests_and_static_checks(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
job = frontend_job(workflow)
|
||||
|
||||
self.assertIn("- name: Run tests\n", job)
|
||||
self.assertIn("NODE_ENV: production", job)
|
||||
self.assertIn("pnpm --filter @kandev/web test", job)
|
||||
self.assertIn("- name: Run lint\n", job)
|
||||
self.assertIn("- name: Run typecheck\n", job)
|
||||
self.assertIn("- name: Build\n", job)
|
||||
self.assertNotIn("FRONTEND_TEST_SHARD", job)
|
||||
self.assertNotIn("matrix:", job)
|
||||
|
||||
def test_frontend_tests_remain_outside_the_planner_until_adoption_evidence(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
planner = workflow.partition(" changes:\n")[0]
|
||||
|
||||
self.assertNotIn('"name":"frontend_tests"', planner)
|
||||
self.assertNotIn(" frontend_tests:\n", workflow)
|
||||
|
||||
def test_required_gate_requires_every_frontend_verification_result(self) -> None:
|
||||
workflow = WORKFLOW.read_text(encoding="utf-8")
|
||||
gate = frontend_gate_job(workflow)
|
||||
|
||||
self.assertIn(
|
||||
"needs: [changes, frontend, runner_plan]",
|
||||
gate,
|
||||
)
|
||||
self.assertNotIn("FRONTEND_TESTS_RESULT:", gate)
|
||||
self.assertNotIn("Frontend test matrix finished with result:", gate)
|
||||
|
||||
def test_test_step_reproduces_the_production_environment(self) -> None:
|
||||
step = run_tests_step(WORKFLOW.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@@ -23,6 +23,11 @@ E2E_FAMILIES = [
|
||||
]
|
||||
FRONTEND_FAMILIES = [
|
||||
{"name": "frontend", "tier": "standard"},
|
||||
{
|
||||
"name": "frontend_tests",
|
||||
"tier": "standard",
|
||||
"instances": [{"shard": 1, "total": 2}, {"shard": 2, "total": 2}],
|
||||
},
|
||||
{"name": "frontend_gate", "tier": "light"},
|
||||
]
|
||||
BACKEND_MATRIX_FAMILY = [
|
||||
@@ -62,6 +67,12 @@ class RunnerPlanTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(plan.plan["frontend_runner"], "ubuntu-latest")
|
||||
self.assertEqual(plan.plan["frontend_gate_runner"], "ubuntu-latest")
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["runner"] == "ubuntu-latest"
|
||||
for item in plan.plan["frontend_tests_matrix"]["include"]
|
||||
)
|
||||
)
|
||||
|
||||
def test_matrix_percentage_uses_floor_and_is_stable(self) -> None:
|
||||
first = runner_plan.build_plan(
|
||||
@@ -217,6 +228,12 @@ class RunnerPlanTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(plan.plan["frontend_runner"], "standard-runner")
|
||||
self.assertEqual(plan.plan["frontend_gate_runner"], "light-runner")
|
||||
self.assertTrue(
|
||||
all(
|
||||
item["runner"] == "standard-runner"
|
||||
for item in plan.plan["frontend_tests_matrix"]["include"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user