Skip to content
Merged
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
18 changes: 14 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
# dependency-scan pull requests and merge groups only: fails on a HIGH or
# CRITICAL vulnerability that is present at head and absent
# at base. Inherited findings are Dependabot's job.
# gate needs every other job; fails if any of them failed or was
# cancelled.
# gate needs every other job; only dependency-scan may skip, on
# events other than pull_request and merge_group.
#
# Scripts are inline on purpose: a reusable workflow checks out the CALLER, so
# it cannot read helper files from this repository.
Expand Down Expand Up @@ -358,9 +358,19 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every job to succeed or be skipped
- name: Require success with explicit skip policy
env:
NEEDS: ${{ toJSON(needs) }}
EVENT_NAME: ${{ github.event_name }}
run: |
: "${EVENT_NAME:?GitHub event is required for the skip policy}"
jq -r 'to_entries[] | "\(.key): \(.value.result)"' <<<"${NEEDS}"
jq -e 'all(.[]; .result == "success" or .result == "skipped")' <<<"${NEEDS}" >/dev/null
jq -e --arg event "${EVENT_NAME}" '
has("check") and has("dependency-scan")
and .check.result == "success"
and all(to_entries[];
.value.result == "success"
or (.key == "dependency-scan" and .value.result == "skipped"
and $event != "pull_request" and $event != "merge_group")
)
' <<<"${NEEDS}" >/dev/null
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ Jobs:
advisory (CVSS 7.0 or more, or rated HIGH/CRITICAL by GitHub) that the
change adds. Inherited findings are reported as a notice and left to
Dependabot.
- `gate`: needs every other job, runs with `if: always()`, and fails if any of
them failed or was cancelled. Skipped jobs pass.
- `gate`: needs every other job and runs with `if: always()`. `make check`
must succeed. Only `dependency-scan` may skip, on events other than
`pull_request` and `merge_group`, matching its job condition. Missing jobs,
other skips, failures and cancellations fail the gate.

### `auto-tag.yml`

Expand Down
45 changes: 33 additions & 12 deletions tests/test_v2_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,21 +299,42 @@ def test_no_required_check_arms_nothing(self) -> None:


class GateTest(unittest.TestCase):
SCRIPT = step_script("ci.yml", "Require every job to succeed or be skipped")
SCRIPT = step_script("ci.yml", "Require success with explicit skip policy")

def gate(self, **results: str) -> int:
def gate(self, event: str = "pull_request", **results: str) -> int:
needs = {job: {"result": result, "outputs": {}} for job, result in results.items()}
with tempfile.TemporaryDirectory() as tmp:
return run_bash(self.SCRIPT, Path(tmp), {"NEEDS": json.dumps(needs)}).returncode

def test_success_and_skipped_pass(self) -> None:
self.assertEqual(self.gate(check="success", scan="skipped"), 0)

def test_failure_fails(self) -> None:
self.assertNotEqual(self.gate(check="failure", scan="success"), 0)

def test_cancelled_fails(self) -> None:
self.assertNotEqual(self.gate(check="success", scan="cancelled"), 0)
return run_bash(
self.SCRIPT, Path(tmp), {"NEEDS": json.dumps(needs), "EVENT_NAME": event}
).returncode

def test_event_and_result_matrix(self) -> None:
for event in ("pull_request", "merge_group", "push", "workflow_dispatch", "repository_dispatch", "schedule"):
for check in ("success", "failure", "cancelled", "skipped"):
for scan in ("success", "failure", "cancelled", "skipped"):
with self.subTest(event=event, check=check, scan=scan):
allowed = check == "success" and (
scan == "success"
or (scan == "skipped" and event not in ("pull_request", "merge_group"))
)
result = self.gate(event, **{"check": check, "dependency-scan": scan})
self.assertEqual(result == 0, allowed)

def test_missing_required_job_fails(self) -> None:
for results in ({}, {"check": "success"}, {"dependency-scan": "success"}):
with self.subTest(results=results):
self.assertNotEqual(self.gate("push", **results), 0)

def test_additional_jobs_have_no_skip_allowance(self) -> None:
for result in ("success", "failure", "cancelled", "skipped", "unknown"):
with self.subTest(result=result):
code = self.gate(
"push", **{"check": "success", "dependency-scan": "skipped", "new-job": result}
)
self.assertEqual(code == 0, result == "success")

def test_missing_event_fails(self) -> None:
self.assertNotEqual(self.gate("", **{"check": "success", "dependency-scan": "skipped"}), 0)


class DetectToolchainsTest(unittest.TestCase):
Expand Down
Loading