Thanks for helping harden the NDDev CI/CD supply chain. This repository ships reusable GitHub Actions workflows consumed across the estate by full commit SHA. A workflow here is a security-critical dependency for every caller, so the bar for changes is deliberately high.
By contributing you agree to the Code of Conduct and to license your contribution under AGPL-3.0-or-later.
- Security vulnerabilities are never contributions. Do not open a public issue or PR. Report privately via GitHub Security Advisories — see SECURITY.md.
- Propose a new reusable workflow or capability — open a Workflow request first. Describe the capability, which tier(s) it targets (public / private-free / private-paid), the underlying tools, the motivation, and the security considerations. Agreeing on scope and tier before code avoids wasted work.
- Report a bug in an existing workflow — use the Bug report form.
- Flag a pinned tool/action update — use the Tool update form (Dependabot already batches most of these).
- Propose a hardening improvement — use the Security hardening form. This is for defense-in-depth ideas, not vulnerabilities.
- Fix a docs gap — use the Docs gap form.
These are enforced by review, the self-CI ci-gate, actionlint, and zizmor.
A PR that misses any of them will not be merged.
-
Full-SHA action pins with version comments. Every
uses:of a third-party action pins a 40-character commit SHA followed by a version comment, e.g.:uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
No tags, no branches, no floating major versions. Dependabot bumps the SHA.
-
Least-privilege
permissions. Start frompermissions: {}at the top level and grant only the exact scopes a job needs (e.g.contents: read,security-events: write). Never rely on the default token scope. -
concurrencyon every workflow, to cancel superseded runs, e.g.group: <workflow>-${{ github.ref }}withcancel-in-progress: true. -
timeout-minuteson every job. No unbounded jobs. -
persist-credentials: falseon every read-onlyactions/checkout. Only opt in to credential persistence when a later step provably needs to push. -
No template injection. Never interpolate
${{ inputs.* }},${{ github.event.* }}, or any other expression directly inside arun:block. Pass untrusted values throughenv:and reference them as shell variables:- env: VERSION: ${{ inputs.version }} run: | set -euo pipefail echo "building $VERSION" bash -c 'do-something "$VERSION"'
zizmorwill flagtemplate-injection; treat that as a hard failure. -
Separate paid and private-free contracts. A workflow available to the private-free tier must not reference a paid action or GitHub feature. Split SARIF/no-SARIF workflows and public/GHAS runtime hardening at the file boundary; never try to disable an action with lifecycle hooks through a step-level boolean condition.
-
Reusable contract. Every workflow intended for callers must declare
on: workflow_call:and document its inputs in a header comment. The self-CI contract job verifies this. -
Harden public/GHAS jobs explicitly. Use
step-security/harden-runneras the unconditional first step with an egress policy (auditby default,blockwhere the endpoint set is known). Do not reference it from cross-tier or private-free workflows.
Run these before opening a PR (they mirror the self-CI ci-gate):
# Lint all workflow YAML
actionlint
# Workflow security analysis. Pedantic is what ci-gate runs, and the token is
# not optional: without it zizmor skips its online audits and reports a false
# "No findings".
GH_TOKEN=$(gh auth token) uvx zizmor@1.26.1 --persona pedantic --min-severity low .github/workflows
# Complete repository contract, catalog, example, and generated-doc checks
.venv/bin/python -I -B scripts/check_python_syntax.py
.venv/bin/python -I -B scripts/check_python_execution_contract.py --launch validate_all.py --Install validator dependencies with the hash-locked file:
python3.13 -I -B -m venv --copies .venv
uv pip install --python .venv/bin/python --require-hashes -r requirements-ci.txtInstall the tools locally with:
# actionlint — pinned version, checksum-verified, matching actionlint.yml's
# `actionlint_version` default. Read the pin from the workflow if they disagree.
curl -fsSL -o /tmp/actionlint.tar.gz \
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz
echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 /tmp/actionlint.tar.gz" | sha256sum -c -
tar -xzf /tmp/actionlint.tar.gz -C ~/.local/bin actionlint
# zizmor — run the version and persona CI runs, not whatever is on PATH. The pin is
# zizmor-sarif.yml's `zizmor_version` default.
GH_TOKEN=$(gh auth token) uvx zizmor@1.26.1 --persona pedantic --min-severity low .github/workflowsEvery command here uses uv or a checksum-verified download on purpose. The
estate policy forbids pip, pipx, npm/npx, and mutable version resolution
such as go install ...@latest; CI itself installs with uv pip install --system --require-hashes. A setup guide that told contributors to use a
forbidden installer, while CI used a different one, is how three descriptions of
one command ended up disagreeing.
- Conventional Commits. Subject lines follow
type(scope): summary(e.g.feat(workflows): add sbom diff reusable,fix(zizmor): pin analysis to full sha). Keep the subject under 100 characters. - Sign off every commit (DCO). Add a
Signed-off-by:trailer withgit commit -s, certifying the Developer Certificate of Origin. - Sign your commits.
mainrequires cryptographically signed commits (git commit -S, orgit config commit.gpgsign true). Unsigned commits are rejected. - Sole authorship. Do not add
Co-Authored-By:trailers. - Fill in the pull request template completely, including the permissions diff and threat-model note when a change touches workflow behavior or token scopes.
- Update the README capability table (and
docs//catalog/where present) and add aCHANGELOG.mdentry under[Unreleased].
main is protected: signed commits, required review plus code-owner review,
linear history, no force-push or deletion, and the required ci-gate status
check. All workflow files are owned by @rldyourmnd
via CODEOWNERS, so a maintainer review is always required.
Open PRs against main from a topic branch; the ci-gate check (contract +
actionlint + zizmor) must be green before merge.
Releases are tag-driven (MAJOR.MINOR.PATCH) and publish an SPDX SBOM,
SHA256SUMS, and SLSA build-provenance attestations. Contributors do not tag
releases; the maintainer cuts them.