vendkit docs

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 / ci are the two environment axes (DR-0015). They are independent: a GitHub-hosted repo built by Azure Pipelines is scm: github, ci: azure-pipelines. Conformance parses the pipeline dialect named by ci: — never env-sniffed, so fleet audits decide identically.
  • ci: none is 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 report skipped.
  • publisher coordinates deliberately also appear in the manifest's source: (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.
  • handlers may 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:

  1. Vendor. Seed an empty manifest, then materialise with scope reconciliation — the profile's full export slice lands, tracked, with provenance recorded.
  2. Scaffold. Render the CI-keyed template set with the slice identity (nothing for ci: none). Placeholder substitution must fail loudly on any unresolved variable.
  3. 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> via VENDKIT_TOKEN_OPEN_PR; the reference handler refuses a GITHUB_TOKEN fallback (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: none leaves 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 runs vendkit self-verify against the engine.sha256 pin before the lane proper — no interpreter, no build step on the runner. The fetch defaults its asset base to the committed engine.base_url root (#15); the VENDKIT_ENGINE_BASE_URL pipeline 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: ./vendkitw for humans and ci: none repos (resolves engine.version — and the release-asset root engine.base_url, needed when the slice's publisher is not on GitHub — from the slice config, or from .vendkit/publisher/vendkit-engine.yml in 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 the resources.pipelines trigger (azure-pipelines) or the repository_dispatch receiver (github-actions) to the sync pipeline — the consumer receiver side of the hint. The publisher sender side is vendkit push-hint reading 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)

  1. Grant the CI identity read on the publisher repo (content checkout + engine artefact fetch).
  2. Record the engine checksums in engine.sha256 from the pinned release's SHA256SUMS.txt (one hex per platform). Until filled, vendkit self-verify is advisory; the fetch step still verifies each download against SHA256SUMS.txt (DR-0016). (Skipped for ci: none.)
  3. 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.)
  4. Enable branch protection / branch policies; make the gate a required check (github: ruleset; azure-repos: Build Validation). Under ci: none this step instead states plainly: there is no PR-time gate — automated drift enforcement is forfeited and vendkit gate --strict must be run by hand.
  5. On azure-repos: add a required-reviewers branch policy covering .vendkit/** (the CODEOWNERS equivalent) and record the required_reviewers_policy attestation.
  6. 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.