Skip to content

Latest commit

 

History

History
167 lines (138 loc) · 7.61 KB

File metadata and controls

167 lines (138 loc) · 7.61 KB

Contributing

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.

Ways to contribute

  • 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.

Non-negotiables for every workflow PR

These are enforced by review, the self-CI ci-gate, actionlint, and zizmor. A PR that misses any of them will not be merged.

  1. 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.

  2. Least-privilege permissions. Start from permissions: {} 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.

  3. concurrency on every workflow, to cancel superseded runs, e.g. group: <workflow>-${{ github.ref }} with cancel-in-progress: true.

  4. timeout-minutes on every job. No unbounded jobs.

  5. persist-credentials: false on every read-only actions/checkout. Only opt in to credential persistence when a later step provably needs to push.

  6. No template injection. Never interpolate ${{ inputs.* }}, ${{ github.event.* }}, or any other expression directly inside a run: block. Pass untrusted values through env: and reference them as shell variables:

    - env:
        VERSION: ${{ inputs.version }}
      run: |
        set -euo pipefail
        echo "building $VERSION"
        bash -c 'do-something "$VERSION"'

    zizmor will flag template-injection; treat that as a hard failure.

  7. 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.

  8. 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.

  9. Harden public/GHAS jobs explicitly. Use step-security/harden-runner as the unconditional first step with an egress policy (audit by default, block where the endpoint set is known). Do not reference it from cross-tier or private-free workflows.

Local checks

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.txt

Install 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/workflows

Every 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.

Commits and pull requests

  • 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 with git commit -s, certifying the Developer Certificate of Origin.
  • Sign your commits. main requires cryptographically signed commits (git commit -S, or git 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 a CHANGELOG.md entry under [Unreleased].

Branch protection and CI

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

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.