A drop-in third-party-attribution pipeline. A dispatcher reads a [[blocks]]
array from attribution.toml and routes each block to an ecosystem-specific
driver — Rust (cargo-about),
Node (license-checker), Go
(go-licenses), Python
(pip-licenses), and
hand-maintained bundled binaries — splicing each driver's output between
BEGIN/END marker comments in a target markdown file (typically
ACKNOWLEDGEMENTS.md). Hand-curated content above, between, and below the
markers is preserved verbatim.
This repository is a read-only mirror. The canonical source lives in a private upstream repository and is force-pushed here whenever it changes. Direct commits to
mainhere will be overwritten on the next sync. Please open issues againsteddacraft/anvil— or, if you don't have access, file an issue here and the maintainer will mirror it upstream.
As a tracked subtree (recommended — easy to pull updates). Use a per-kit prefix so this kit is its own independently tracked subtree:
git subtree add --prefix tools/starters/acknowledgements \
https://github.com/eddacraft/acknowledgements-starter.git main --squashDon't shorten this to
--prefix tools/starters.git subtree addmakes the prefix directory the tracked subtree as a whole — using the parent would lock the entiretools/starters/directory to this one kit, and a second starter (logging-starter, etc.) could not be added at the same parent. Keep one subtree per kit.
To pull future updates:
git subtree pull --prefix tools/starters/acknowledgements \
https://github.com/eddacraft/acknowledgements-starter.git main --squashOr just cp -r the directory in if you don't want subtree tracking.
main always holds the latest kit and is force-pushed on every change — adopt
or pull from main (above) to track the bleeding edge.
To pin a specific, immutable version instead, adopt or pull from a release
tag (vX.Y.Z):
git subtree add --prefix tools/starters/acknowledgements \
https://github.com/eddacraft/acknowledgements-starter.git v1.0.0 --squashRelease tags are append-only — a published vX.Y.Z never changes. Each release
has a GitHub Release with notes drawn from CHANGELOG.md;
watch this repository's releases to be notified when the kit updates, and
see the
Releases page
for the version history. Versions follow SemVer: a major
bump signals a breaking change to the kit's contract.
The starter kit grew out of the Rust-CLI attribution pipeline shipped with the
eddacraft/anvil project. The multi-ecosystem roadmap (CycloneDX intermediate,
bundled-binaries support, multi-block markers) is tracked in that repository.
This mirror is purely the kit; consult anvil for the "why".
The remainder of this file is the kit's contract documentation, mirrored verbatim from upstream:
A drop-in third-party-attribution pipeline. The generator is a dispatcher
that reads a [[blocks]] array from attribution.toml and routes each block to
an ecosystem-specific driver under drivers/<ecosystem>.sh. Each driver emits
markdown that gets spliced between BEGIN/END marker comments in a target
markdown file (typically ACKNOWLEDGEMENTS.md). Hand-curated content above,
between, and below the markers is preserved verbatim.
Shipped drivers: Rust
(cargo-about), Node
(license-checker), Go
(go-licenses), Python
(pip-licenses), and bundled
binaries (a hand-maintained inventory, no external tool). Existing consumers
with a legacy flat [rust] config keep working unchanged via a back-compat shim
(see "Configuration reference" below).
The kit is the canonical home of the generator. To adopt it in another repo,
copy this directory wholesale and edit one file (attribution.toml) — no script
edits required.
| File | Purpose |
|---|---|
generate-acknowledgements.sh |
Dispatcher: parses config, loops blocks, invokes drivers, splices output |
drivers/ |
Ecosystem driver scripts (rust.sh, node.sh, go.sh, python.sh, bundled-binaries.sh) |
bundled-binaries.toml.example |
Inventory template for the bundled-binaries driver (non-package-manager binaries) |
expand-licences.sh |
Single-source allow-list expander |
attribution.toml.example |
Annotated template for the consumer-side config |
about.toml.template |
cargo-about config template (licence allow-list etc) |
about.hbs.template |
cargo-about handlebars render template |
templates/go-licenses.tmpl |
go-licenses report template for the Go driver (rows only; driver sorts) |
licences.node-allow.txt.template |
Marker scaffolding for the Node driver's allow-list file |
licences.go-allow.txt.template |
Marker scaffolding for the Go driver's allow-list file |
licences.python-allow.txt.template |
Marker scaffolding for the Python driver's allow-list file |
ACKNOWLEDGEMENTS.md.template |
Bootstrap target file with markers in place |
ci-freshness.yml.snippet |
GitHub Actions freshness-gate job |
tests/ |
Self-tests pinning the kit's invariants |
README.md |
This file (the marker-splice contract) |
-
Copy the kit. Use a per-kit prefix so it becomes its own independently tracked subtree (each starter kit you adopt should get its own subdirectory at its own subtree prefix — using the parent
tools/starterswould lock the entire parent to one kit):git subtree add --prefix tools/starters/acknowledgements \ <upstream> main --squash
Or just
cp -rthe directory in if you don't want subtree tracking. -
Bootstrap the always-needed files (one-off, then commit):
cp tools/starters/acknowledgements/attribution.toml.example attribution.toml cp tools/starters/acknowledgements/ACKNOWLEDGEMENTS.md.template ACKNOWLEDGEMENTS.md
Then copy the per-driver config for each ecosystem you ship. For Rust:
cp tools/starters/acknowledgements/about.toml.template about.toml cp tools/starters/acknowledgements/about.hbs.template about.hbs
The Node, Go, and Python drivers use allow-list files (
licences.<eco>-allow.txt.template) instead; the bundled-binaries driver usesbundled-binaries.toml.example. See the per-driver block reference below for the files each ecosystem needs. -
Edit
attribution.toml: declare one[[blocks]]entry per ecosystem you ship, pointing each block at the manifest it attributes (a Rust block'smanifest_pathat the shipping binary'sCargo.toml— usuallycrates/your-cli/Cargo.tomlrather than the workspace root, so dev-only deps stay out — a Node block's at itspackage.json, and so on). The per-driver reference lists each driver's keys. -
Tune each declared driver's licence policy — e.g.
about.toml'sacceptedlist andtargetsfor Rust, or the allow-list files for Node, Go, and Python. -
Generate:
tools/starters/acknowledgements/generate-acknowledgements.sh
-
Wire CI: drop
ci-freshness.yml.snippetinto your existing workflow.
The template at ACKNOWLEDGEMENTS.md.template is a structural scaffold, not a
finished file. After copying it to your repo root, replace the {{PLACEHOLDER}}
tokens and prune the example sections to fit your stack.
All placeholders use {{DOUBLE_BRACES}} so they are easy to grep and replace.
None of them are interpreted by the generator — they are plain text the template
author left for you to fill in.
| Placeholder | Replace with |
|---|---|
{{PROJECT_NAME}} |
Display name of your project, e.g. Anvil |
{{PROJECT_BINARY}} |
The shipping binary or package name, e.g. anvil |
{{GENERATOR_TOOL}} |
The upstream tool that produces the auto-generated block, e.g. cargo-about, license-checker, go-licenses, or pip-licenses |
{{GENERATOR_TOOL_URL}} |
Upstream URL for the tool you use, e.g. https://github.com/EmbarkStudios/cargo-about or https://github.com/davglass/license-checker |
{{LOCKFILE_NAME}} |
The lockfile the attribution derives from, e.g. Cargo.lock, pnpm-lock.yaml, go.sum, or requirements.txt |
{{GENERATOR_SCRIPT_PATH}} |
Path to the generator script as it appears in your repo, e.g. tools/starters/acknowledgements/generate-acknowledgements.sh |
A one-shot sed works for most of these. This Rust example can be adapted to
whichever driver owns your generated block:
sed -i \
-e 's|{{PROJECT_NAME}}|Anvil|g' \
-e 's|{{PROJECT_BINARY}}|anvil|g' \
-e 's|{{GENERATOR_TOOL}}|cargo-about|g' \
-e 's|{{GENERATOR_TOOL_URL}}|https://github.com/EmbarkStudios/cargo-about|g' \
-e 's|{{LOCKFILE_NAME}}|Cargo.lock|g' \
-e 's|{{GENERATOR_SCRIPT_PATH}}|tools/starters/acknowledgements/generate-acknowledgements.sh|g' \
ACKNOWLEDGEMENTS.mdThe template ships four illustrative subsections (Language and tooling, Testing
and quality, Build and CI, Developer environment) with two Project A /
Project B bullets each. They exist to show the structure — categorised ###
subsections, bullet list with link-reference style, link references grouped
immediately after each list.
Treat them as a starting point:
- Keep the categories that fit your stack.
- Rename categories if the labels don't match (e.g.
Monorepo and TypeScript toolinginstead ofLanguage and tooling). - Delete sections you don't use — there's no requirement that any specific category exists.
- Add sections for ecosystems the template doesn't anticipate (e.g.
Infrastructure,Documentation,Design).
The generator does not police the shape of the hand-curated region; it only preserves it verbatim.
Two things in the template are load-bearing for the generator:
-
The marker pair at the bottom:
<!-- BEGIN AUTO-GENERATED --> <!-- END AUTO-GENERATED -->
Each must appear exactly once on a line of its own. If you customise the marker text via
[project].marker_begin/marker_endinattribution.toml, update both the template and the config together. -
The order: BEGIN must precede END. The generator splices content between them; if they're swapped or duplicated, the marker-count gate fails.
Everything else — heading levels, prose, link styles, the intro paragraph — is yours to edit.
Run the generator once to populate the auto-generated block:
tools/starters/acknowledgements/generate-acknowledgements.shThen commit both the customised ACKNOWLEDGEMENTS.md and (if applicable) the
updated attribution.toml.
This section is the canonical reference for the kit's invariants. The generator and any downstream consumer of the output rely on every rule below.
The target markdown file must contain exactly one BEGIN marker and exactly one END marker, on lines of their own:
<!-- BEGIN AUTO-GENERATED -->
<!-- END AUTO-GENERATED -->The default markers are HTML comments so the file remains valid markdown and the
markers don't render in viewers. Marker text is overridable per project via
[project].marker_begin / [project].marker_end in attribution.toml (e.g.
for projects that grow multi-block markers).
The generator matches markers via literal substring containment, not regex, so the marker text need not be regex-safe.
Running the generator twice in a row against unchanged driver inputs produces a
byte-identical file the second time. The freshness gate (--check) relies on
this: it regenerates into a temporary file and diffs against the on-disk copy.
Idempotency requires:
- Each driver's render output is deterministic for its manifest or lockfile, template, and config. Pin external scanner versions in CI to keep this true across machines.
- Hand-edited content above the BEGIN marker and below the END marker is never rewritten. The splice loop emits those regions verbatim.
The generator never writes the target file in place. It:
- Generates each declared driver's output to a
mktempfile. - Splices each output between that block's markers in a working target temp.
mvs the completed target over the on-disk file only after all blocks succeed.
A failure mid-run leaves the target untouched. Combined with the empty-output guard (next), this prevents a partial generation from silently clobbering content.
Each ecosystem driver owns its own strictness checks before render. A disallowed licence, missing required field, missing tool, missing manifest, or invalid inventory exits non-zero and leaves the on-disk target byte-identical. The per-driver reference below names the exact command or validation rule for each ecosystem.
The Rust driver additionally invokes cargo about generate --fail, so a
workspace crate missing both license and license-file in its Cargo.toml
causes a hard error rather than a silent warning. The fixture test under
tests/strict-license-field.sh pins that Rust-specific contract.
Driver allow-lists are generated from a single canonical licences.toml at the
repo root. The kit's expand-licences.sh reads licences.toml and rewrites the
Rust about.toml, Rust deny.toml, and Node / Go / Python allow-list files
between BEGIN/END marker comments — the same splice pattern the acknowledgements
generator uses on ACKNOWLEDGEMENTS.md. CI runs the expander in --check mode
so drift between licences.toml and any generated consumer fails the build.
To add or remove a licence: edit licences.toml, run
tools/starters/acknowledgements/expand-licences.sh, and commit licences.toml
with every generated allow-list file that changed. The schema (single-line
strings only) is documented at the top of licences.toml itself.
The fixture test under tests/licences-drift.sh walks three scenarios — clean
expand → check passes, new licence in source → drift detected, hand-edit in
consumer → drift detected — so a regression that loosens the matcher is caught
in CI.
If any driver produces a zero-byte file (e.g. because a template path is wrong,
a manifest doesn't resolve, or an external scanner crashes silently), the
generator aborts with a non-zero exit before the mv step. The target is never
overwritten with an empty block.
Before generation runs, the generator counts BEGIN and END marker occurrences in the target. If either count is not exactly 1, it exits with a non-zero status and an actionable error. This catches:
- A target file that's missing the marker block entirely.
- A merge conflict that duplicated the markers.
- A typo introduced when adding a new block.
Without this gate, the splice loop would no-op silently and --check would
falsely report "all good" while regeneration never happened.
| Exit | Meaning |
|---|---|
| 0 | Success / no drift. Safe to merge. |
| 1 | Drift detected, missing markers, empty output, missing tool, missing config, or other recoverable failure. CI should fail. |
| 2 | CLI argument error (mutually exclusive flags, missing argument value). Indicates an invocation bug; rerun with corrected args. |
--check is the freshness gate: it does the full generate-and-splice into a
temporary file, then diff -us against the on-disk target. Drift is reported as
a unified diff so the CI log makes the missing update obvious; the trailing
message points contributors at the fixit_command configured in
attribution.toml.
Everything above the BEGIN marker and below the END marker is permanent,
hand-curated content. The kit explicitly does not police its shape — projects
use this region for ## Thanks lists, intro paragraphs, link references, etc.
The generator treats those bytes as opaque.
attribution.toml (consumer-side, repo root) drives every project-specific
value. The dispatcher carries no hard-coded paths, markers, or fix-it strings.
Two schema shapes are accepted; they are mutually exclusive within one file. Mixing them is a hard error so silent precedence rules never apply.
Each entry declares one block. name and ecosystem are required; remaining
keys are ecosystem-specific (the driver script for that ecosystem documents what
it expects).
[project]
target_path = "ACKNOWLEDGEMENTS.md" # required
fixit_command = "tools/starters/acknowledgements/generate-acknowledgements.sh" # required
# marker_begin = "<!-- BEGIN AUTO-GENERATED -->" # optional; default shown
# marker_end = "<!-- END AUTO-GENERATED -->" # optional; default shown
[[blocks]]
name = "rust" # required; kebab-case when non-empty;
# used to suffix the block's markers
# (`<!-- BEGIN AUTO-GENERATED rust -->`)
ecosystem = "rust" # required; must match drivers/<ecosystem>.sh
manifest_path = "crates/your-cli/Cargo.toml" # ecosystem-specific (rust driver)
template_path = "about.hbs" # ecosystem-specific (rust driver)
config_path = "about.toml" # ecosystem-specific (rust driver)A repo can declare multiple blocks — typically one per shipping artefact (a
pnpm-workspace monorepo shipping a CLI + an HTTP API + a sidecar daemon declares
three entries, each pointing at its own package.json). Block names must be
unique within the file; each block gets its own per-block marker pair in the
target file.
The kit's original single-block schema. A config with [rust] and no
[[blocks]] entries is treated as if it declared a single unnamed block with
ecosystem = "rust". Markers for the unnamed block omit the name suffix
(<!-- BEGIN AUTO-GENERATED -->), keeping today's ACKNOWLEDGEMENTS.md files
byte-identical after the refactor.
[project]
target_path = "ACKNOWLEDGEMENTS.md"
fixit_command = "tools/starters/acknowledgements/generate-acknowledgements.sh"
[rust]
manifest_path = "crates/your-cli/Cargo.toml"
template_path = "about.hbs"
config_path = "about.toml"Existing consumers don't need to migrate — the shim is permanent for the
Rust-only single-block case. Migrate to [[blocks]] when you want a second
block (Node devtools attribution, bundled binaries, etc.) or when you adopt a
non-Rust driver.
All paths are resolved relative to the directory containing attribution.toml.
Absolute paths are also accepted.
Each driver under drivers/<ecosystem>.sh declares its own block-config keys.
The dispatcher passes the resolved block JSON to the driver verbatim; the driver
is the authority on which keys it requires.
| Key | Required | Description |
|---|---|---|
manifest_path |
yes | Cargo.toml to walk — typically the shipping binary's manifest, not the workspace root, to keep dev-only deps out of attribution. |
template_path |
yes | about.hbs handlebars template the driver hands to cargo about generate. |
config_path |
yes | about.toml carrying cargo-about's accepted = [...] list (auto-populated by expand-licences.sh from licences.toml). |
Strict-licence gate: cargo about generate --fail rejects workspace crates
missing a license field. Always-on; no opt-out (consumer fixes the missing
field instead).
| Key | Required | Description |
|---|---|---|
manifest_path |
yes | package.json to walk. For monorepos: one block per shipping package.json is the recommended pattern (see Monorepo guidance below). |
node_allow_path |
yes | licences.node-allow.txt — single semicolon-joined SPDX list between the kit's BEGIN/END markers. Auto-populated by expand-licences.sh from licences.toml. |
prod_only |
no (default true) |
If true, license-checker --production excludes devDependencies. Set false to include dev tooling (the Anvil-devtools path). |
exclude |
no | Semicolon-joined package@version list, forwarded raw to license-checker --excludePackages. Use to drop hoisted internal packages a workspace shim surfaces. |
Worked example:
[[blocks]]
name = "node-devtools"
ecosystem = "node"
manifest_path = "package.json"
node_allow_path = "licences.node-allow.txt"
prod_only = falseStrict-licence gate: license-checker --onlyAllow "<semi-joined SPDX list>"
runs before render; one disallowed dep exits non-zero, names the offending
package@version in stderr, and leaves the on-disk target byte-identical.
--excludePrivatePackages is always-on, so the consumer's own package and any
internal @workspace/* packages marked "private": true stay out of the
rendered block automatically.
The driver requires license-checker on PATH. Install per-project:
npm install --save-dev license-checker
# or globally: npm install -g license-checker| Key | Required | Description |
|---|---|---|
module_path |
yes | Directory of the package/binary to walk (e.g. cmd/anvil). The driver finds the enclosing go.mod and runs go-licenses from that module root. |
go_allow_path |
yes | licences.go-allow.txt — single comma-joined SPDX list between the kit's BEGIN/END markers. Auto-populated by expand-licences.sh from licences.toml. |
template_path |
no | go-licenses report template emitting | module | licence | rows (no header — the driver sorts and adds it). Defaults to the kit's templates/go-licenses.tmpl. |
Worked example:
[[blocks]]
name = "go"
ecosystem = "go"
module_path = "cmd/anvil"
go_allow_path = "licences.go-allow.txt"Strict-licence gate:
go-licenses check --allowed_licenses "<comma-joined SPDX list>" runs before
render; one disallowed dep exits non-zero, names the offending library in
stderr, and leaves the on-disk target byte-identical. The project's own main
module (from go list -m) is --ignored so it is never attributed to itself.
go.mod replace directives are honoured natively, so internal monorepo
modules need no special handling.
The rendered block carries the module import path + SPDX licence only — no
source URL. go-licenses resolves the URL over the network, which would make
--check non-deterministic across online/offline environments; the Go import
path is itself the canonical source location.
The driver requires go and go-licenses on PATH, and the module cache
populated (run go mod download first):
go install github.com/google/go-licenses@latest
go mod download| Key | Required | Description |
|---|---|---|
venv_path |
yes | A consumer-supplied pre-built virtualenv. The kit ships no installer opinions (no uv/poetry/pdm) — build the venv with your own toolchain, install your runtime deps and pip-licenses into it, then point venv_path at it. |
python_allow_path |
yes | licences.python-allow.txt — single semicolon-joined SPDX list between the kit's BEGIN/END markers. Auto-populated by expand-licences.sh from licences.toml. |
Worked example:
[[blocks]]
name = "python"
ecosystem = "python"
venv_path = ".venv"
python_allow_path = "licences.python-allow.txt"The driver runs the venv's own pip-licenses, so the attribution reflects
exactly the consumer's environment. pip-licenses self-excludes its own
dependency chain (pip-licenses/prettytable/wcwidth/…), so a venv that contains
only your runtime deps + pip-licenses renders just your dependencies. Render is
deterministic (pip-licenses --format markdown --order name).
Strict-licence gate: pip-licenses --allow-only "<semicolon-joined SPDX list>"
runs before render; one disallowed dep exits non-zero, names the offending
package, and leaves the on-disk target byte-identical.
Licence-name caveat. pip-licenses reports licence names derived from each
package's Trove classifiers / metadata, which are not always exact SPDX
identifiers (e.g. a package may report BSD License rather than
BSD-3-Clause, or MIT License rather than MIT). The allow-list expanded
from licences.toml is SPDX, so a mismatch can cause a false strict failure.
Do not work around this by adding classifier-style names (BSD License,
MIT License) to licences.toml: the spdx field there also feeds
about.toml (cargo-about) and deny.toml (cargo-deny), which require valid
SPDX identifiers — a non-SPDX value breaks those consumers. Safe options
instead:
- normalise the offending package's metadata (or pin a version that publishes a
modern SPDX
License-Expression); - enable
pip-licenses --partial-match(not enabled by the kit) soMITmatchesMIT License, if you accept the looser matching; - ignore the package in your venv composition if it is not actually shipped.
A Python-specific SPDX-alias mechanism in the expander is a possible future
enhancement; today the kit keeps licences.toml strictly SPDX.
CI provisioning (per your existing Python toolchain), e.g.:
python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt pip-licensesFor third-party binaries shipped alongside your project that are not a package manager's dependencies — OpenSSH, Mosh, FFmpeg, busybox, … The language drivers never see these; you attribute them from a hand-maintained inventory.
| Key | Required | Description |
|---|---|---|
inventory_path |
yes | bundled-binaries.toml — an array of [[binary]] entries (see bundled-binaries.toml.example). |
Inventory entry schema (name + spdx required; version, source optional;
spdx is any SPDX expression — no closed enum):
[[binary]]
name = "OpenSSH"
version = "9.6p1"
spdx = "BSD-3-Clause"
source = "https://www.openssh.com/"Worked example:
[[blocks]]
name = "binaries"
ecosystem = "bundled-binaries"
inventory_path = "bundled-binaries.toml"There is no ecosystem-specific external tool — the driver is pure bash/awk
over the inventory (it shares the dispatcher's jq requirement for parsing the
block config, but needs no licence scanner). Its "strict" step is field
validation: an entry missing name or spdx fails the gate, named, before
render. Render is a deterministic | Binary | Version | License | Source |
table sorted by name. The curator owns licence correctness (there is no upstream
scanner to cross-check). If you ship no bundled binaries, omit the block rather
than shipping an empty inventory.
The "right" attribution surface in a monorepo depends on shape. Three patterns cover almost every case:
One block per shipping artefact. A pnpm-workspace monorepo shipping a CLI +
an HTTP API + a sidecar daemon declares three [[blocks]] entries, each pointed
at its own package.json:
[[blocks]]
name = "cli"
ecosystem = "node"
manifest_path = "packages/cli/package.json"
node_allow_path = "licences.node-allow.txt"
[[blocks]]
name = "http-api"
ecosystem = "node"
manifest_path = "packages/http-api/package.json"
node_allow_path = "licences.node-allow.txt"
[[blocks]]
name = "sidecar"
ecosystem = "node"
manifest_path = "packages/sidecar/package.json"
node_allow_path = "licences.node-allow.txt"Each block gets its own marker pair in the target file. --check reports
per-block drift so a regression in one shipping package doesn't mask drift in
the others. Internal @workspace/* packages stay out automatically because they
are marked "private": true.
Workspace-wide attribution (single block). Point manifest_path at the root
package.json, accept the union of every workspace's prod deps:
[[blocks]]
name = "node"
ecosystem = "node"
manifest_path = "package.json"
node_allow_path = "licences.node-allow.txt"
prod_only = trueSimple, broad, blunt. License-checker walks the root and resolves through the
workspace's hoisted node_modules. Use this when per-package separation is not
worth the bookkeeping.
Devtools-only block (Anvil pattern). When the prod surface is non-JS but you
still want to attribute the JS tooling the repo builds with (linters,
formatters, Nx, kindling integration), set prod_only = false on a
root-manifest block:
[[blocks]]
name = "node-devtools"
ecosystem = "node"
manifest_path = "package.json"
node_allow_path = "licences.node-allow.txt"
prod_only = falseThe block then includes devDependencies. Pair with exclude if a noisy dep
keeps surfacing.
The dispatcher (generate-acknowledgements.sh) is responsible for: TOML
parsing, schema validation (mixed-schema detection, name uniqueness, ecosystem →
driver lookup), per-block marker-count gating, splicing each driver's output
between its block's markers in a working temp, and a single atomic mv over the
target at the end of the loop. Any driver exiting non-zero aborts before the
atomic mv — the on-disk target stays byte-identical.
A driver script under drivers/<ecosystem>.sh is invoked with two arguments:
drivers/<ecosystem>.sh <block-config-json> <output-temp-path>
<block-config-json> is a compact JSON object containing the block's resolved
config (absolute paths). <output-temp-path> is where the driver writes its
rendered markdown.
Each driver must:
- Preflight — verify required tool + state; actionable error on stderr; non-zero exit if anything is missing.
- Render — deterministic markdown sorted/structured by the tool's own
template. Idempotency under
--checkdepends on it. - Strict-licence check — reject disallowed or missing licences before
render (cargo-about uses
--fail; other drivers wire their equivalent). - No side effects on the splice target — write only to the
<output-temp-path>argument. Never touchtarget_pathdirectly.
Tests can swap in stub drivers via ATTRIB_DRIVERS_DIR=<dir>; production
consumers should leave the env var unset and let the dispatcher use the
kit-local drivers/ directory.
The dispatcher + driver-per-ecosystem architecture landed with the Rust driver; Node, Go, Python, and bundled-binaries drivers followed — see the per-driver block reference above. The roadmap is tracked in the upstream Anvil project:
- Java/Kotlin / Ruby / Swift drivers deferred until a real consumer needs them
Each new driver is a self-contained drivers/<eco>.sh against the driver
contract documented above; the dispatcher doesn't need to change.
This directory is the canonical source. A read-only public mirror is maintained
at
eddacraft/acknowledgements-starter
for projects (typically public ones) that can't git subtree from this private
repository.
The mirror is force-pushed by
.github/workflows/mirror-acknowledgements-starter.yml on every change to
main under this directory. Direct commits to the public mirror are overwritten
on the next sync — all edits land here.
To force a manual re-sync (e.g. after editing the workflow itself):
gh workflow run mirror-acknowledgements-starter.yml --ref main \
-f reason='why you re-synced'The reason input is recorded on the throwaway prepend-README commit and in the
GitHub step summary, so manual dispatches are auditable later.
Alongside the rolling main mirror, deliberate releases are cut as immutable
vX.Y.Z tags + GitHub Releases on the mirror, so external consumers can pin a
known-good version and be notified of updates. The kit's version lives in
VERSION and CHANGELOG.md; both travel into
the mirror.
To cut one: bump VERSION + add a CHANGELOG.md entry (PR), then tag the merge
commit acknowledgements-starter-vX.Y.Z and push it.
.github/workflows/release-acknowledgements-starter.yml does the rest. Full
procedure (incl. the mirror tag-protection pre-flight) is in
docs/runbooks/acknowledgements-starter-release.md.
Check VERSION ↔ CHANGELOG.md consistency locally with:
bash tools/starters/acknowledgements/check-version.sh