.github/scripts/test_check_app_terraform_isolation.py
Adam Moussa 7dc8db2af0
fix(ci): classify pull request diffs against the merge parent (#165)
A stale event base SHA was unioning commits already on main into the
isolation diff, so a clean PR failed as a mixed app and Terraform change.
2026-10-06 18:52:41 +00:00

275 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""Tests for check_app_terraform_isolation."""
from __future__ import annotations
import os
import re
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ACTION = ROOT / ".github" / "actions" / "app-terraform-isolation"
WORKFLOW = ROOT / ".github" / "workflows" / "ci-terraform.yaml"
sys.path.insert(0, str(ACTION))
from check_app_terraform_isolation import ( # noqa: E402
first_isolation_violation,
isolation_violation,
)
APP_PATHS = "src/\npackage.json\npackage-lock.json\n"
class IsolationTests(unittest.TestCase):
def test_terraform_only(self) -> None:
self.assertIsNone(
isolation_violation(
["terraform/lambda.tf", "terraform/README.md"],
APP_PATHS,
)
)
def test_app_only(self) -> None:
self.assertIsNone(
isolation_violation(
["src/processPaymentCsv.js", "package.json", "package-lock.json"],
APP_PATHS,
)
)
def test_docs_and_workflows_with_terraform(self) -> None:
self.assertIsNone(
isolation_violation(
[
"terraform/lambda.tf",
".github/workflows/deploy.yaml",
"SETUP.md",
"scripts/check_app_terraform_isolation.py",
],
APP_PATHS,
)
)
def test_mixed_app_and_terraform_fails(self) -> None:
violation = isolation_violation(
["terraform/lambda.tf", "src/processPaymentCsv.js", "package.json"],
APP_PATHS,
)
self.assertIsNotNone(violation)
terraform_files, app_files = violation or ([], [])
self.assertEqual(terraform_files, ["terraform/lambda.tf"])
self.assertEqual(app_files, ["package.json", "src/processPaymentCsv.js"])
def test_empty_app_paths_skips(self) -> None:
self.assertIsNone(
isolation_violation(
["terraform/lambda.tf", "src/processPaymentCsv.js"],
"",
)
)
def test_separate_commits_pass_when_classified_alone(self) -> None:
self.assertIsNone(
first_isolation_violation(
[
["terraform/lambda.tf"],
["src/processPaymentCsv.js"],
],
APP_PATHS,
)
)
def test_union_of_separate_commits_fails(self) -> None:
violation = isolation_violation(
["terraform/lambda.tf", "src/processPaymentCsv.js"],
APP_PATHS,
)
self.assertIsNotNone(violation)
def test_non_default_terraform_dir_mixed_fails(self) -> None:
violation = isolation_violation(
["infra/main.tf", "src/app.js"],
"src/\n",
terraform_dir="infra",
)
self.assertIsNotNone(violation)
terraform_files, app_files = violation or ([], [])
self.assertEqual(terraform_files, ["infra/main.tf"])
self.assertEqual(app_files, ["src/app.js"])
def test_default_dir_leaves_other_prefixes_neutral(self) -> None:
self.assertIsNone(
isolation_violation(["infra/main.tf", "src/app.js"], "src/\n")
)
def test_terraform_prefix_does_not_match_a_longer_directory(self) -> None:
self.assertIsNone(
isolation_violation(
["infrastructure/main.tf", "src/app.js"],
"src/\n",
terraform_dir="infra",
)
)
def test_terraform_dir_trailing_slash(self) -> None:
violation = isolation_violation(
["infra/main.tf", "src/app.js"],
"src/\n",
terraform_dir="infra/",
)
self.assertIsNotNone(violation)
def test_blank_terraform_dir_defaults_to_terraform(self) -> None:
violation = isolation_violation(
["terraform/lambda.tf", "src/app.js"],
"src/\n",
terraform_dir=" ",
)
self.assertIsNotNone(violation)
class WorkflowDiffTests(unittest.TestCase):
def test_classified_diffs_include_deletions(self) -> None:
self.assertEqual(_workflow_diff_filters(), ["ACMRD", "ACMRD", "ACMRD"])
def test_pull_request_diffs_merge_commit_against_first_parent(self) -> None:
action = (ACTION / "action.yml").read_text()
self.assertIn("git rev-parse --verify --quiet HEAD^2", action)
self.assertIn(
"git diff --name-only --diff-filter=ACMRD HEAD^1 HEAD | classify",
action,
)
self.assertIn('git merge-base "${PR_BASE_SHA}" HEAD', action)
def test_merge_commit_excludes_commits_already_on_the_base(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
repo = Path(tmp)
_commit_base(repo)
old_base = _git(repo, "rev-parse", "HEAD").strip()
(repo / "terraform" / "versions.tf").write_text("aws\n")
_git(repo, "add", "-A")
_git(repo, "commit", "-m", "terraform provider")
(repo / "package.json").write_text("{}\n")
_git(repo, "add", "-A")
_git(repo, "commit", "-m", "npm bump")
_git(repo, "checkout", "-b", "pr", old_base)
workflows = repo / ".github" / "workflows"
workflows.mkdir(parents=True)
(workflows / "ci.yaml").write_text("name: CI\n")
_git(repo, "add", "-A")
_git(repo, "commit", "-m", "pin bump")
_git(repo, "checkout", "main")
_git(repo, "merge", "--no-ff", "pr", "-m", "Merge pr into main")
pr_only = _changed_paths(repo, "HEAD^1", "HEAD", "ACMRD")
stale = _changed_paths(repo, old_base, "HEAD", "ACMRD")
self.assertEqual(pr_only, [".github/workflows/ci.yaml"])
self.assertIn("package.json", stale)
self.assertIn("terraform/versions.tf", stale)
self.assertIsNone(isolation_violation(pr_only, APP_PATHS))
self.assertIsNotNone(isolation_violation(stale, APP_PATHS))
def test_workflow_passes_working_directory(self) -> None:
self.assertIn(
"terraform-dir: ${{ inputs.working-directory }}",
WORKFLOW.read_text(),
)
def test_checker_is_this_repos_action_at_the_workflow_commit(self) -> None:
text = WORKFLOW.read_text()
self.assertIn("uses: $/.github/actions/app-terraform-isolation", text)
self.assertNotIn("github.workflow_sha", text)
self.assertNotIn("repository: Sea-Haven-Industries/.github", text)
def test_deleted_app_file_is_classified(self) -> None:
diff_filter = _workflow_diff_filters()[0]
with tempfile.TemporaryDirectory() as tmp:
repo = Path(tmp)
base = _commit_base(repo)
(repo / "terraform" / "lambda.tf").write_text("changed\n")
(repo / "src" / "processPaymentCsv.js").unlink()
_git(repo, "add", "-A")
_git(repo, "commit", "-m", "decommission handler")
omitted = _changed_paths(repo, base, "HEAD", "ACMR")
included = _changed_paths(repo, base, "HEAD", diff_filter)
self.assertNotIn("src/processPaymentCsv.js", omitted)
self.assertIn("src/processPaymentCsv.js", included)
self.assertIn("terraform/lambda.tf", included)
self.assertIsNotNone(isolation_violation(included, APP_PATHS))
def test_deleted_terraform_file_is_classified(self) -> None:
diff_filter = _workflow_diff_filters()[0]
with tempfile.TemporaryDirectory() as tmp:
repo = Path(tmp)
base = _commit_base(repo)
(repo / "terraform" / "lambda.tf").unlink()
(repo / "src" / "processPaymentCsv.js").write_text("changed\n")
_git(repo, "add", "-A")
_git(repo, "commit", "-m", "remove lambda")
omitted = _changed_paths(repo, base, "HEAD", "ACMR")
included = _changed_paths(repo, base, "HEAD", diff_filter)
self.assertNotIn("terraform/lambda.tf", omitted)
violation = isolation_violation(included, APP_PATHS)
self.assertIsNotNone(violation)
terraform_files, app_files = violation or ([], [])
self.assertEqual(terraform_files, ["terraform/lambda.tf"])
self.assertEqual(app_files, ["src/processPaymentCsv.js"])
def _workflow_diff_filters() -> list[str]:
action = (ACTION / "action.yml").read_text()
return re.findall(r"--diff-filter=([A-Z]+)", action)
def _git(repo: Path, *args: str) -> str:
env = os.environ.copy()
env["GIT_CONFIG_GLOBAL"] = os.devnull
env["GIT_CONFIG_NOSYSTEM"] = "1"
completed = subprocess.run(
[
"git",
"-c",
"commit.gpgsign=false",
"-c",
"user.name=test",
"-c",
"user.email=test@example.com",
*args,
],
cwd=repo,
check=True,
capture_output=True,
text=True,
env=env,
)
return completed.stdout
def _commit_base(repo: Path) -> str:
_git(repo, "init", "-b", "main")
(repo / "terraform").mkdir()
(repo / "src").mkdir()
(repo / "terraform" / "lambda.tf").write_text("resource\n")
(repo / "src" / "processPaymentCsv.js").write_text("export {}\n")
_git(repo, "add", ".")
_git(repo, "commit", "-m", "base")
return _git(repo, "rev-parse", "HEAD").strip()
def _changed_paths(repo: Path, base: str, head: str, diff_filter: str) -> list[str]:
output = _git(
repo,
"diff",
"--name-only",
f"--diff-filter={diff_filter}",
base,
head,
)
return [line for line in output.splitlines() if line]
if __name__ == "__main__":
unittest.main()