| title | Security CI gates |
|---|---|
| folder | docs |
| description | GitHub Actions workflows that harden the repo outside the verify gate — workflow lint, workflow security, and secret scanning. |
| entry_point | false |
The verify gate is intentionally fast and deterministic. It is not a security audit. This document records the security-oriented GitHub Actions workflows that run alongside it. Read it with ci-automation.md: that companion page covers PR-title linting, spell check, Dependabot version updates, and the non-security automation that completes the repository posture.
Each gate is a separate workflow file under .github/workflows/ so it can be enabled, disabled, or replaced without touching the verify gate.
| Workflow | File | Trigger | What it catches |
|---|---|---|---|
| actionlint | .github/workflows/actionlint.yml |
PR + push to main, paths .github/workflows/**, .github/actions/** |
Workflow YAML schema errors, deprecated action calls, shellcheck inside run: blocks. |
| zizmor | .github/workflows/zizmor.yml |
PR + push to main (workflow paths) + weekly schedule |
Workflow security smells: template injection, unpinned actions, excessive permissions, dangerous triggers. SARIF results land in the GitHub Security tab. |
| CodeQL | .github/workflows/codeql.yml |
PR + push to main + weekly schedule |
Static analysis for javascript-typescript (TypeScript scripts) and actions (GitHub Actions workflows). SARIF results land in the GitHub Security tab. |
| gitleaks | .github/workflows/gitleaks.yml |
PR + push to main + weekly schedule |
Committed secrets — API keys, tokens, private keys — detected against the full git history. |
| dependency-review | .github/workflows/dependency-review.yml |
PRs touching package.json, package-lock.json, npm-shrinkwrap.json, or workflow/action files |
New vulnerable npm or GitHub Actions dependencies introduced by the PR diff. Fails on high and critical severities; license policy is deferred. |
The least-privilege permissions: block on each workflow is documented in docs/rbac.md §GitHub Actions, alongside the harness and branch-protection rules that bound the rest of the autonomous flow.
- Independent failure surfaces. A flaky workflow lint should not block a security scan and vice versa.
- Targeted triggers.
actionlintandzizmoronly run on workflow file changes;gitleaksruns on every PR. - Replaceable. Adopters of this template can swap any of the three for an internal equivalent without rewiring the others.
actionlint and zizmor defend the CI pipeline itself — every other gate lives downstream of GitHub Actions, so that surface gets hardened first. gitleaks defends the repo against the most common single-event leak (a secret pasted into a commit). dependency-review catches vulnerable packages and actions before dependency diffs merge. Together they close the highest-leverage gaps for a workflow-template repo.
This map keeps deferred candidates separate from checks that already exist so adopters do not reverse-engineer the intended state from workflow files.
| Control | Where it lives | Notes |
|---|---|---|
| Local deterministic verify gate | verify-gate.md, .github/workflows/verify.yml |
Required on every PR. |
| Workflow lint | .github/workflows/actionlint.yml |
Path-triggered for workflow/action changes. |
| Workflow security scan | .github/workflows/zizmor.yml |
Uploads SARIF to code scanning and gates default-persona findings. |
| CodeQL static analysis | .github/workflows/codeql.yml |
Explicit pinned workflow; scans javascript-typescript and actions. Uploads SARIF to code scanning on every PR, push to main, and weekly. |
| Secret scan | .github/workflows/gitleaks.yml |
Required on every PR. |
| Dependency review | .github/workflows/dependency-review.yml |
Path-triggered for dependency, workflow, and local-action changes. |
| PR-title lint | ci-automation.md, .github/workflows/pr-title.yml |
Required on every PR. |
| Spell check | ci-automation.md, .github/workflows/typos.yml |
Required on every PR. |
| Dependabot version updates | ci-automation.md, .github/dependabot.yml |
Bumps pinned GitHub Actions and npm dev dependencies. |
| Supply-chain posture (Scorecard) | .github/workflows/scorecard.yml |
Weekly + push to the default branch (develop in this upstream repo). Uploads SARIF as an artifact for every repo and to code scanning on public repos. Initial findings are advisory — triage via Security tab. |
| Dependabot alerts | GitHub repository settings | Alerts on vulnerable dependencies introduced to the repo. Security update PRs are opt-in. |
| Markdown lint (advisory) | .github/workflows/markdownlint.yml |
PR-triggered, changed files only, non-blocking. Promotion plan: docs/markdownlint-rollout.md. |
Note: Scorecard findings are advisory at first. Public repositories also publish results to the GitHub Security/code-scanning tab; private/internal downstream repositories still receive the SARIF artifact without requiring GitHub Code Security. Triage results and file concrete issues before promoting any check to a required gate.
| Candidate | Current decision |
|---|---|
| Markdown lint | Non-blocking changed-files workflow active. See docs/markdownlint-rollout.md for the promotion plan. |
| Candidate | Why not now |
|---|---|
| License blocking in dependency review | Needs an explicit license allow/deny policy before it can be a fair merge gate. |
| Dependabot security update PRs by default | Disabled until the alert baseline is known and the maintainer wants automatic remediation PR volume. Dependabot alerts are enabled (see Implemented table). |
| Global required checks for path-triggered workflows | GitHub does not create those checks for unrelated PRs, so a global requirement can deadlock ordinary documentation or script changes. Use path-scoped rulesets where available. |
The upstream main ruleset requires the always-running checks that every PR should produce:
VerifyConventional Commits PR titlespell checkscan for committed secrets
The ruleset also requires an up-to-date branch and resolved review threads. It does not require approving reviews yet; for the current solo-maintainer workflow, required checks provide the stronger signal without adding approval ceremony. This makes the remote policy match the local rule that npm run verify must be green before merge.
Path-triggered security workflows are intentionally not global required checks:
actionlintruns only for.github/workflows/**and.github/actions/**.zizmor static analysisruns only for workflow/action changes and on its weekly schedule.dependency reviewruns only for dependency manifest, lockfile, workflow, and local-action changes.
If GitHub rulesets in the target repository support path-scoped required checks, require those checks for the matching paths. Otherwise, reviewers must treat the path-triggered job result as merge-blocking whenever GitHub runs it.
Use this checklist after creating a downstream repository or auditing this upstream one. It covers settings that cannot be fully represented by committed workflow files.
| Setting | Required posture | Source |
|---|---|---|
| Integration-branch ruleset | Protect main for Shape A, or develop, main, demo, and product-page for Shape B; block deletion and non-fast-forward updates; require PRs; require up-to-date branches; require resolved review threads. |
branching.md#required-ruleset-for-develop-main-demo-product-page |
| Always-running required checks | Require Verify, Conventional Commits PR title, spell check, and scan for committed secrets. |
This document and ci-automation.md |
| Path-triggered checks | Require actionlint, zizmor static analysis, and dependency review only with path-scoped rulesets when supported; otherwise treat them as merge-blocking whenever they run. |
Required status checks |
| Dependabot alerts | Enable repository-level Dependabot alerts so new advisories against already-merged dependencies surface outside PR review. | ci-automation.md#dependabot-policy |
| Dependabot security updates | Leave disabled until the team accepts automatic remediation PR volume, then enable deliberately. | ci-automation.md#dependabot-policy |
| Code scanning | Enable code scanning visibility. Both zizmor and codeql publish SARIF to the Security tab. |
Automation posture map |
| Secret scanning | Enable GitHub secret scanning when the account plan supports it; keep gitleaks as the committed CI safety net. |
Workflow table |
| GitHub Pages | Enable Pages for the product page workflow if the project publishes sites/index.html. |
rbac.md#github-pages |
| CODEOWNERS | Replace placeholder owners in .github/CODEOWNERS with real users or teams for a live repository, or document that the file is template-only. |
branching.md |
Project reviews and security posture checks should record dated evidence for the settings above. Store the evidence in the review artifact that requested it, usually quality/<review-slug>/history-review.md or quality/<review-slug>/findings.md; do not commit screenshots or raw API payloads unless a maintainer explicitly wants that audit bundle in the repository.
For each checked setting, record:
| Field | What to record |
|---|---|
| Date | UTC date when the setting was inspected. |
| Actor/tool | Human reviewer, Codex, Claude Code, gh, browser UI, or other tool. |
| Repository | owner/name and branch model shape under review. |
| Setting | The checklist row being inspected. |
| Source | API command, workflow URL, UI path, or screenshot filename if stored out of repo. |
| Observed posture | Short factual summary, including rule names and required checks where applicable. |
| Result | matches, partial, missing, not visible to token, or not applicable, plus a linked follow-up issue for anything unresolved. |
Use gh api where the token can inspect the setting:
# Rulesets, required checks, bypass actors, and target refs.
gh api repos/OWNER/REPO/rulesets --jq '.[] | {name, target, enforcement, conditions, bypass_actors, rules}'
# Repository security and vulnerability settings visible to the token.
gh api repos/OWNER/REPO --jq '{visibility, default_branch, has_vulnerability_alerts, security_and_analysis}'
# Pages source, custom domain, and build type when Pages is enabled.
gh api repos/OWNER/REPO/pages --jq '{status, html_url, source, build_type, cname}'
# Repository variables used by release readiness and operational bots.
gh variable list --repo OWNER/REPO
# Secret names only; values are never retrievable.
gh secret list --repo OWNER/REPO
# Recent code-scanning analyses, including CodeQL and uploaded SARIF tools.
gh api repos/OWNER/REPO/code-scanning/analyses --paginate --jq '.[] | {tool: .tool.name, category, ref, commit_sha, created_at}' | head -20Some settings remain manual because GitHub does not expose them consistently to every token or plan:
| Setting | Manual evidence expectation |
|---|---|
| Secret scanning availability and push protection | Security -> Secret scanning UI summary, or note not visible to token with the account plan if unavailable. |
| Dependabot alerts and security updates | Security -> Dependabot UI summary when has_vulnerability_alerts or security_and_analysis is absent from the API response. |
| Maintainer 2FA or organization security policy | Organization/user security settings, recorded as maintainer-attested if the reviewer cannot inspect them. |
| Branch ruleset bypass actor identity | Ruleset UI confirmation when API output redacts or abbreviates actor details. |
| Pages environment allow-list and deployment approvals | Environments -> github-pages UI summary when environment APIs are unavailable to the token. |
If a setting is manual-only, record the UI path and the reviewer who inspected it. If the reviewer cannot inspect it, record not visible to token and file a follow-up rather than inferring the posture from workflow YAML.
The dependency-review workflow uses GitHub's dependency graph diff for pull requests. It runs only when a PR changes the npm manifest/lock or GitHub Actions workflow/action files.
Policy:
- Fail on vulnerabilities with severity
highorcritical. - Report lower-severity findings in the job output without blocking the PR.
- Keep
license-check: falsefor now; license allow/deny policy needs a separate decision. - Do not post PR comments from the action. Reviewers read the job summary and logs, and branch protection can require the
dependency reviewcheck once the repo ruleset is updated.
The workflow complements, but does not replace, repository-level Dependabot alerts. Dependabot alerts must be enabled in the GitHub repository security settings so newly disclosed vulnerabilities in already-merged dependencies surface outside PR review.
When iterating on workflow YAML or hunting a leaked secret without waiting for CI:
# actionlint
ACTIONLINT_INSTALLER_SHA=914e7df21a07ef503a81201c76d2b11c789d3fca
ACTIONLINT_VERSION=1.7.12
bash <(curl -fsSL "https://raw.githubusercontent.com/rhysd/actionlint/${ACTIONLINT_INSTALLER_SHA}/scripts/download-actionlint.bash") "${ACTIONLINT_VERSION}"
./actionlint -color
# zizmor (requires uv: https://github.com/astral-sh/uv)
uvx zizmor==1.24.1 .
# gitleaks (requires the gitleaks binary: https://github.com/gitleaks/gitleaks)
gitleaks detect --source . --redact
# dependency-review has no direct local equivalent; it depends on GitHub's
# pull-request dependency graph comparison API. Use npm audit locally for
# a coarse npm-only check.
npm audit --audit-level=highThese are not bundled into npm run verify on purpose — see the verify gate doc for the rationale.
zizmor runs twice in the workflow:
- Auditor persona, SARIF output — uploads everything (including pedantic findings) to the Security tab so reviewers can see the full picture. This step is
continue-on-errorso the SARIF file always lands. - Default persona, gate — fails the build on
unpinned-usesand any other default-persona finding. The auditor persona is reserved for SARIF visibility so reviewers see the full picture without making the build flap on stylistic findings.
Tighten the gate to --persona=auditor once every pedantic finding has been resolved.
Every action call in this repo is pinned to a commit SHA with the human-readable version comment alongside, e.g.:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2Pinning to SHA is a hard requirement enforced by zizmor's unpinned-uses rule. To bump an action:
- Find the new tag's commit SHA (
gh api repos/<owner>/<repo>/git/refs/tags/<tag>; if the object type istag, dereference it viagh api repos/<owner>/<repo>/git/tags/<sha>). - Update the
@<sha>and the trailing# <version>comment in the same edit. - Verify the workflow still parses (
actionlint) and is happy with zizmor.
SHA bumps are automated by Dependabot — see ci-automation.md.
Pinned tool installers follow the same review habit even when they are not
GitHub Actions uses: references. actionlint.yml pins both the upstream
installer script commit and the downloaded actionlint release version.
zizmor.yml pins the PyPI package version used by uvx; bump it in the
workflow and the local-equivalent command above in the same PR.
When this template is adopted into a real project:
- Keep all three workflow files unchanged unless you have a stronger internal equivalent.
- If the project is hosted in a GitHub organisation, gitleaks may require a
GITLEAKS_LICENSEsecret — check the gitleaks-action docs for current terms. - Wire the SARIF tab in the GitHub UI: zizmor findings show under the repo's Security → Code scanning view once the first run completes.
- Treat any new finding as a real defect — do not silence at the rule level without an ADR.