Fleet renders shared repository surfaces for the starhaven-io estate from this
hub repository. Shared surfaces maintained as hand-edited copies always drift,
and drift is only caught by a full-estate audit; fleet makes those surfaces
reviewable generated artifacts with one source of truth, leaving only genuine
repo idiosyncrasies as hand-maintained content. Consumer CI never fetches
canonical content at runtime; convergence arrives only as reviewable PRs.
Every fleet-relevant file in every consumer is assigned exactly one tier:
- Tier 1 files are byte-identical whole files sourced from
fleet/files/. Never edited in-repo; the sync PR reverts any local change. - Tier 2 blocks are hub-owned fragments fenced inside repo-owned files with
fleet:blockmarkers. Content inside the fence is hub-owned; everything outside is repo-owned. - Tier 3 files are rendered whole files:
dependabot.ymland thin SHA-pinned callers for the reusable workflows in.github/workflows/. Each consumer's.fleet.ymlis also rendered from its hub-owned per-repository config. - Tier 4 files retain repo-owned orchestration. Fleet keeps first-party reusable workflow pins current and rejects consumer PRs that remove an established first-party reusable workflow call.
Tier 1:
| File | Notes |
|---|---|
.editorconfig |
all consumers |
.githooks/commit-msg |
Claude/Codex trailer guard on all commits; DCO hook with fixup/squash/merge skip |
.githooks/pre-push |
deletion-skip, just-guard, just check |
CLAUDE.md |
exactly @AGENTS.md |
LICENSE |
one canonical file per license type in fleet/files/licenses/ |
.mcp.json |
astro-docs config; consumers with astro-docs: true |
scripts/check-npm-install-policy.mjs |
deny-by-default install-script checker; consumers with npm-policy |
Tier 2 (managed blocks):
| Block | Host file | Scope |
|---|---|---|
commit-and-pr-conventions |
AGENTS.md |
all consumers; commit, PR, and comment discipline |
local-state |
.gitignore |
all consumers; the org-minimum header section |
install-hooks |
justfile |
all consumers |
npm-policy |
justfile |
consumers with npm-policy; parameterized by project directories |
audit |
justfile |
all workflow-owning consumers |
pinprick-audit |
justfile |
all consumers without a cited exception |
badges + license-section |
README.md |
public project repos, parameterized by repo name and badge workflow |
Tier 3 (rendered files and thin callers):
| File | Mechanism | Parameters |
|---|---|---|
.fleet.yml |
rendered copy of fleet/repos/<name>.yml |
complete effective fleet config, kept consumer-side for discoverability and guard base-state classification |
.github/dependabot.yml |
rendered file | ecosystems, directories, and dependency policies |
renovate.json |
rendered file | explicit shared-preset reference pinned to the current immutable fleet release; consumers with renovate: true |
.github/workflows/zizmor.yml |
caller of reusable-zizmor.yml |
extra push paths, schedule, timeout; defaults render the canonical shape |
.github/workflows/pinprick-audit.yml |
caller of reusable-pinprick-audit.yml |
advanced-security (false also drops the security-events grant), fail-on-findings, timeout |
.github/workflows/link-check.yml |
caller of reusable-link-check.yml |
targets, build-site, site directory, schedule |
.github/workflows/codeql.yml |
caller of reusable-codeql.yml |
languages, paths, runner, build mode and profile |
.github/workflows/fleet-guard.yml |
caller of reusable-fleet-guard.yml |
none |
| first-party reusable workflow calls | semantic jobs.<id>.uses values matching starhaven-io/.github/.github/workflows/reusable-*.yml@... |
sync keeps the SHA and fleet version comment current; guard prevents consumer PRs from removing established calls |
Tier 4 includes repo-owned ci.yml orchestration, release and deploy
workflows, all AGENTS.md content outside the managed block, README bodies,
repo-specific justfile recipes, and all source code. Inside
repo-owned workflows, Fleet owns the identity, multiplicity, and pin of each
established first-party reusable workflow call within its workflow file.
Triggers, conditions, matrices, inputs, dependency edges, and surrounding job
logic remain repo-owned.
Tier 2 fences use the host file's comment syntax:
<!-- fleet:block commit-and-pr-conventions -->
...hub-owned content...
<!-- fleet:end --># fleet:block local-state
...
# fleet:endMarkdown fences pad the hub-owned content with blank lines so Prettier-checked consumers do not reformat inside the fence; hash fences stay tight. Markers carry the constraint "do not hand-edit inside". A missing or mangled marker fails the sync run loudly rather than guessing.
The renderer fails on missing or mangled markers. It reads configuration only
from the hub and renders the configured bytes to the consumer's .fleet.yml;
it never derives configuration from existing consumer workflow files.
fleet/repos.yml is the small, flat registry of consumer names. The canonical
parameters for each consumer live in fleet/repos/<name>.yml, so a single
repository's configuration has a focused, legible review diff without turning
the registry into one large nested document. For example, starhaven.io reads
fleet/repos/starhaven.io.yml, and this hub's own config is
fleet/repos/.github.yml.
The sync renders that file as the consumer's .fleet.yml. The copy stays in
the consumer so contributors can discover the effective policy without
visiting the hub, and so the guard can classify surfaces that were managed in
the pull request's base. It is not an edit surface: configuration changes start
in the hub file and arrive through the fleet sync bot.
schema: 1
visibility: "public"
license: "agpl"
params:
renovate: true
codeql:
languages: ["actions", "javascript-typescript"]
paths: ["src/**", ".github/workflows/**"]
dependabot:
github-actions: ["/"]
npm: ["/"]
link-check:
targets: "README.md AGENTS.md"
build-site: false
readme:
badges:
workflow: "ci.yml"
exceptions: {}Use the array form when a Dependabot entry needs per-repository policy. For
example, this entry can be added to
fleet/repos/starhaven.io.yml. The ignore list accepts Dependabot dependency
names plus version ranges or semantic update types:
params:
dependabot:
- package-ecosystem: "npm"
group: "npm-dependencies"
directory: "/"
ignore:
- dependency-name: "typescript"
reason: "TypeScript 7.0 lacks Astro's required API; reassess with 7.1: https://github.com/withastro/astro/issues/17268"
versions: [">=7.0.0 <7.1.0"]The npm-policy param opts a consumer into the deny-by-default install-script
policy ahead of npm 12. It syncs scripts/check-npm-install-policy.mjs and
renders the npm-policy justfile recipe, parameterized by the project
directories the checker validates:
params:
npm-policy:
projects: [".", "site", "trigger"]The recipe renders into a repo-owned # fleet:block npm-policy fence in the
justfile, so a consumer must carry that fence before it is enabled, the same
as the other justfile blocks. The per-package allowScripts map in each
package.json and repo-specific CI and deploy integration stay repo-owned. The
shared link-check workflow runs the checker against the configured site
directory before either npm ci --strict-allow-scripts path. Configuration
validation requires every built site directory to be enrolled in
npm-policy.projects.
Exceptions are explicit and cited; a managed surface with an exception entry is left untouched by the renderer, so every variant is self-documenting:
exceptions:
pinprick-audit: "build-from-source: audits the local checkout"
pinprick-audit-recipe: "build-from-source: audits the local checkout"Use pinprick-audit for the workflow and pinprick-audit-recipe for the
justfile recipe when only one of those surfaces is exempt.
When a parameter is removed, the renderer compares the prior consumer
.fleet.yml ownership ledger with the desired config. Former whole-file
surfaces are deleted and former managed blocks are cleared while retaining
their fences. A newly cited exception deliberately transfers the existing
surface to repository ownership instead of deleting it. Publication carries
both additions and deletions in the verified commit.
To adopt a repository, add its name to fleet/repos.yml and add its validated
fleet/repos/<name>.yml config in the same hub change. The first sync bot pull
request creates .fleet.yml together with the other managed surfaces. A human
consumer pull request cannot create or change .fleet.yml, even when the base
branch has no copy; this keeps adoption on the same trusted path as later
configuration changes.
Before enrollment, the consumer preparation pull request must establish
.githooks/commit-msg and .githooks/pre-push from fleet/files/ with mode
100755. The signed createCommitOnBranch mutation publishes file contents
but has no file-mode field, so fleet sync fails closed rather than create a
non-executable hook or a mode-only empty pull request.
Every tier-2 fence must exist in its host file before the first render can succeed; the renderer fails on a missing fence rather than guessing where the block belongs. The consumer must carry, empty or populated:
AGENTS.md:<!-- fleet:block commit-and-pr-conventions -->.gitignore:# fleet:block local-statejustfile:# fleet:block install-hooks, plus# fleet:block npm-policywhennpm-policyis configured,# fleet:block auditunless theauditexception is cited, and# fleet:block pinprick-auditunless thepinprick-audit-recipeexception is citedREADME.md:<!-- fleet:block badges -->whenreadme.badgesis configured and<!-- fleet:block license-section -->whenreadme.licenseis configured
Repo-owned justfile recipes and aliases must not reuse a managed recipe name
(install-hooks, npm-policy, audit, pinprick-audit): just identifies a
recipe by name alone, and the renderer rejects the collision in every mode.
ruby fleet/sync.rb --repo-root <checkout> --repo-name <name> --adopt
appends any missing fences empty to existing host files and renders, which
covers most of the checklist mechanically; it never creates the host files
themselves.
Thin caller workflows keep on:, permissions, and concurrency in the
consumer repo, so zizmor and pinprick audit the effective trigger and grant
surface where it executes. The shared job bodies live in this hub:
reusable-zizmor.ymlreusable-pinprick-audit.ymlreusable-link-check.ymlreusable-codeql.ymlreusable-conventional-commits.ymlreusable-fleet-guard.yml
Fleet releases are tagged with CalVer: vYYYY.MM.DD.N, N starting at 1 each
Pacific day, cut whenever fleet/**, a reusable workflow, or the shared
renovate-config.json preset changes behavior. The canon is a dated cut, not an
API, so compatibility-semantic versions carry no information here. Every tag
carries all four segments: Dependabot cannot compare mixed-arity versions, so a
bare day tag strands pins (tags from 2026-07-05 predate this rule and stay as
they are).
Consumer callers pin reusable workflows by hub commit SHA with a fleet version
comment. The sync is the only writer for fleet pins: every render seeds every
caller from the authenticated release tag. Publication always starts from the
trusted default-branch workflow. Current-main code authenticates the release
and performs a no-write safety preflight against the consumer; the exact tagged
renderer then applies tagged canon from a separate checkout of the peeled
release commit. The annotated tag's name, exact annotation,
peeled commit, embedded fleet/VERSION, and identity as the latest first-parent
main commit that changed the version file must agree. Proposed releases remain
renderable during PR validation, but no consumer write can fall back to an
untagged or unmerged commit. The preflight bridges current safety checks to the
tagged renderer's stable command-line interface, so an older authenticated
release does not need current-main publication helpers and cannot inherit newer
rendering semantics. Because that bridge evaluates the authenticated release's
registry, configs, templates, helpers, locals, and surface set with the
current-main renderer, changes to those interfaces must remain backward
compatible with the active release. Stage removals across releases: first cut a
release whose canon no longer consumes the interface while retaining renderer
support, then remove that support only after the new tag is active. Each release
is one PR per consumer carrying canon changes and pin movement together.
Dependabot ignores starhaven-io/.github refs entirely and owns third-party
dependencies only.
Renovate consumers opt in with params.renovate: true. The renderer is the sole
writer for their root renovate.json, including the load-bearing Merge
Confidence opt-out, and pins the shared preset by immutable fleet release tag.
The fleet validation workflow uses the exact Renovate version declared in
fleet/validator/package.json for strict, no-global validation of the preset
and each rendered adopter stub. It actionlints every hub workflow and each
consumer workflow whose complete contents the fleet renders; unrelated
repo-owned workflows remain the consumer's own CI responsibility. Ephemeral
release-PR validation can propose a new version, while publication requires the
real authenticated tag.
Consumer Dependabot and the shared Renovate preset enforce the seven-day age gate for their eligible third-party updates. Same-organization actions are explicitly excluded from Dependabot's cooldown: first-party changes instead cross the reviewed, immutable fleet release boundary. Dependabot never writes fleet pins.
Fleet releases are cut through fleet-release.yml. Manual dispatch opens a
release PR that bumps fleet/VERSION to the next Pacific CalVer tag name. The
version parser requires exactly one valid vYYYY.MM.DD.N line and monotonic
progression. Keep the generated release change as one commit and merge it with
squash. The enforced invariant is that the VERSION change is in the resulting
main tip commit; an annotated tag is created for that exact commit. A rebased
history fails closed when any later commit separates the VERSION change from
the tip.
An existing tag is accepted only when its type, name, annotation, and peeled
commit match; mismatches fail rather than move the ref. After authentication,
the release workflow sends a repository_dispatch event, which makes the sync
load its workflow definition from the default branch rather than from the tag.
A root renovate-config.json change enters fleet validation but does not
publish until a maintainer dispatches the release workflow. Organization tag
rules should reserve v* creation and deletion for the release App.
fleet-sync.yml runs on a weekly schedule and on the default branch for a
fleet-sync repository dispatch. A scoped manual run can set the dispatch's
client_payload.repo to one name from fleet/repos.yml; arbitrary workflow
refs are intentionally not accepted. Every entry declares validated public
or private visibility; pre-merge validation may skip a failed checkout only
for an explicitly private consumer. The sync authenticates the release tag and
checks out both current-main tooling and the release snapshot. Current-main
tooling first performs a no-write render preflight, including path, marker,
configuration, and release checks. The tagged renderer then applies only tagged
canon. It clones each consumer, renders .fleet.yml, tiers 1 through 3, and
first-party reusable workflow pins before diffing against the working tree. The
hub consumer is checked out at the exact main commit captured during release
authentication. Before write credentials are minted, the job intersects the
tagged render's changed paths with every path changed between the release and
that captured commit. A disjoint set permits a missed hub self-sync to retry
after unrelated commits; any overlap fails closed so tagged canon cannot revert
a newer hub path. Paths remain
NUL-delimited through mode checks and the GraphQL payload. If anything differs
it opens or updates a single PR on branch
fleet-sync-<version> titled chore(fleet): sync managed surfaces <version>,
through a verified createCommitOnBranch commit. Update and auto-merge require
the exact base repository, branch, App author, and returned commit OID; stale
cleanup uses the same repository, base, author, and reserved-prefix checks. A
same-named fork PR is never selected. The PR body lists each
converged or retired surface. A repo in canon produces no PR; scheduled silence
is the health signal.
Every consumer receives .github/workflows/fleet-guard.yml, a required PR
check that calls reusable-fleet-guard.yml. The guard rejects any human pull
request that creates or changes .fleet.yml, then looks for PR changes to tier
1 files, tier 3 rendered files, and the content inside tier 2 fleet:block
markers. If none changed, it exits silently. If managed surfaces changed, it
runs the renderer in check mode against the PR tree using the hub config pinned
by the base branch's guard caller. Direct edits to managed files or blocks fail
with the exact fleet/repos/<name>.yml path to change in this hub. Sync-bot and
Dependabot PRs are exempt, and the job always reports a conclusion so the check
can be required.
First-party reusable workflow calls inside Tier 4 workflows are monotonic for consumer PRs: calls may be introduced, but a consumer PR cannot reduce the number of calls to a given reusable workflow within an existing workflow file. That prevents a policy job from being moved aside or replaced with a repo-local copy while leaving the rest of the repo-owned CI topology flexible. Intentional moves or removals are coordinated through the trusted hub and its sync bot. This protects the reusable call itself, not its execution: repo-owned conditions, inputs, path selection, and dependency edges can still cause the job to be skipped, and the guard does not claim to enforce those surfaces.
The guard reads its hub version from the caller pin in the consumer checkout, and in this hub it checks a PR against its own in-tree canon, since a hub PR carries the canon it proposes. That hub exemption is enabled only from the trusted workflow repository context, not from consumer-provided repo naming. Stage two fails only on surfaces the PR itself touched: drift that predates the branch belongs to the sync, not to the author. When current hub canon retires a managed surface that a consumer still carries, unrelated consumer PRs remain unblocked, but a human change or deletion of that retiring surface fails until a new fleet release and sync transfer ownership. A PR that pairs parameter changes with output rendered under a newer canon than the guard pin may still need the fleet pins bumped first; that window also closes only after the canon is released and synced.
The in-tree guard is an authoring and drift check. It cannot be the sole
adversarial control for edits to its own caller workflow, because a
pull_request run resolves that caller from the PR tree. Consumers that require
tamper-resistant enforcement need an org ruleset or required workflow sourced
from a trusted ref.
- Hub branch, tag, environment, and required-check rules are external
prerequisites: this repository cannot prove their live installation. They
should require reviewed hub PRs, block force-pushes and tag deletion, reserve
v*tag creation for the release App, and protect thestarhavenenvironment. - Hub
mainis a high-trust boundary because its scheduled workflows can mint repository-scoped App tokens. Consumer changes still arrive through signed commits and required-check-gated PRs, but a compromised hub workflow must not be treated as contained by SHA pins alone. - The org Actions policy implicitly allows same-org actions and reusable workflows; the explicit allowlist is reserved for third-party trust grants.
- The org-ruleset required guard (
fleet-guard-required.yml) deliberately runs the renderer from hubmainagainst consumer pull requests so hardening applies without waiting for a release. Residual risk: a compromised hubmainexecutes Ruby in consumer PR context, mitigated by a contents-read-only token, no secrets in that context, and hubmainitself requiring reviewed pull requests.
Render a consumer checkout in place:
ruby fleet/sync.rb --repo-root ../midden --repo-name middenCheck for drift without writing:
ruby fleet/sync.rb --repo-root ../midden --repo-name midden --checkGuard a pull request branch against its base:
ruby fleet/sync.rb --repo-root ../midden --repo-name midden --guard origin/mainScaffold missing fences during adoption, then render (never with --check or
--guard):
ruby fleet/sync.rb --repo-root ../midden --repo-name midden --adopt--repo-name is required and must name an entry in fleet/repos.yml; it
selects the matching hub-owned config file.