Single source-of-truth workflow for org-wide Zagware Scanner enforcement on GitHub, mirroring what GitLab's Pipeline Execution Policies already give you natively. Two paths, depending on plan:
workflows ruleset rule |
Reusable workflow + caller | |
|---|---|---|
| Plan required | GitHub Team or higher | Any (Free/Pro/Team/Enterprise) |
| Caller file needed in target repo | No | Yes, one per repo |
| Silently stops enforcing if... | Never — nothing to tamper with in the target repo | Caller file is deleted/renamed, or its job ID drifts from the branch-protection "required" name |
| Supported trigger events | pull_request, pull_request_target, merge_group only |
Anything the caller declares |
| Fails closed if Actions is disabled on target repo | Yes — the check never resolves and merge is permanently blocked (expect support tickets) | Same |
Confirmed by testing directly against both a Free-plan org
(zagware) and a Team-plan org (zagware):
orgs/{org}/rulesets returns 403 Upgrade to GitHub Team on Free/Pro, full
stop — there is no repo-level substitute for the workflows rule type at
any plan (repos/{owner}/{repo}/rulesets rejects it structurally, GitHub's
own docs confirm it's org/enterprise-only). Repository-level rulesets are
otherwise unrestricted by plan; they just don't carry this specific rule.
.github/workflows/security-scan.yml in this repo is the file a
workflows-type organization ruleset points at. GitHub runs it directly
against every targeted repo's pull requests — the github context (repo,
ref, token, PR number) is bound to the target repo the same way a reusable
workflow's context is bound to its caller, so actions/checkout with no
repository: argument checks out the target PR automatically, and secrets
referenced in the workflow resolve against whatever the target repo can see.
Setup:
./rulesets/setup-org-level.sh <your-org>This defines a zagware-scan-scope custom property, then creates one
organization ruleset scoped to zagware-scan-scope=enabled repos. Opt a repo
in without touching the ruleset:
gh api orgs/<org>/properties/values -X PATCH \
-f 'repository_names[]=<repo>' \
-f 'properties[][property_name]=zagware-scan-scope' \
-f 'properties[][value]=enabled'Verified live end-to-end against zagware/gtp-test-insecure — a repo with
zero .github/workflows/ files: the required check
"IaC + SCA + Secrets security scan" appeared, ran, and passed, and
GET /repos/{owner}/{repo}/rules/branches/{branch} confirms it as an active
rule sourced from the organization ruleset (ruleset_source_type: "Organization").
Per-target-repo prerequisite (real GitHub setting, not something the ruleset can grant): Settings → Actions → General → Workflow permissions → "Read and write permissions", needed for the PR comment.
Org-level rulesets also need this same setting available org-wide
(orgs/<org>/actions/permissions/workflow) if you want it to apply to every
targeted repo by default rather than toggling it per repo.
.github/workflows/reusable-scan.yml holds the actual scan logic behind a
workflow_call trigger. Each target repo gets a thin caller:
# .github/workflows/zagware.yml (in the TARGET repo)
name: Zagware Security Scanner
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
scan:
uses: <org>/zagware-security-workflows/.github/workflows/reusable-scan.yml@main
secrets: inheritThen mark the scan job's check (Zagware Security Scanner / scan) required
in the target repo's branch protection or a repository-level ruleset's
required_status_checks rule (that rule type IS repo-level-compatible on any
plan). Bumping the image tag, adding an env var, or changing the scan logic
in this one reusable-scan.yml file propagates to every caller on its next
run — but the caller file itself is real, per-repo state that can drift or
be deleted without the org noticing.
Verified live against zagware/zagware-ruleset-demo
(Free plan) using this exact pattern.
/zagware suppress pushes a commit back to the PR branch using
GITHUB_TOKEN. GitHub's anti-recursion protection means a GITHUB_TOKEN-
authored push never re-triggers pull_request: synchronize — so under
Path A, the new commit's required check would never get created and the PR
would stay blocked indefinitely. This isn't solvable from inside the
workflow; it needs a non-GITHUB_TOKEN identity (a GitHub App installation
token) to make the push, since App-authored pushes DO re-trigger workflows.
See docs/GITHUB_ORG_ENFORCEMENT_DESIGN.md in zagware/git-tracking-platform
(private repo) for the full writeup, including why a single narrow, opt-in
contents: write grant on the platform's existing shared GitHub App (not a new
per-customer App, and not the org-wide write grants an earlier draft of that doc
wrongly asked for) is the only permission change worth considering here — everything
else about org-wide enforcement setup is designed to run under the org owner's own
credentials, not the App's.