mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-10-03 21:23:15 +00:00
* ci(terraform): fail mixed app and Terraform changes * fix(ci): count deletions and honor the Terraform working directory Deleted paths were excluded from the isolation diff, so a mixed change could pass. The checker now treats working-directory as the Terraform prefix. * fix(ci): load the isolation checker from this workflow's commit The second checkout used the caller's SHA and the caller's token, so a private clone of this repo could not resolve the script. The checker is now a composite action referenced with $/.
239 lines
7.9 KiB
Python
239 lines
7.9 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"])
|
|
|
|
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()
|