CI baseline
What CI and the git hooks check in a generated project, why, and what the template leaves out. A condition in parentheses is the answer that turns the piece on; a value in extras is an optional feature.
What every scaffold gets
Every rendered project has these. A piece that depends on the stack or the license answer says so.
| Piece | Where | Why |
|---|---|---|
| Pinned action digests with version comments | Every workflow | Tags are mutable; SHA digests prevent supply-chain tampering. Dependabot bumps both |
Pinned git hook SHAs with # frozen: comments |
.pre-commit-config.yaml |
Reproducible hook execution across local machines and CI. Dependabot updates both |
Minimal default permissions (contents: read or {}) |
Every workflow | Implements least privilege; elevated permissions granted per job |
concurrency with cancel-in-progress |
Non-release workflows | Cancels stale PR runs when new commits push. Excludes release, property, mutation, and copier-update workflows |
timeout-minutes on all jobs |
Every workflow | Prevents hung jobs from consuming runner minutes |
Caches saved only from main |
Caching workflows | PRs restore caches saved on main without saving new ones, preventing cache eviction within the 10 GB limit |
persist-credentials: false on checkout |
Every workflow | Prevents subsequent job steps from reusing the runner checkout token |
| Stack validation checks | python.yml, ts.yml, rust.yml, go.yml |
Executes lint, format, type-check, test, and dependency audits matching just check |
| Git hooks executed in CI | checks.yml |
Enforces hook compliance even if local contributors omit prek install |
| Conventional commit and PR title checks | committed.toml, commit-msg hook, pr-title.yml |
Enforces clean git history for squash merges to main |
| zizmor workflow auditor | Git hook and CI | Scans workflows for injection vulnerabilities, leaked secrets, and compromised actions |
| jactionlint | Git hook and CI | Validates workflow syntax, expressions, and shell commands in run: blocks |
| Dependency review | checks.yml (PRs only) |
Blocks pull requests that introduce dependencies with known security advisories |
| Cooldown period on dependency updates | .github/dependabot.yml or renovate.json5 |
Delays non-critical updates by 7 days to allow malicious releases to be detected and yanked |
| Dependency license check | python.yml (licensecheck), ts.yml (scripts/checkLicenses.mjs), rust.yml (cargo deny), go.yml (go-licenses); not with license=none |
Lists the license of each dependency that ships, not dev tooling, on every pull request and fails on one the project's license cannot take. For TypeScript, Rust and Go the allow-list follows the license answer: permissive licenses and MPL-2.0 for every project, the GPL family for a GPL or AGPL one. Python uses licensecheck's own compatibility table |
| Scheduled dependency audits | advisories.yml |
Runs weekly scans (uv audit, pnpm audit, cargo deny, govulncheck, zizmor) without commits |
| Scheduled link validation | links.yml |
Validates internal and external links weekly via lychee and reports broken URLs |
| Failure issues for scheduled runs | report-failure.yml, called by property.yml, mutation.yml, copier-update.yml, advisories.yml and the daily preview-retire.yml sweep |
Opens an issue when a scheduled run fails, or comments on the one already open, so a failure reaches the maintainer |
| Multi-version Python testing | python.yml (library kind) |
Tests packages across all supported minor versions from python_min to python_version |
| Public API break detection | python.yml (griffe, with publish_to_pypi), ts.yml (API Extractor, with publish_to_npm), rust.yml (cargo-semver-checks, Rust libraries) |
Compares the public API with the last release and fails on a break (API-break check) |
| Container security scans | docker.yml (docker stack with docker_image) |
Trivy scans images for HIGH/CRITICAL vulnerabilities; weekly scheduled rebuilds check base updates |
| Compose and shell lint | compose.yml (Compose-only repo) |
Validates the Compose files against .env.example, then runs shellcheck and shfmt on the tracked shell scripts |
Public repositories and citation
These follow the public answer, the license answer or the project kind.
| Piece | Where | Why |
|---|---|---|
| CodeQL code scanning | GitHub default setup, turned on by just repo-settings (when public) |
Finds injection and other security bugs in the code; free for public repos |
SECURITY.md |
.github/ (when public) |
Documents private vulnerability disclosure procedures, SLAs (14 days), and remediation policies |
CONTRIBUTING.md, CODE_OF_CONDUCT.md, PR template |
.github/ (when public) |
Provides contributor setup instructions, sign-off guidelines, and Contributor Covenant 3.0 |
| Issue forms | .github/ISSUE_TEMPLATE/ (when public) |
Structured templates for bugs and feature requests, validated against GitHub schemas |
| DCO sign-off check | dco.yml (when public) |
Verifies Signed-off-by: lines on fork commits; enforced on all branches with level=community. The dco-signoff commit-msg hook refuses an unsigned commit locally in every public render |
CITATION.cff, codemeta.json |
Licensed library kinds, plus licensed Python research and data-pipeline |
Maintained for Zenodo, Software Heritage, and GitHub citation. citesync verifies consistency |
| REUSE licensing check | reuse git hook (when licensed) |
Fails while a file has no copyright and license in REUSE.toml or LICENSES/ |
| OpenSSF Scorecard | scorecard.yml (when public) |
Evaluates supply-chain security practices and generates README status badges |
| FAIR software checks | fair.yml (when public with citation) |
Validates repo compliance against fair-software.eu recommendations |
What the extras add
Each piece turns on with one value in extras.
| Piece | Where | Why |
|---|---|---|
| Python tests on Windows | python.yml (python_windows extra) |
Runs the tests on windows-latest with python_version, without lint or type checks |
| Code coverage reporting | python.yml, ts.yml, codecov.yml (codecov extra) |
Uploads coverage to Codecov; flags PR regressions exceeding 2 percentage points |
| Scheduled extended property tests | property.yml (property extra) |
Runs 10,000 cases per property (or 10 minutes of Go fuzzing) weekly; excluded from PRs |
| Scheduled mutation testing | mutation.yml (mutation extra) |
Measures test resilience against injected bugs weekly; excluded from PRs |
| Pull request benchmarks | benchmarks.yml (codspeed extra) |
Runs CodSpeed instruction-count benchmarks on PRs that touch benchmarked code and on every push to main, using OIDC authentication |
| Documentation site build | docs.yml (docs_site extra) |
Builds docs/ with Zensical in --strict mode on every pull request and push to main, and deploys that build from main to GitHub Pages |
| Cold-install setup verification | cold-install.yml (cold_install extra) |
Executes README development instructions in a minimal Debian container to catch missing prerequisites. Also runs when pyproject.toml, uv.lock or .python-version change |
Web apps and sites
The web-app kind of the TypeScript stack, or a site.
| Piece | Where | Why |
|---|---|---|
| Production deploy from CI | ts.yml (web-app kind) |
Deploys the build the checks ran on to a Cloudflare Worker on each push to main, through a production environment limited to main |
| Pull request previews | ts.yml and site.yml preview jobs, preview-retire.yml (web-app kind, or a site) |
Deploys each open, ready pull request to its own Worker, and deletes it when the pull request closes, merges or turns draft |
| Static site on GitHub Pages or a Worker | site.yml (site) |
Builds site/ with just site on every pull request and push to main, and deploys that build from main to GitHub Pages, or to a Cloudflare Worker with site_host=cloudflare |
| Browser tests in Playwright container | ts.yml (web-app kind) |
Runs end-to-end tests against production builds using the pinned Playwright container |
| Automated accessibility tests (axe) | tests/e2e/a11y.spec.ts (web-app kind) |
Tests routes in light and dark modes against WCAG 2.0–2.2 Levels A and AA |
| Content-Security-Policy enforcement | public/_headers, tests/e2e/csp.spec.ts (web-app kind) |
Validates strict CSP (script-src 'self'), HSTS, and frame protection without inline scripts |
| Viewport reflow validation | tests/e2e/reflow.spec.ts (web-app kind) |
Verifies responsive layout at 320 px viewport width without horizontal scrollbars (WCAG 1.4.10) |
| Accessibility statement checks | tests/e2e/accessibility-statement.spec.ts (web-app kind) |
Validates statement sections, contact details, and valid status |
| Legal page tests | tests/e2e/legal-pages.spec.ts (web-app kind) |
Ensures all legal pages render properly, are linked from every route in LEGAL_LINKED_FROM, and link back home |
| Clean state on initial load | tests/e2e/privacy.spec.ts (web-app kind) |
Verifies routes set no cookies, create no storage beyond what its ALLOWED lists, and contact only declared origins on load |
| Bundle size budget enforcement | scripts/checkBundleBudget.mjs (web-app kind) |
Fails builds if gzipped JavaScript bundles exceed declared size limits |
Publishing and release
| Piece | Where | Why |
|---|---|---|
| Publication provenance | Release workflows | Generates Sigstore attestations and provenance bundles for PyPI, npm, and GHCR artifacts |
| Tag-based release publishing | Release workflows | Restricts publishing to version tags (vX.Y.Z) signed with a key in .github/allowed_signers on the default branch; disables manual workflow dispatch |
Rendered workflows default to contents: read or {}. Docker containers execute as non-root users (Docker images). |
[!NOTE] GitHub disables scheduled workflows in a public repository after 60 days without activity. Push a commit or re-enable them in the Actions tab.
CI skips hooks that require the local runtime environment (ruff, ty, biome, tsc, cargo fmt, cargo clippy, golangci-lint), as stack workflows run them directly. It also skips betterleaks, which operates only on staged files.
What is left out, and why
| Omitted | Rationale | When to add |
|---|---|---|
| In-workflow Renovate | Requires private GitHub App and secrets; Dependabot works out of the box | When advanced Renovate configuration options are required |
| release-please | Creates release PR churn on every push; ill-suited for tag-driven research releases | When release frequency justifies automated changelog PR generation |
| Repository history secret scan | GitHub secret scanning covers public repos; local hooks scan staged commits | For private repositories without GitHub Advanced Security |
| Docker base images pinned to SHA | The base image tag matches the python_version answer |
In production deployments requiring byte-exact base images |
| Unified "CI result" job | Stack-specific workflows are small enough to list directly in branch rules | When workflow matrices expand beyond manual ruleset configuration |
| CODEOWNERS | Unnecessary for single-maintainer repositories | When adding multi-maintainer teams with distinct ownership areas |
| Multi-browser Playwright matrix | Triples CI test duration; modern engines rarely exhibit isolated rendering bugs | When using browser-specific Web APIs across rendering engines. Run Firefox and WebKit on pushes to main and weekly, with the deploy waiting on them, not on PRs: in credit-matrix they took 65% of a PR run's minutes |
| Dynamic CSP inline script hashing | Template enforces strict script-src 'self' without inline scripts |
Not needed under current zero-inline-script policy |
| Offline service workers | Aggressive caching complicates rollback of broken deployments | When offline availability is an explicit user requirement |
Path filters
A path filter skips work a pull request cannot affect. It never applies to pushes to main or to scheduled runs, which refill the caches and give CodSpeed its base results. The one exception is docker.yml with a nested python_dir, which filters its push trigger too: a push that changes no build context has no image to build.
- Required checks: a workflow that a ruleset requires takes no workflow-level
paths:. A pull request it skips never reports the check, so the merge waits for it (Protecting main, "Requires pull requests"). - Optional single-purpose workflows filter their
pull_requesttrigger by path:benchmarks.yml,cold-install.yml, anddocker.ymlwhen the image builds from a subdirectory. - Repos with independent parts (a web app beside a Python pipeline, each with jobs that take minutes) add one
changesjob that lists the PR's files and sets an output per part. Each part's jobs run withif: ${{ !cancelled() && needs.changes.outputs.<part> != 'false' }}, so a skipped or failedchangesjob runs everything, and a skipped required job counts as passed. A matrix job is the exception: skipped by itsif, it reports no check under its legs' names, so the ruleset requires a small non-matrix job that needs it and passes when it succeeded or was skipped. Files outside every part, such as workflows and root config, select all parts. A job further down the chain checksneeds.<job>.resultexplicitly: by default a failedchangesjob would skip it, though the jobs between them run. - Single-stack repos use no filter: the jobs run in parallel and a skip saves little.
Caveats
- Dependency review: Requires GitHub dependency graph. If disabled on a new repository, enable Dependabot alerts in repository settings (
gh api -X PUT repos/OWNER/REPO/vulnerability-alerts). Skipped on private repositories without GitHub Advanced Security. - zizmor: Online audits (impostor commits, action advisories) require a GitHub token. CI passes the read-only
github.token; locally, setGH_TOKENto run online checks. - Accessibility scope: Automated axe and reflow tests apply exclusively to the
web-appkind. CLI programs receive a staticdocs/accessibility.mdspecification. - Local network bindings: Local development servers bind
127.0.0.1(just serve, Docker Compose port mappings, Vite dev servers). Deployed services communicate over HTTPS. - Data pipeline network access:
data-pipelineprojects fetch live sources in CI. The pipeline retries transient network errors three times over 35 seconds before failing.