Spec: Onboarding and consumer configuration
Status: stable (frozen at v1.0.0) · Owner: Layer 3
1. Consumer configuration file
One file per vendored slice: .vendkit/consumer/<slice>.yml. Consumer-owned (scaffolded
once, then hand-maintained; not manifest-tracked — it holds consumer-local
values). This is the single consumer config surface: environment axes,
profile binding, pin location, watch channel, delivery handlers,
attestations, waivers.
.vendkit/ has two halves and they are never mixed (DR-0019): consumer/ holds
the control plane described here, publisher/ holds this repository's own
publishing configuration if it publishes a slice of its own. A repository can be
both without either side's discovery globs reaching the other's files. The
publisher's slice.aliases, if declared, are copied into the slice config so the
consumer knows the names its upstream answers to.
schema_version: 1
slice: docs
publisher:
scm: github # github | azure-repos — provenance and
repo: example-org/design-docs # shorthand-expansion hint only; any
# git URL or path is used verbatim
aliases: ["design-docs"] # copied from the publisher's declaration:
# the names this upstream answers to
scm: github # the CONSUMER's SCM host (DR-0015)
ci: github-actions # github-actions | azure-pipelines | none
profile: code-repo # optional; must exist in the declaration
pin:
# First entry is the authoritative read source; the sync PR advances the
# matching reference line in every listed file, in lockstep. With
# `ci: none` the pin block must be EMPTY — the manifest's source.release
# is the pin.
pattern: "ref: refs/tags/v"
files:
- .github/workflows/docs-sync.yml
- .github/workflows/vendkit-gate.yml
engine: # the pinned engine binary (DR-0016)
version: v1.4.2 # advanced with the content pin by sync
sha256: # per-platform; blank = advisory
linux/amd64: "…"
darwin/arm64: "…"
watch:
channel: stable # stable | rc
handlers: # delivery handlers (handler protocol)
pr:
exec: [vendkit, handler, github]
handoff:
exec: [vendkit, handler, github]
dedup_key: vendkit-watch-docs
fact-verify:
exec: [vendkit, handler, github]
seeds:
notes: informational # informational | silent (DR-0013)
attestations:
branch_protection_enabled: true
sync_credential_provisioned: true
waivers: [] # [{rule: <id>, reason: "…"}]
Beside it, engine-owned: .vendkit/consumer/<slice>-manifest.json (manifest spec).
Discovery convention (fixed; INV-8): tools enumerate .vendkit/consumer/*.yml for
slices and .vendkit/consumer/*-manifest.json for manifests — nothing else, nowhere
else. The namespace is strict: a .vendkit/consumer/*.yml that does not parse as
a slice config is a usage error, never a silent skip (DR-0012).
Field notes:
scm/ciare the two environment axes (DR-0015). They are independent: a GitHub-hosted repo built by Azure Pipelines isscm: github, ci: azure-pipelines. Conformance parses the pipeline dialect named byci:— never env-sniffed, so fleet audits decide identically.ci: noneis fully manual mode: no pipelines were scaffolded; gate, watch and sync are run by the human (or cron); the pin block is empty; pipeline-dependent conformance rules reportskipped.publishercoordinates deliberately also appear in the manifest'ssource:(immutable provenance of what was actually vendored) and in the publisher's own declaration (its identity). The three agree by construction; a conformance tree-check flags drift between config and manifest.handlersmay be omitted (all deliveries unwired — report-only) and each kind is independently optional.VENDKIT_HANDLER_<KIND>env vars override per run (handler-protocol spec §4).
2. The scaffolder
vendkit init --ci github-actions|azure-pipelines|none [--scm github|azure-repos] --version vX.Y.Z [--profile <p>] [--mode primary|additive] [--base-branch main] [--pr-token-secret <name>] [--codeowners <owners>] [--push-hint] (alias: onboard)
Run from a checkout of the publisher at the release being pinned (so the
scaffold, engine and content come from one immutable tree). --scm defaults
to inference from the consumer's origin remote (github.com /
dev.azure.com / *.visualstudio.com); no remote and no flag is a loud
usage error, never a guess. Three phases:
- Vendor. Seed an empty manifest, then materialise with scope reconciliation — the profile's full export slice lands, tracked, with provenance recorded.
- Scaffold. Render the CI-keyed template set with the slice identity
(nothing for
ci: none). Placeholder substitution must fail loudly on any unresolved variable. - Report. Print the manual step list (§4) and the conformance gaps that
remain. The scaffolder never performs trust-bootstrap acts itself and never
re-implements conformance judgment — it defers to
vendkit conformance.
Scaffolded outputs
| Output | github-actions | azure-pipelines | none | Mode |
|---|---|---|---|---|
| Gate pipeline (all slices, no path filter) | .github/workflows/vendkit-gate.yml |
azure-pipelines/vendkit-gate.yml |
— | primary only |
| Sync pipeline (per slice; schedule + optional push hint) | .github/workflows/<slice>-sync.yml |
azure-pipelines/<slice>-sync.yml |
— | always |
| Watch pipeline (all slices; weekly + PR dry-run self-test) | .github/workflows/vendkit-watch.yml |
azure-pipelines/vendkit-watch.yml |
— | primary only |
| Conformance pipeline (advisory) | .github/workflows/vendkit-conformance.yml |
azure-pipelines/vendkit-conformance.yml |
— | primary only |
| Engine fetch template — reusable steps for the consumer's OWN pipelines | .github/actions/vendkit-fetch/action.yml (composite action) |
azure-pipelines/vendkit-fetch.yml (steps template) |
— | primary only |
Bootstrap launcher (byte-identical to the vendkitw release asset; §5) |
./vendkitw |
same | same | always; skipped if present |
| Slice config | .vendkit/consumer/<slice>.yml |
same | same | always |
| Slice manifest | .vendkit/consumer/<slice>-manifest.json |
same | same | always |
CODEOWNERS stanza covering .vendkit/ |
opt-in via --codeowners (scm github only) |
refused: Azure Repos does not honour CODEOWNERS — required-reviewers policy on the checklist instead | scm-dependent | opt-in |
primary vs additive: the first slice onboards the shared machinery
(gate, watch, conformance pipelines — which are slice-agnostic by design,
architecture §5); each further slice adds only its own sync pipeline, config
and manifest. Additive onboarding must be idempotent: re-running detects the
existing slice and repairs rather than duplicates.
Scaffolded pipeline callers are consumer-owned after generation (they embed
consumer-local values: schedules, secret names, base branch) and are therefore
not drift-gated. The framework components they call are pinned by tag; the
conformance pipeline-wired rules keep the wiring honest.
3. Platform notes baked into the scaffolds
- github-actions sync: the PR handler takes its token from
secrets.<pr-token-secret>viaVENDKIT_TOKEN_OPEN_PR; the reference handler refuses aGITHUB_TOKENfallback (differences ledger #2) and the workflow carries a comment saying why. - azure-pipelines sync/gate: scaffold emits the pipeline YAML and prints the branch-policy commands/URLs for Build Validation (which cannot be set from YAML). The corresponding conformance rules stay red until done — the checklist is executable, not prose.
- Handlers: the slice config's default handler commands are keyed by the
consumer's
scm(vendkit handler github/vendkit handler ado, built into the engine binary);ci: noneleaves them unwired with a comment showing how to wire one. - Engine (DR-0016): the scaffolded lanes' first step fetches the pinned
engine binary, verifies it against the release
SHA256SUMS.txt, caches it, and runsvendkit self-verifyagainst theengine.sha256pin before the lane proper — no interpreter, no build step on the runner. The fetch defaults its asset base to the committedengine.base_urlroot (#15); theVENDKIT_ENGINE_BASE_URLpipeline variable overrides it, and remains the only path when the root is blank and the publisher is not on GitHub. - Launcher + fetch template (DR-0016 amendment): the same
resolve → fetch → verify-before-exec order is scaffolded twice over for
everything that is not a scaffolded lane:
./vendkitwfor humans andci: nonerepos (resolvesengine.version— and the release-asset rootengine.base_url, needed when the slice's publisher is not on GitHub — from the slice config, or from.vendkit/publisher/vendkit-engine.ymlin a publisher checkout (#15); caches under XDG outside the repo, and never degrades to an unpinned or unverified fetch), and the reusable fetch template for consumer-authored pipelines. Both are engine-owned: a consumer never hand-rolls the trust-boundary fetch. The launcher and the fetch template are consumer-owned files after scaffolding (like every scaffold output) and are deliberately byte-identical to / derived from the released, checksummed originals, so they can always be compared upstream. - Push hint:
--push-hint(off by default) adds theresources.pipelinestrigger (azure-pipelines) or therepository_dispatchreceiver (github-actions) to the sync pipeline — the consumer receiver side of the hint. The publisher sender side isvendkit push-hintreading its subscribers file (platform-integration spec §4). The schedule is always the reconciler, so the receiver is a pure latency optimisation (DR-0006).
4. Irreducible manual steps (reported, never performed)
- Grant the CI identity read on the publisher repo (content checkout + engine artefact fetch).
- Record the engine checksums in
engine.sha256from the pinned release'sSHA256SUMS.txt(one hex per platform). Until filled,vendkit self-verifyis advisory; the fetch step still verifies each download againstSHA256SUMS.txt(DR-0016). (Skipped forci: none.) - Provision the PR-capable credential as the named secret for the PR
handler; on ADO also policy-exempt it for creation only, on GitHub
prefer a GitHub App. (Skipped for
ci: none.) - Enable branch protection / branch policies; make the gate a required
check (github: ruleset; azure-repos: Build Validation). Under
ci: nonethis step instead states plainly: there is no PR-time gate — automated drift enforcement is forfeited andvendkit gate --strictmust be run by hand. - On azure-repos: add a required-reviewers branch policy covering
.vendkit/**(the CODEOWNERS equivalent) and record therequired_reviewers_policyattestation. - Record the attestations for the above in the slice config.
Each maps 1:1 to a conformance rule, so "fully onboarded" is
vendkit conformance --strict passing — the onboarding checklist and the
conformance spec can never diverge.
5. Bootstrap: acquiring the engine before init
Chicken-and-egg: running a pinned, checksum-verified VendKit needs a bootstrap step, and that step must itself be a released, checksummable artefact — never a recipe each adopter retypes and quietly edits (DR-0016 amendment). The one-time acquisition is a literal, verifiable download from a tagged release:
V=vX.Y.Z # the engine release you are bootstrapping from
BASE=https://github.com/jameswbaxter/vendkit/releases/download/$V
curl -fsSLO "$BASE/vendkitw" && curl -fsSLO "$BASE/SHA256SUMS.txt"
grep ' vendkitw$' SHA256SUMS.txt | sha256sum -c - # macOS: shasum -a 256 -c -
chmod +x vendkitw
From here the launcher fetches, verifies, and caches the engine itself
(VENDKIT_VERSION=$V ./vendkitw … until a slice config carries the pin), and
once vendkit init has run it owns the in-tree copy — the bootstrap file and
the scaffolded file are byte-identical, so SHA256SUMS.txt vouches for both.
A publisher checkout has no slice config to grow a pin; instead of exporting
VENDKIT_VERSION in every environment, commit the pin to
.vendkit/publisher/vendkit-engine.yml (#15) — the same key paths a slice
config uses, read by the launcher whenever the checkout has
.vendkit/publisher/export-declaration.yml and no .vendkit/consumer/:
engine:
version: vX.Y.Z
# Version-independent release root; the launcher appends /<version>.
# Needed when the engine's releases are not derivable (e.g. a mirror):
base_url: https://github.com/jameswbaxter/vendkit/releases/download
Equivalently, download the full engine binary the same verified way and run
init from it; the launcher is then purely the convenience for subsequent
runs. Either way the acquisition step is documented, tagged, and checksummed —
go install …@latest deliberately is not this path: it needs a toolchain and
pins nothing.