Skip to content

eddacraft/acknowledgements-starter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

acknowledgements-starter

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 main here will be overwritten on the next sync. Please open issues against eddacraft/anvil — or, if you don't have access, file an issue here and the maintainer will mirror it upstream.

Adopting the kit

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

Don't shorten this to --prefix tools/starters. git subtree add makes the prefix directory the tracked subtree as a whole — using the parent would lock the entire tools/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 --squash

Or just cp -r the directory in if you don't want subtree tracking.

Tracking latest vs pinning a release

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

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

Design history

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:

Acknowledgements starter kit

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.

What ships in this kit

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)

Adoption checklist (downstream consumer)

  1. 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/starters would lock the entire parent to one kit):

    git subtree add --prefix tools/starters/acknowledgements \
      <upstream> main --squash

    Or just cp -r the directory in if you don't want subtree tracking.

  2. 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 uses bundled-binaries.toml.example. See the per-driver block reference below for the files each ecosystem needs.

  3. Edit attribution.toml: declare one [[blocks]] entry per ecosystem you ship, pointing each block at the manifest it attributes (a Rust block's manifest_path at the shipping binary's Cargo.toml — usually crates/your-cli/Cargo.toml rather than the workspace root, so dev-only deps stay out — a Node block's at its package.json, and so on). The per-driver reference lists each driver's keys.

  4. Tune each declared driver's licence policy — e.g. about.toml's accepted list and targets for Rust, or the allow-list files for Node, Go, and Python.

  5. Generate:

    tools/starters/acknowledgements/generate-acknowledgements.sh
  6. Wire CI: drop ci-freshness.yml.snippet into your existing workflow.

Customising ACKNOWLEDGEMENTS.md

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.

Placeholders

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

"Thanks" sections

The 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 tooling instead of Language 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.

What you must not change

Two things in the template are load-bearing for the generator:

  1. 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_end in attribution.toml, update both the template and the config together.

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

After customising

Run the generator once to populate the auto-generated block:

tools/starters/acknowledgements/generate-acknowledgements.sh

Then commit both the customised ACKNOWLEDGEMENTS.md and (if applicable) the updated attribution.toml.

The marker-splice contract

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.

Marker syntax

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.

Idempotency

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.

Atomic write

The generator never writes the target file in place. It:

  1. Generates each declared driver's output to a mktemp file.
  2. Splices each output between that block's markers in a working target temp.
  3. 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.

Strict driver gates

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.

Single-source licence allow-list

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.

Empty-output guard

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.

Marker-count gate

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.

--check exit-code semantics

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.

Hand-curated content

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.

Configuration reference

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.

Canonical: [[blocks]] array

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.

Back-compat: flat [rust] table

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.

Per-driver block reference

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.

ecosystem = "rust"

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

ecosystem = "node"

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        = false

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

ecosystem = "go"

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

ecosystem = "python"

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) so MIT matches MIT 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-licenses

ecosystem = "bundled-binaries"

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

Monorepo guidance

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        = true

Simple, 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        = false

The block then includes devDependencies. Pair with exclude if a noisy dep keeps surfacing.

Dispatcher and driver contracts

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:

  1. Preflight — verify required tool + state; actionable error on stderr; non-zero exit if anything is missing.
  2. Render — deterministic markdown sorted/structured by the tool's own template. Idempotency under --check depends on it.
  3. Strict-licence check — reject disallowed or missing licences before render (cargo-about uses --fail; other drivers wire their equivalent).
  4. No side effects on the splice target — write only to the <output-temp-path> argument. Never touch target_path directly.

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.

Future evolution

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.

Public mirror

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.

Versioned releases

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 VERSIONCHANGELOG.md consistency locally with:

bash tools/starters/acknowledgements/check-version.sh

About

Drop-in third-party-attribution starter kit (mirror)

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors