A scaffolder that makes a new repository OSS-ready — governance, security policy, CI, and documentation — from the first commit.
Quick Start • What You Get • Profiles • Filling Docs In • OpenSSF • Contributing
Every project ends up needing the same twenty files: a SECURITY.md nobody wants to write twice, a
CI pipeline, a threat model, an ADR directory, a Makefile whose targets mean the same thing in
every repository. This repository holds that layer once, as a set of templates plus a scaffolder
that renders them into a new project.
# A new project
make new TARGET=../MyNewTool PROFILE=python
# Preview without writing anything
make dry-run TARGET=../MyNewTool PROFILE=rust
# Add the missing layer to a repository that already exists (never overwrites)
make new TARGET=../ExistingRepo PROFILE=generic
# How close is any repository to the badge?
make audit REPO=../TransparentTorProxyThe scaffolder prompts for the project identity, or takes it from flags:
./scripts/bootstrap.sh --target ../MyNewTool --profile python --non-interactive \
--set PROJECT_NAME="My New Tool" --set PROJECT_SHORT=MNT \
--set PROJECT_DESC="One line that says what it does"Answers are saved to <target>/.template/answers.env so the same values can be replayed later with
--answers.
A generated repository contains, all rendered with the project's own name, owner, and license:
| Area | Files |
|---|---|
| Governance | GOVERNANCE.md, MAINTAINERS.md (roles, access, bus factor, offboarding), CODE_OF_CONDUCT.md, CONTRIBUTING.md (DCO, coding standards, test policy, Actions hardening), SUPPORT.md |
| Security | SECURITY.md (private disclosure, response SLA, scope), SAST_POLICY.md, SCA_POLICY.md, SECRETS_POLICY.md, DYNAMIC_ANALYSIS_POLICY.md, HALL_OF_FAME.md, docs/security-assessment.md (STRIDE skeleton) |
| Documentation | docs/architecture.md, docs/interfaces.md, docs/verification.md, docs/documentation-policy.md (which file to update for which change), ADR directory with a template, and a Diátaxis-structured MkDocs Material site |
| CI/CD | ci.yml, codeql.yml, scorecard.yml, dependency-review.yml, dco.yml, fuzzing.yml, docs.yml, stale.yml, release.yml (SBOM + Sigstore signing + checksums) |
| Repository hygiene | .gitignore, .gitattributes, .editorconfig, .markdownlint.yaml, .codacy.yml, .env.example, dependabot.yml, CODEOWNERS, issue and pull request templates, CITATION.cff |
| Build | A Makefile that includes make/*.mk fragments, with the same target names in every project |
The generated Makefile splits into language-agnostic orchestration and one language fragment:
Makefile identity variables, then includes:
├── make/common.mk help, setup, install-hooks, verify, todo, clean
├── make/quality.mk lint, lint-shell, lint-docs, lint-secrets, format, test, audit
├── make/docs.mk docs-build, docs-serve, docs-sync, adr
├── make/release.mk build, sbom, checksums, sign, release-check, release-dry
└── make/<profile>.mk lang-setup, lang-lint, lang-test, lang-build, …
Only the last file changes between languages. make verify means the same thing in a Rust project
and a Python one, which is the point: muscle memory transfers between repositories, and so does CI.
To support a language that has no profile yet, copy template/generic/ and fill in the nine
lang-* targets. make check-profiles verifies that every profile implements the full contract.
| Profile | Toolchain | Ships |
|---|---|---|
python |
Ruff, mypy, pytest, Hypothesis, pip-audit, hatchling | pyproject.toml with security lints (flake8-bandit), coverage config, bump-my-version, package skeleton, fuzz targets with tuned Hypothesis profiles |
node |
TypeScript, ESLint (type-aware), Prettier, Vitest | Strict tsconfig, flat ESLint config, coverage, build/pack targets |
rust |
Clippy (pedantic), rustfmt, proptest, cargo-audit | Cargo.toml with unsafe_code = "forbid", overflow checks in release, MSRV job in CI |
generic |
Yours | A documented no-op implementation of the target contract, ready to fill in |
Every profile was generated and then actually run: make lint, make test, and make build pass
out of the box in each one.
A scaffold that demands twenty finished documents on day one gets abandoned on day one. Instead, every section that needs project-specific content carries a marker:
<!-- TODO(template): state the goal in two or three sentences, and the non-goals immediately after. -->The markers are not "fill me in" stubs — each one says what belongs there and why, so the document can be completed incrementally as the project takes shape:
make todo # every remaining section, with file and linemake release-check refuses to tag a release while README.md or SECURITY.md still contains
markers, so the documents that users read first cannot ship unfinished.
make audit REPO=../MyProjectThe audit maps OpenSSF Best Practices criteria onto concrete artifacts and reports each one as satisfied, missing, or needing a human decision — bus factor and coverage thresholds are never assumed to pass. Findings come with the remediation.
A freshly generated repository satisfies 100% of the automatically checkable passing and silver criteria, on every profile; what remains is the project-specific content the markers ask for, plus the gold criteria that depend on having more than one maintainer.
The audit also reads the workflows themselves, which is where the expensive mistakes live: a
pull_request_target trigger combined with a checkout of the pull request's own code (Scorecard's
Dangerous-Workflow finding), untrusted input interpolated into a run: block, a missing
permissions: block, an action pinned to a movable tag. pull_request_target without an untrusted
checkout is reported as needing a human decision rather than quietly passed — it is legitimate, and
it is one edit away from not being.
├── Makefile # scaffolder entrypoint — make help
├── scripts/
│ ├── bootstrap.sh # renders template/ into a target repository
│ ├── openssf-audit.sh # badge readiness report + workflow safety checks
│ ├── pin-actions.sh # refreshes the action SHA pins under template/
│ └── lib/render.sh # placeholder substitution engine
├── template/
│ ├── common/ # everything language-agnostic
│ ├── python/ node/ rust/ # language profiles
│ ├── generic/ # starting point for a new profile
│ └── licenses/ # license texts
├── tests/
│ ├── run-tests.sh # the suite — scaffolds every profile and audits it
│ └── fixtures/ # a dangerous workflow, and its safe counterpart
└── docs/
├── usage.md # full scaffolder reference
├── interfaces.md # every flag, target, and file contract
├── profiles.md # the lang-* contract, and how to add a profile
├── placeholders.md # every {{VARIABLE}} and where it comes from
└── openssf-checklist.md # criterion-by-criterion mapping
make test # end-to-end: scaffold every profile, audit what came out
make check # shellcheck + profile contract + placeholder binding
make check-placeholders # every {{VAR}} used is bound by bootstrap.sh or a profile
make check-profiles # every profile implements all nine lang-* targets
make check-pins # action pins under template/ that have fallen behindThe template is verified through its output rather than its sources: make test scaffolds all four
profiles into a temporary directory and asserts that nothing is left unrendered, that the audit
reports zero missing criteria, and that replaying answers.env reproduces the repository
byte-for-byte. CONTRIBUTING.md covers adding a check.
Substitution is done with Bash parameter expansion over a fixed key list, not sed, so values
containing slashes need no escaping and GitHub Actions expressions such as ${{ github.ref }} pass
through untouched.
Every action in every generated workflow is pinned to a commit SHA. Dependabot maintains this
repository's own pins but cannot see the copies under template/, so make pin-actions does that
job — it reads the version comment on each uses: line to decide what "newer" means.
MIT. See LICENSE.