From 682c49fcd7164b9748e9bb47a4f19db26013de52 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 4 Aug 2026 15:43:34 +0500 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20gate.yml=20=E2=80=94=20caller?= =?UTF-8?q?-named=20gate=20job=20for=20branch-protection=20contexts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reusable workflows produce job names that don't match the short required-check context names branch protection expects (CI, CodeQL, Scorecard). This new reusable workflow emits a single job named exactly check_name, runs if: always(), and validates that every declared upstream job result is 'success' (with allow_skipped tolerance). Enables migration of repos with branch-protection required checks to reusable workflows. Catalog entry + CHANGELOG included. --- .github/workflows/gate.yml | 102 +++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 9 ++++ catalog/capabilities.yml | 20 ++++++++ 3 files changed, 131 insertions(+) create mode 100644 .github/workflows/gate.yml diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml new file mode 100644 index 0000000..4f744cd --- /dev/null +++ b/.github/workflows/gate.yml @@ -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@ +# 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..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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1916953..47c223d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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** diff --git a/catalog/capabilities.yml b/catalog/capabilities.yml index 369c6e0..bdee03e 100644 --- a/catalog/capabilities.yml +++ b/catalog/capabilities.yml @@ -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..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" From c663fa2312a8e117ef730ac937f76a0e4219ee36 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Tue, 4 Aug 2026 15:44:32 +0500 Subject: [PATCH 2/2] chore: add runtime-coverage record + regenerate docs for gate.yml --- catalog/runtime-coverage.yml | 5 +++++ docs/generated/capability-matrix.md | 1 + docs/generated/workflow-inventory.md | 1 + 3 files changed, 7 insertions(+) diff --git a/catalog/runtime-coverage.yml b/catalog/runtime-coverage.yml index 4f6af8c..489a784 100644 --- a/catalog/runtime-coverage.yml +++ b/catalog/runtime-coverage.yml @@ -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 diff --git a/docs/generated/capability-matrix.md b/docs/generated/capability-matrix.md index 36d5cbe..6210a53 100644 --- a/docs/generated/capability-matrix.md +++ b/docs/generated/capability-matrix.md @@ -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` | diff --git a/docs/generated/workflow-inventory.md b/docs/generated/workflow-inventory.md index a4d0f3c..75fbc2e 100644 --- a/docs/generated/workflow-inventory.md +++ b/docs/generated/workflow-inventory.md @@ -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 |