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
102 changes: 102 additions & 0 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: gate

# Reusable caller-named gate job for branch-protection required-check contexts.
#
# Reusable workflows produce job names like "rust (1.94.0)" or "CodeQL (rust)"
# that don't match the short, exact required-check context names that branch
# protection rules expect (e.g. "CI", "CodeQL", "Scorecard"). This workflow
# emits a single job whose `name:` is exactly the caller-supplied check name,
# runs `if: always()`, and validates that every declared upstream job result
# is "success" (with an optional allow-list for "skipped").
#
# Usage from a caller workflow:
#
# jobs:
# # ... fan-out jobs (check, test, clippy, fmt, ...) ...
#
# ci-gate:
# needs: [check, test, clippy, fmt]
# uses: NDDev-it-com/ci-workflows/.github/workflows/gate.yml@<sha>
# with:
# check_name: CI
# required_jobs: "check,test,clippy,fmt"
# allow_skipped: "coverage" # coverage may be skipped if CODECOV_ENABLED is false
#
# The gate job name in the GitHub UI / branch-protection context will be exactly
# "CI", matching the required-check entry.

on:
workflow_call:
inputs:
check_name:
description: 'Exact branch-protection required-check context name. The gate job will be named this.'
type: string
required: true
required_jobs:
description: 'Comma-separated list of upstream job IDs (from `needs`) that must succeed.'
type: string
required: true
allow_skipped:
description: 'Comma-separated list of job IDs allowed to be "skipped" (not failure). Empty = none.'
type: string
default: ''
runner:
description: 'Runner label. Defaults to ubuntu-latest.'
type: string
default: 'ubuntu-latest'

permissions: {}

jobs:
gate:
name: ${{ inputs.check_name }}
runs-on: ${{ inputs.runner }}
timeout-minutes: 5
if: always()
permissions:
contents: read
steps:
- name: Validate required job results
shell: bash
env:
REQUIRED_JOBS: ${{ inputs.required_jobs }}
ALLOW_SKIPPED: ${{ inputs.allow_skipped }}
# Each upstream job result is passed via toJSON(needs) — the caller
# must pass this as a secret/env because reusable workflows cannot
# access the caller's `needs` context directly.
NEEDS_JSON: ${{ toJson(needs) }}
run: |
set -euo pipefail

# Parse the needs JSON to extract each job's result.
# needs.<job_id>.result is the field we care about.
python3 -I <<'PY'
import json
import os
import sys

needs = json.loads(os.environ["NEEDS_JSON"])
required = [j.strip() for j in os.environ["REQUIRED_JOBS"].split(",") if j.strip()]
allow_skipped = {j.strip() for j in os.environ["ALLOW_SKIPPED"].split(",") if j.strip()}

failures = []
for job_id in required:
entry = needs.get(job_id)
if entry is None:
failures.append(f" {job_id}: NOT FOUND in needs (typo or missing dependency?)")
continue
result = entry.get("result", "unknown")
if result == "success":
continue
if result == "skipped" and job_id in allow_skipped:
continue
failures.append(f" {job_id}: {result}")

if failures:
print(f"gate '{os.environ.get('CHECK_NAME', '?')}' FAILED — required jobs did not all succeed:", file=sys.stderr)
for f in failures:
print(f, file=sys.stderr)
sys.exit(1)

print(f"gate '{os.environ.get('CHECK_NAME', '?')}' PASSED — all required jobs succeeded.")
PY
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@

### Added

- **`gate.yml` — caller-named gate job for branch-protection required-check contexts.**
Reusable workflows produce job names like `rust (1.94.0)` or `CodeQL (rust)`
that don't match the short, exact context names branch protection expects
(`CI`, `CodeQL`, `Scorecard`). This workflow emits a single job whose `name:`
is the caller-supplied `check_name`, runs `if: always()`, and validates that
every declared upstream job result is `success` (with an `allow_skipped`
allow-list for jobs like coverage that may be toggled off). Enables migration
of repos with branch-protection required checks to reusable workflows.

- **Personal-account consumer tier.** A repository owned by a *personal* GitHub
account (not an organization) inherits the private-free posture but cannot
reach an org-level self-hosted runner group — it needs a **repo-level**
Expand Down
20 changes: 20 additions & 0 deletions catalog/capabilities.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1528,3 +1528,23 @@ capabilities:
sources:
- "https://github.com/googleapis/release-please-action"
- "https://github.com/changesets/action"

- id: gate
name: Caller-named gate job for branch-protection contexts
cluster: actions-core
status: ga
public_oss: free
private_free: free
private_paid: available
workflow: .github/workflows/gate.yml
example: null
required_permissions:
- "contents: read"
required_settings: []
risks:
- "The gate validates needs.<job_id>.result; a typo in required_jobs will fail the gate with a NOT FOUND error"
- "allow_skipped only tolerates 'skipped' — it does NOT tolerate 'failure' or 'cancelled'"
deprecations: null
last_verified: "2026-08-04"
sources:
- "https://docs.github.com/actions/using-jobs/using-jobs-in-a-workflow#defining-prerequisite-jobs"
5 changes: 5 additions & 0 deletions catalog/runtime-coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,8 @@ entries:
last_run: https://github.com/NDDev-it-com/ci-workflows/actions/runs/30706690921
proven_digest: 4f355439c4bcb5ffb1c4c3d3e4b2e2bc1b9ffa5c0d95d1355281a9ed60c88ab0
waiver: null
- workflow: .github/workflows/gate.yml
status: unverified
evidence: 'New reusable workflow; awaiting first live workflow_call run from a consumer.'
last_run: null
waiver: null
1 change: 1 addition & 0 deletions docs/generated/capability-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
| Docs CI (`docs-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/docs-ci.yml` | `examples/infra/docs.yml` |
| Docs quality (links, spelling, markdown) (`docs-quality`) | actions-core | ga | free | free | available | `.github/workflows/docs-quality.yml` | `examples/quality/docs-quality.yml` |
| .NET CI (`dotnet-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/dotnet-ci.yml` | `examples/languages/dotnet.yml` |
| Caller-named gate job for branch-protection contexts (`gate`) | actions-core | ga | free | free | available | `.github/workflows/gate.yml` | `-` |
| Go CI (`go-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/go-ci.yml` | `examples/languages/go.yml` |
| Java CI (`java-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/java-ci.yml` | `examples/languages/java.yml` |
| Kotlin/Android CI (`kotlin-android-ci`) | actions-core | ga | free | conditional | available | `.github/workflows/kotlin-android-ci.yml` | `examples/languages/kotlin-android.yml` |
Expand Down
1 change: 1 addition & 0 deletions docs/generated/workflow-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
| `.github/workflows/docs-quality.yml` | `docs-quality` | ga |
| `.github/workflows/dotnet-ci.yml` | `dotnet-ci` | ga |
| `.github/workflows/fuzzing.yml` | `fuzzing` | ga |
| `.github/workflows/gate.yml` | `gate` | ga |
| `.github/workflows/gitleaks.yml` | internal | internal |
| `.github/workflows/go-ci.yml` | `go-ci` | ga |
| `.github/workflows/grype-scan.yml` | `grype-sca` | ga |
Expand Down
Loading