Skip to content
Closed
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
32 changes: 27 additions & 5 deletions .github/scripts/detect_native_gates.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ revision_has_supabase() {
git -C "$repository" cat-file -e "$1:supabase/config.toml" 2>/dev/null
}

# PR_LABELS carries labels from the pull_request event context (never from
# repository file content), so a head cannot self-authorize by editing
# tracked files. merge_group events have no labels, so this is empty there
# and the strict behavior below applies unchanged.
has_allow_label=0
for label in ${PR_LABELS:-}; do
if [ "$label" = "allow-protected-workflow-change" ]; then
has_allow_label=1
break
fi
done

for revision in "$base" "$head"; do
if [ -z "$revision" ] || ! revision_exists "$revision"; then
printf 'Cannot resolve required-gate revision: %s\n' "${revision:-<empty>}" >&2
Expand All @@ -40,19 +52,29 @@ done

# A gate already required by the protected base revision is itself protected.
# Allowing its workflow file to change in the same head would let that head
# manufacture a passing check with weaker behavior.
# manufacture a passing check with weaker behavior. The
# allow-protected-workflow-change PR label is a deliberate, auditable escape
# hatch for intentionally strengthening a protected workflow.
while IFS= read -r -d '' path; do
if ! git -C "$repository" diff --quiet "$base" "$head" -- "$path"; then
printf 'Base-required workflow was modified or removed: %s\n' "$path" >&2
exit 1
if [ "$has_allow_label" -eq 1 ]; then
printf 'Protected workflow changed under allow-protected-workflow-change: %s\n' "$path"
else
printf 'Base-required workflow was modified or removed: %s\n' "$path" >&2
exit 1
fi
fi
done < <(workflows_with_job "$base" repo-quality-gate)

if revision_has_supabase "$base"; then
while IFS= read -r -d '' path; do
if ! git -C "$repository" diff --quiet "$base" "$head" -- "$path"; then
printf 'Base-required workflow was modified or removed: %s\n' "$path" >&2
exit 1
if [ "$has_allow_label" -eq 1 ]; then
printf 'Protected workflow changed under allow-protected-workflow-change: %s\n' "$path"
else
printf 'Base-required workflow was modified or removed: %s\n' "$path" >&2
exit 1
fi
fi
done < <(workflows_with_job "$base" db-tests)
fi
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ jobs:
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.sha }}
# From the pull_request event context only, never repository file
# content, so a head cannot self-authorize by editing tracked files.
# merge_group events carry no labels, so this is empty there.
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }}
run: bash .quality-gate-source/.github/scripts/detect_native_gates.sh . "$BASE_SHA" "$HEAD_SHA"

node-ci:
Expand Down
106 changes: 105 additions & 1 deletion tests/test_native_required_gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,17 @@ def commit(self, message: str) -> str:
capture_output=True,
).stdout.strip()

def detect_result(self, base: str, head: str) -> subprocess.CompletedProcess[str]:
def detect_result(
self, base: str, head: str, pr_labels: str | None = None
) -> subprocess.CompletedProcess[str]:
environment = os.environ.copy()
if pr_labels is not None:
environment["PR_LABELS"] = pr_labels
else:
environment.pop("PR_LABELS", None)
return subprocess.run(
["bash", str(DETECT), str(self.repository), base, head],
env=environment,
text=True,
capture_output=True,
check=False,
Expand Down Expand Up @@ -97,6 +105,102 @@ def test_modifying_base_gate_workflow_is_rejected(self) -> None:
self.assertNotEqual(result.returncode, 0)
self.assertIn(".github/workflows/quality.yml", result.stderr)

def test_modifying_base_gate_workflow_without_label_still_rejected(self) -> None:
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: ubuntu-latest\n",
)
base = self.commit("base native aggregate")
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: self-hosted\n",
)
head = self.commit("attempt to modify protected workflow")

result = self.detect_result(base, head, pr_labels="some-other-label")
self.assertNotEqual(result.returncode, 0)
self.assertIn("Base-required workflow was modified or removed", result.stderr)
self.assertIn(".github/workflows/quality.yml", result.stderr)

def test_modifying_base_gate_workflow_with_allow_label_succeeds(self) -> None:
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: ubuntu-latest\n",
)
base = self.commit("base native aggregate")
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: self-hosted\n",
)
head = self.commit("intentionally strengthen protected workflow")

result = self.detect_result(
base, head, pr_labels="allow-protected-workflow-change"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn(
"Protected workflow changed under allow-protected-workflow-change: "
".github/workflows/quality.yml",
result.stdout,
)

def test_unrelated_workflow_change_unaffected_by_label(self) -> None:
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: ubuntu-latest\n",
)
self.write(
".github/workflows/other.yml",
"jobs:\n other-job:\n runs-on: ubuntu-latest\n",
)
base = self.commit("base with protected and unrelated workflows")
self.write(
".github/workflows/other.yml",
"jobs:\n other-job:\n runs-on: self-hosted\n",
)
head = self.commit("modify unrelated workflow only")

for pr_labels in (None, "allow-protected-workflow-change"):
with self.subTest(pr_labels=pr_labels):
result = self.detect_result(base, head, pr_labels=pr_labels)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("other.yml", result.stderr)

def test_allow_label_has_no_effect_without_protected_workflow_change(self) -> None:
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: ubuntu-latest\n",
)
base = self.commit("base native aggregate")
self.write("README.md", "unrelated change\n")
head = self.commit("touch unrelated file")

result = self.detect_result(
base, head, pr_labels="allow-protected-workflow-change"
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("Protected workflow changed", result.stdout)

def test_merge_group_style_empty_labels_keeps_strict_behavior(self) -> None:
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: ubuntu-latest\n",
)
base = self.commit("base native aggregate")
self.write(
".github/workflows/quality.yml",
"jobs:\n repo-quality-gate:\n runs-on: self-hosted\n",
)
head = self.commit("attempt to modify protected workflow in merge_group")

# merge_group events carry no labels, so PR_LABELS is unset/empty and
# the strict rejection must apply exactly as without the label.
for pr_labels in (None, ""):
with self.subTest(pr_labels=pr_labels):
result = self.detect_result(base, head, pr_labels=pr_labels)
self.assertNotEqual(result.returncode, 0)
self.assertIn(".github/workflows/quality.yml", result.stderr)

def test_adding_gate_in_head_activates_requirement(self) -> None:
self.write("README.md", "initial\n")
base = self.commit("base")
Expand Down