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 $/.
This commit is contained in:
Adam Moussa 2026-10-01 20:52:08 -04:00
parent e7712e6d0f
commit 39a6a91c9c
No known key found for this signature in database
5 changed files with 97 additions and 50 deletions

6
.github/actionlint.yaml vendored Normal file
View file

@ -0,0 +1,6 @@
# actionlint 1.7.12 rejects `$/`, which GitHub accepts as a self-repository
# action reference (runner 2.336.0+). Drop this ignore when a release parses it.
paths:
.github/workflows/ci-terraform.yaml:
ignore:
- 'specifying action "\$/.github/actions/app-terraform-isolation" in invalid format because ref is missing'

View file

@ -0,0 +1,64 @@
name: App and Terraform isolation
description: Fail when a change set mixes Terraform with deployable application files.
inputs:
app-paths:
description: Newline-separated deployable paths. A trailing slash is a prefix. Any other entry is an exact file.
required: true
terraform-dir:
description: Directory containing Terraform sources.
required: true
default: terraform
event-name:
description: github.event_name from the calling workflow.
required: true
pr-base-sha:
description: pull_request base SHA. Empty outside pull_request.
required: false
default: ""
merge-group-base-sha:
description: merge_group base SHA. Empty outside merge_group.
required: false
default: ""
runs:
using: composite
steps:
- name: Classify changed paths
shell: bash
working-directory: ${{ github.workspace }}
env:
APP_PATHS: ${{ inputs.app-paths }}
TERRAFORM_DIR: ${{ inputs.terraform-dir }}
EVENT_NAME: ${{ inputs.event-name }}
PR_BASE_SHA: ${{ inputs.pr-base-sha }}
MERGE_GROUP_BASE_SHA: ${{ inputs.merge-group-base-sha }}
CHECKER: ${{ github.action_path }}/check_app_terraform_isolation.py
run: |
set -euo pipefail
classify() {
python3 "${CHECKER}"
}
case "${EVENT_NAME}" in
pull_request)
if [[ -z "${PR_BASE_SHA}" ]]; then
echo "FAIL: pull_request base SHA is empty" >&2
exit 1
fi
merge_base="$(git merge-base "${PR_BASE_SHA}" HEAD)"
git diff --name-only --diff-filter=ACMRD "${merge_base}" HEAD | classify
;;
merge_group)
if [[ -z "${MERGE_GROUP_BASE_SHA}" ]]; then
echo "FAIL: merge_group base SHA is empty" >&2
exit 1
fi
while IFS= read -r sha; do
[[ -z "${sha}" ]] && continue
git diff --name-only --diff-filter=ACMRD "${sha}^" "${sha}" | classify
done < <(git rev-list --reverse --first-parent "${MERGE_GROUP_BASE_SHA}..HEAD")
;;
*)
echo "SKIP: live isolation runs on pull_request and merge_group (event: ${EVENT_NAME})"
;;
esac

View file

@ -21,6 +21,9 @@ name: CI — Terraform
# merge_group classifies each first-parent commit against its parent, so a # merge_group classifies each first-parent commit against its parent, so a
# Terraform-only PR stacked with an app-only PR still passes. The classified # Terraform-only PR stacked with an app-only PR still passes. The classified
# diff includes deletions. Terraform paths are those under working-directory. # diff includes deletions. Terraform paths are those under working-directory.
# The checker is a composite action in this repository. `$/` resolves that
# action at this workflow's commit, so callers do not clone this private
# repository with their GITHUB_TOKEN.
on: on:
workflow_call: workflow_call:
@ -72,50 +75,12 @@ jobs:
- name: Terraform validate - name: Terraform validate
run: terraform validate run: terraform validate
- name: Checkout isolation checker
if: inputs.app-paths != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Sea-Haven-Industries/.github
ref: ${{ github.workflow_sha }}
path: .ci-org-github
persist-credentials: false
- name: App and Terraform isolation - name: App and Terraform isolation
if: inputs.app-paths != '' if: inputs.app-paths != ''
working-directory: ${{ github.workspace }} uses: $/.github/actions/app-terraform-isolation
env: with:
APP_PATHS: ${{ inputs.app-paths }} app-paths: ${{ inputs.app-paths }}
TERRAFORM_DIR: ${{ inputs.working-directory }} terraform-dir: ${{ inputs.working-directory }}
EVENT_NAME: ${{ github.event_name }} event-name: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} pr-base-sha: ${{ github.event.pull_request.base.sha }}
MERGE_GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }} merge-group-base-sha: ${{ github.event.merge_group.base_sha }}
CHECKER: ${{ github.workspace }}/.ci-org-github/scripts/check_app_terraform_isolation.py
run: |
set -euo pipefail
classify() {
python3 "${CHECKER}"
}
case "${EVENT_NAME}" in
pull_request)
if [[ -z "${PR_BASE_SHA}" ]]; then
echo "FAIL: pull_request base SHA is empty" >&2
exit 1
fi
merge_base="$(git merge-base "${PR_BASE_SHA}" HEAD)"
git diff --name-only --diff-filter=ACMRD "${merge_base}" HEAD | classify
;;
merge_group)
if [[ -z "${MERGE_GROUP_BASE_SHA}" ]]; then
echo "FAIL: merge_group base SHA is empty" >&2
exit 1
fi
while IFS= read -r sha; do
[[ -z "${sha}" ]] && continue
git diff --name-only --diff-filter=ACMRD "${sha}^" "${sha}" | classify
done < <(git rev-list --reverse --first-parent "${MERGE_GROUP_BASE_SHA}..HEAD")
;;
*)
echo "SKIP: live isolation runs on pull_request and merge_group (event: ${EVENT_NAME})"
;;
esac

View file

@ -6,14 +6,19 @@ from __future__ import annotations
import os import os
import re import re
import subprocess import subprocess
import sys
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from check_app_terraform_isolation import first_isolation_violation, isolation_violation 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))
WORKFLOW = ( from check_app_terraform_isolation import ( # noqa: E402
Path(__file__).resolve().parents[1] / ".github" / "workflows" / "ci-terraform.yaml" first_isolation_violation,
isolation_violation,
) )
APP_PATHS = "src/\npackage.json\npackage-lock.json\n" APP_PATHS = "src/\npackage.json\npackage-lock.json\n"
@ -133,10 +138,16 @@ class WorkflowDiffTests(unittest.TestCase):
def test_workflow_passes_working_directory(self) -> None: def test_workflow_passes_working_directory(self) -> None:
self.assertIn( self.assertIn(
"TERRAFORM_DIR: ${{ inputs.working-directory }}", "terraform-dir: ${{ inputs.working-directory }}",
WORKFLOW.read_text(), 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: def test_deleted_app_file_is_classified(self) -> None:
diff_filter = _workflow_diff_filters()[0] diff_filter = _workflow_diff_filters()[0]
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
@ -173,7 +184,8 @@ class WorkflowDiffTests(unittest.TestCase):
def _workflow_diff_filters() -> list[str]: def _workflow_diff_filters() -> list[str]:
return re.findall(r"--diff-filter=([A-Z]+)", WORKFLOW.read_text()) action = (ACTION / "action.yml").read_text()
return re.findall(r"--diff-filter=([A-Z]+)", action)
def _git(repo: Path, *args: str) -> str: def _git(repo: Path, *args: str) -> str: