Scaffolder Reference
Full reference for scripts/bootstrap.sh. For the short version, see the
README.
Synopsis
| Option | Meaning |
|---|---|
--target <dir> |
Destination repository. Created if missing. Required. |
--profile <name> |
python, node, rust, or generic. Prompted for if omitted. |
--answers <file> |
Load values from a previously generated answers.env. |
--set KEY=VALUE |
Override one variable. Repeatable. Wins over --answers. |
--merge |
Skip files that already exist. This is the default. |
--force |
Overwrite existing files. |
--dry-run |
Print what would be written; change nothing. |
--non-interactive |
Never prompt. Fails if a required value has no default. |
The three ways to run it
A new project
You are prompted for the identity, ownership, and release values. Defaults come from git config
and from the target directory name, so pressing Enter through the whole prompt sequence produces a
sensible repository.
An existing repository
make dry-run TARGET=../ExistingRepo PROFILE=generic # look first
make new TARGET=../ExistingRepo PROFILE=generic # then apply
Existing files are never touched. The run ends with a list of what it skipped, so you can diff the
template's version against yours and merge by hand where it is worth it. Only .gitignore is
appended to rather than skipped, and only once — the profile block is marked with a
# Profile: <name> header that the script looks for before appending again.
Two consequences are worth knowing before you run it:
The source skeleton is written all or not at all. A profile ships a small package with a CLI
entry point plus the tests that import it. If the target already has source under the same name,
the whole skeleton — source, tests/, fuzzing/ — is skipped, because writing only the tests
would leave them importing a module the scaffolder deliberately did not write. The paths that make
up the skeleton are declared per profile in SKELETON_PATHS.
The Makefile is skipped, so the make/*.mk fragments land inert. The fragments are written,
but your existing Makefile does not include them and nothing changes until you reconcile the two.
The migration that works:
# 1. Keep the identity block and the include chain from the template's Makefile.
# template/common/Makefile is the reference; fill in your own project values.
# 2. Move every target that has no equivalent in the contract into make/project.mk,
# which the generated Makefile already includes.
# 3. Extend a contract target by adding a prerequisite, never by redefining a recipe:
clean: clean-packages # runs after the fragment's clean, no override warning
integration-test: docker-suite # adds to the contract target rather than replacing it
Redefining a target that a fragment already defines makes GNU Make print
warning: overriding recipe for target and silently discard the fragment's version. Adding a
prerequisite with no recipe is the supported way to extend one.
Expect the contract's gates to be stricter than what the repository had. lint runs the type
checker as well as the formatter, and verify is lint test audit; a project whose previous
lint was formatter-only will surface a backlog on the first run. That backlog is a finding, not a
reason to loosen the fragment.
Replaying a previous run
Every run writes <target>/.template/answers.env. After the template gains a new file, re-running
with those answers adds it without re-asking anything:
Add --force to also refresh files that already exist — review the diff afterwards, since that
overwrites local edits.
What happens during a run
- The profile's
profile.envis sourced, supplying language-derived values (PRIMARY_LANGUAGE,CODEQL_LANGUAGE,DEPENDABOT_ECOSYSTEM, …). - Missing values are prompted for, or taken from defaults in non-interactive mode.
- Derived values are computed (
DOCS_URL,CRATE_NAME,PY_TARGET,COPYRIGHT_YEAR, …). template/common/is rendered into the target, thentemplate/<profile>/.- The profile's
.gitignore.appendis appended to the base.gitignore. - The license text for
LICENSE_IDis rendered fromtemplate/licenses/. - Answers are recorded, and the run reports unresolved placeholders and remaining
TODO(template)markers.
Filenames are rendered too: the directory __PKG__/ in the Python profile becomes the real package
directory, and any {{VAR}} in a path is substituted.
Licenses
template/licenses/ ships the MIT text. For any other SPDX identifier the scaffolder warns and
points at https://spdx.org/licenses/; drop the text in as LICENSE yourself, or add it to
template/licenses/<SPDX-ID>.txt so future runs pick it up automatically.
After scaffolding
cd ../MyNewTool
git init && git add -A && git commit -s -m "chore: scaffold from TemplateRepository"
make setup # create the environment and install dev dependencies
make install-hooks # lint + tests run before every commit
make todo # what is left to write
make verify # the gate that CI also runs
Then, on GitHub:
- Enable private vulnerability reporting (Settings → Code security).
SECURITY.mdalready points at it. - Enable secret scanning and push protection.
- Protect
main: require a pull request, a passing CI check, and linear history. - Register the project at https://www.bestpractices.dev/en/projects/new and put the numeric ID in
the README badge URL, replacing the
0placeholder. - Consider pinning third-party Actions to commit SHAs — Dependabot's
github-actionsecosystem is already configured to keep them current: