Destination
Every piece of custom software in the estate that ships a real artifact is cut as a
versioned release, and its deploy consumes that version: cshuttle/workflows tagged and
pinned by its consumers, and Topology, Atlas, VirtualWindow, chrome-exporter and nmon each
having cut a release through a shared, reusable release workflow.
Reached when all six have a release the deploy path actually references — not when the
policy is merely written down.
Notes
Domain. Homelab estate, ~33 repos under cshuttle. Two deploy planes: ArgoCD
reconciling cshuttle/main at HEAD (Atlas, VirtualWindow) and Komodo stacks on the Docker
hosts (Topology, nmon, Labelmaker). Images live in GHCR under ghcr.io/cshuttle, built by
self-hosted ARC runners (arc-<repo>) — GitHub-hosted runners are disabled estate-wide, so
ubuntu-latest hangs forever.
Starting state (surveyed 2026-08-02).
| Repo |
Artifact today |
Releases / tags |
workflows |
reusable workflows, consumed at @main by 30 repos / 33 refs (plus shared Lefthook hooks at ref: main in 4) |
0 / 0 |
Topology |
ghcr.io/cshuttle/topology:latest + full SHA, then triggers the Komodo stack deploy |
0 / 0 |
Atlas |
ghcr.io/cshuttle/atlas + atlas-config-api, :latest + short SHA; argocd-image-updater rolls :latest |
0 / 0 |
VirtualWindow |
two build workflows |
v1.0.0, 46 commits behind |
Monitoring/chrome-exporter |
Go binaries committed to git, scp'd by deploy-rpi.sh |
0 / 0 |
nmon |
no image at all — stock node:24-alpine with the repo bind-mounted read-only |
0 / 0 |
Execution is in scope. This map overrides wayfinder's plan-don't-do default: task
tickets here do the work, not just decide it. Decisions still come first — the versioning
scheme and the pinning model gate every build ticket.
Research tickets are resolved inline by the session that claims them, not fanned out to
subagents (estate policy: no subagent spawning unless explicitly requested).
Standards. Estate conventions live in STANDARDS.md in this repo; third-party actions
are pinned by SHA everywhere — anything this map adds must hold that line.
Decisions so far
-
Decide what a version number means for each artifact class — semver shape everywhere, meaning per class: workflows is a compatibility contract with immutable exact tags and no floating v1 (consumers pin, Renovate bumps); apps and binaries are rollback anchors where a major = the deploy needs a human step. chrome-exporter is tagged chrome-exporter/vX.Y.Z inside Monitoring. First tag v1.0.0 everywhere.
-
Tag cshuttle/workflows and pin every consumer — done. v1.0.0 cut and released; every consumer pinned across both surfaces (30 ggshield callers, 3 hand-edited callers, 4 Lefthook remotes); the reconciler owns the generated ref and Renovate is excluded from it (proven by a full dry-run); docs and the immutable-tag policy written. Estate audit: 30 pinned, 0 floating.
-
Tag cshuttle/workflows and pin every consumer — done and live: v1.0.0 released, 30 repos + 4 Lefthook remotes + 3 hand-edited callers pinned, 0 floating. Reconciler owns the 30 (Renovate excluded, verified by dry-run); Renovate keeps the 3. Tag policy in the repo README.
-
Map how each deploy path consumes image tags today — Atlas: image-updater digest strategy on :latest, git write-back of digests into the kustomization (30m poll + GHCR webhook). Topology: Komodo deploy trigger + auto-pull. VirtualWindow does NOT float — hand-pinned to a commit-sha tag with a broken-builds comment, absent from the updater CRD, and nothing reads its v1.0.0. A version-only tag triggers no roll; pinning means switching to semver, leaving the CRD, or treating the version as a label.
-
Decide whether deploys pin the release version or keep tracking :latest — deploys track released versions, gated at majors: semver constraint below the next major, so minors/patches roll and a major waits for a human. VirtualWindow joins the model (its hand-picked sha pins go). Build once on merge, release promotes the existing digest — no rebuild. Rollback = narrow the constraint (editing the pin alone is reverted by the updater within 30 min), then fix forward.
-
Decide how the Komodo plane tracks a released version — compose interpolates ${TOPOLOGY_VERSION}; the value lives in control-plane/stacks.toml (the existing OMNI_IMG_TAG pattern) and Renovate bumps it via a custom manager, automerging minor/patch and holding majors. No cross-repo token; accepted cost is up to ~24h lag vs ArgoCD's ~1 min. Rollback = revert plus an allowedVersions cap, because a bare revert is re-bumped.
-
Decide how a release is cut — a workflow_dispatch button taking version + optional summary: validates, promotes the digest, tags, publishes. A commit-driven bot was rejected on evidence — VirtualWindow (1/40) and Monitoring (2/40) have almost no version-bumping conventional commits, so it would be near-silent for 2 of 5 repos. Notes: generated always, optional hand-written lead, no CHANGELOG.md. The missing nudge becomes a health-sweep check.
-
Write the shared reusable release workflow — release-image.yml promotes a tested digest, never rebuilds; guards refuse a reused tag (checked against the remote) and refuse a digest that is not this commit's build. Multi-image, git-only tag-prefix, github-script for the release (no gh on ARC runners). Binaries deliberately excluded — one artifact, no shared abstraction. Model recorded in STANDARDS.md + docs/adr/0001. Unproven until #33 runs it.
-
Cut the first app release: Topology — live: v1.0.0 promoted (same digest), git tag on the build's commit not HEAD, stack running topology:v1.0.0 healthy, Renovate custom manager verified extracting. Forced two guard fixes (v1.1.1, v1.2.0) — the HEAD-sha requirement and user-owned-package auth. The model is proven end to end.
-
Atlas: release both images and settle its deploy pin — live: v1.0.0, both images promoted from one build commit, pods on :v1.0.0, app Synced/Healthy. ArgoCD switched to semver + 1.x; the kustomization's stale digest: removed (it would have overridden the tag). Steady state observed — later reconciles update nothing, including a webhook for a sha tag. Both deploy planes now proven.
-
VirtualWindow: close the gap and restart the cadence — live: v1.1.0, two images from different build commits versioned together, hand-picked shas retired, display verified serving and /api/overlay 200. The ticket's premise was wrong — the deployed image was NEWER than the tag, ~9 commits behind main, because nothing read the tag. Forced v1.3.0 (independent components), v1.4.0 (wrong fix) and v1.5.0 (tag the ref, record provenance in the release). Exposed a pre-existing stale-VolumeAttachment fault — cshuttle/main#99.
-
chrome-exporter: ship binaries as Release assets — live: chrome-exporter/v1.0.0, ~30 MB of binaries out of git, deploy-rpi.sh fetches by version, assets downloaded and verified as the right architectures from outside the pipeline. Built (not promoted) in-repo, as #31 decided. The arch check had to stop using file(1) — absent from the ARC runner image, and it read the gap as a wrong arch.
-
Decide how nmon is packaged — an image, same model as the rest. The app has nothing to build, but its runtime floats (node:24-alpine resolved at pull time), and an image pins both. Tagged-bind-mount rejected: no stack in the estate pins a non-branch ref, so it rested on unverified Komodo behaviour and would make nmon the one app that deploys differently.
-
nmon: build the artifact and cut its first release — live: v1.0.0, stack off the bind-mount and onto the image, healthy, nmon-web-data preserved. Node pinned by digest, so the runtime is versioned too. First repo the shared workflow handled unmodified. Package pushed with GITHUB_TOKEN and came out repo-linked — no PAT.
-
Add a release-drift check to the health sweep — live: reports repos whose default branch ran past their last release, at 20 commits or 30 days, scoped to repos with version-shaped releases (a moving latest tag counted as one and reported 54 phantom commits). First run found one true positive, no noise: blinkstick-mqtt, 11 commits since v1.0.1.
Not yet specified
-
The ARC runner image has nothing but the runner. Three failures today came from assuming a tool
exists on it (gh, file, and shellcheck/jq before that). Whether to document that as a standard,
or bake a minimal toolset into the image, is unsettled.
-
ghcr packages are user-owned, not repo-owned. A leftover from images bootstrapped by manual push:
the repo-scoped GITHUB_TOKEN cannot even read them, so repos carry a classic GHCR_WRITE_TOKEN.
Atlas already fixed this by granting the repo access under the package's Manage Actions access
settings. Doing that estate-wide would retire a long-lived credential.
-
Second-tier release candidates, deferred until the standard exists and the first repos
prove it: Nautobot (SSoT jobs), C4-MCP (npm or ghcr — depends whether anything outside
the gateway consumes it), Monitoring exporters + scripts (rsync'd to VM402, no version
stamp), Terraform and renovate-config (consumed by reference, so they inherit the
pin-a-tag argument), blinkstick-mqtt (has v1.0.1; cadence stalled), Labelmaker
(Komodo run_build = true — builds on the Docker host, so no registry artifact exists to
version).
-
Renovate's role for internal image pins. Settled for reusable-workflow refs (exact pins,
Renovate-bumped — part of the tag-and-pin ticket). Unsettled for versioned container images, which
depends on whether deploys pin at all.
-
GHCR retention. Versioned tags accumulate alongside per-SHA tags; whether cleanup
policy needs to change is unknown.
Out of scope
- GitOps / config repos —
main, k8s, Komodo, Semaphore, Caddy, Garage*,
MCP-Gateway, Docs, Homelab-Skills, Dotfiles, and Monitoring's config side. ArgoCD
and Komodo reconcile these at HEAD by design; a version number there is one nothing reads.
Destination
Every piece of custom software in the estate that ships a real artifact is cut as a
versioned release, and its deploy consumes that version:
cshuttle/workflowstagged andpinned by its consumers, and Topology, Atlas, VirtualWindow, chrome-exporter and nmon each
having cut a release through a shared, reusable release workflow.
Reached when all six have a release the deploy path actually references — not when the
policy is merely written down.
Notes
Domain. Homelab estate, ~33 repos under
cshuttle. Two deploy planes: ArgoCDreconciling
cshuttle/mainat HEAD (Atlas, VirtualWindow) and Komodo stacks on the Dockerhosts (Topology, nmon, Labelmaker). Images live in GHCR under
ghcr.io/cshuttle, built byself-hosted ARC runners (
arc-<repo>) — GitHub-hosted runners are disabled estate-wide, soubuntu-latesthangs forever.Starting state (surveyed 2026-08-02).
workflows@mainby 30 repos / 33 refs (plus shared Lefthook hooks atref: mainin 4)Topologyghcr.io/cshuttle/topology:latest+ full SHA, then triggers the Komodo stack deployAtlasghcr.io/cshuttle/atlas+atlas-config-api,:latest+ short SHA; argocd-image-updater rolls:latestVirtualWindowv1.0.0, 46 commits behindMonitoring/chrome-exporterdeploy-rpi.shnmonnode:24-alpinewith the repo bind-mounted read-onlyExecution is in scope. This map overrides wayfinder's plan-don't-do default:
tasktickets here do the work, not just decide it. Decisions still come first — the versioning
scheme and the pinning model gate every build ticket.
Research tickets are resolved inline by the session that claims them, not fanned out to
subagents (estate policy: no subagent spawning unless explicitly requested).
Standards. Estate conventions live in
STANDARDS.mdin this repo; third-party actionsare pinned by SHA everywhere — anything this map adds must hold that line.
Decisions so far
Decide what a version number means for each artifact class — semver shape everywhere, meaning per class:
workflowsis a compatibility contract with immutable exact tags and no floatingv1(consumers pin, Renovate bumps); apps and binaries are rollback anchors where a major = the deploy needs a human step. chrome-exporter is taggedchrome-exporter/vX.Y.Zinside Monitoring. First tagv1.0.0everywhere.Tag cshuttle/workflows and pin every consumer — done.
v1.0.0cut and released; every consumer pinned across both surfaces (30 ggshield callers, 3 hand-edited callers, 4 Lefthook remotes); the reconciler owns the generated ref and Renovate is excluded from it (proven by a full dry-run); docs and the immutable-tag policy written. Estate audit: 30 pinned, 0 floating.Tag cshuttle/workflows and pin every consumer — done and live:
v1.0.0released, 30 repos + 4 Lefthook remotes + 3 hand-edited callers pinned, 0 floating. Reconciler owns the 30 (Renovate excluded, verified by dry-run); Renovate keeps the 3. Tag policy in the repo README.Map how each deploy path consumes image tags today — Atlas: image-updater
digeststrategy on:latest, git write-back of digests into the kustomization (30m poll + GHCR webhook). Topology: Komodo deploy trigger + auto-pull. VirtualWindow does NOT float — hand-pinned to a commit-sha tag with a broken-builds comment, absent from the updater CRD, and nothing reads itsv1.0.0. A version-only tag triggers no roll; pinning means switching tosemver, leaving the CRD, or treating the version as a label.Decide whether deploys pin the release version or keep tracking
:latest— deploys track released versions, gated at majors: semver constraint below the next major, so minors/patches roll and a major waits for a human. VirtualWindow joins the model (its hand-picked sha pins go). Build once on merge, release promotes the existing digest — no rebuild. Rollback = narrow the constraint (editing the pin alone is reverted by the updater within 30 min), then fix forward.Decide how the Komodo plane tracks a released version — compose interpolates
${TOPOLOGY_VERSION}; the value lives incontrol-plane/stacks.toml(the existingOMNI_IMG_TAGpattern) and Renovate bumps it via a custom manager, automerging minor/patch and holding majors. No cross-repo token; accepted cost is up to ~24h lag vs ArgoCD's ~1 min. Rollback = revert plus anallowedVersionscap, because a bare revert is re-bumped.Decide how a release is cut — a
workflow_dispatchbutton takingversion+ optionalsummary: validates, promotes the digest, tags, publishes. A commit-driven bot was rejected on evidence — VirtualWindow (1/40) and Monitoring (2/40) have almost no version-bumping conventional commits, so it would be near-silent for 2 of 5 repos. Notes: generated always, optional hand-written lead, no CHANGELOG.md. The missing nudge becomes a health-sweep check.Write the shared reusable release workflow —
release-image.ymlpromotes a tested digest, never rebuilds; guards refuse a reused tag (checked against the remote) and refuse a digest that is not this commit's build. Multi-image, git-onlytag-prefix,github-scriptfor the release (noghon ARC runners). Binaries deliberately excluded — one artifact, no shared abstraction. Model recorded inSTANDARDS.md+docs/adr/0001. Unproven until #33 runs it.Cut the first app release: Topology — live: v1.0.0 promoted (same digest), git tag on the build's commit not HEAD, stack running
topology:v1.0.0healthy, Renovate custom manager verified extracting. Forced two guard fixes (v1.1.1,v1.2.0) — the HEAD-sha requirement and user-owned-package auth. The model is proven end to end.Atlas: release both images and settle its deploy pin — live: v1.0.0, both images promoted from one build commit, pods on
:v1.0.0, app Synced/Healthy. ArgoCD switched tosemver+1.x; the kustomization's staledigest:removed (it would have overridden the tag). Steady state observed — later reconciles update nothing, including a webhook for a sha tag. Both deploy planes now proven.VirtualWindow: close the gap and restart the cadence — live: v1.1.0, two images from different build commits versioned together, hand-picked shas retired, display verified serving and
/api/overlay200. The ticket's premise was wrong — the deployed image was NEWER than the tag, ~9 commits behind main, because nothing read the tag. Forcedv1.3.0(independent components),v1.4.0(wrong fix) andv1.5.0(tag the ref, record provenance in the release). Exposed a pre-existing stale-VolumeAttachment fault — cshuttle/main#99.chrome-exporter: ship binaries as Release assets — live: chrome-exporter/v1.0.0, ~30 MB of binaries out of git,
deploy-rpi.shfetches by version, assets downloaded and verified as the right architectures from outside the pipeline. Built (not promoted) in-repo, as #31 decided. The arch check had to stop usingfile(1)— absent from the ARC runner image, and it read the gap as a wrong arch.Decide how nmon is packaged — an image, same model as the rest. The app has nothing to build, but its runtime floats (
node:24-alpineresolved at pull time), and an image pins both. Tagged-bind-mount rejected: no stack in the estate pins a non-branch ref, so it rested on unverified Komodo behaviour and would make nmon the one app that deploys differently.nmon: build the artifact and cut its first release — live: v1.0.0, stack off the bind-mount and onto the image, healthy,
nmon-web-datapreserved. Node pinned by digest, so the runtime is versioned too. First repo the shared workflow handled unmodified. Package pushed withGITHUB_TOKENand came out repo-linked — no PAT.Add a release-drift check to the health sweep — live: reports repos whose default branch ran past their last release, at 20 commits or 30 days, scoped to repos with version-shaped releases (a moving
latesttag counted as one and reported 54 phantom commits). First run found one true positive, no noise: blinkstick-mqtt, 11 commits since v1.0.1.Not yet specified
The ARC runner image has nothing but the runner. Three failures today came from assuming a tool
exists on it (
gh,file, and shellcheck/jq before that). Whether to document that as a standard,or bake a minimal toolset into the image, is unsettled.
ghcr packages are user-owned, not repo-owned. A leftover from images bootstrapped by manual push:
the repo-scoped
GITHUB_TOKENcannot even read them, so repos carry a classicGHCR_WRITE_TOKEN.Atlas already fixed this by granting the repo access under the package's Manage Actions access
settings. Doing that estate-wide would retire a long-lived credential.
Second-tier release candidates, deferred until the standard exists and the first repos
prove it:
Nautobot(SSoT jobs),C4-MCP(npm or ghcr — depends whether anything outsidethe gateway consumes it),
Monitoringexporters + scripts (rsync'd to VM402, no versionstamp),
Terraformandrenovate-config(consumed by reference, so they inherit thepin-a-tag argument),
blinkstick-mqtt(hasv1.0.1; cadence stalled),Labelmaker(Komodo
run_build = true— builds on the Docker host, so no registry artifact exists toversion).
Renovate's role for internal image pins. Settled for reusable-workflow refs (exact pins,
Renovate-bumped — part of the tag-and-pin ticket). Unsettled for versioned container images, which
depends on whether deploys pin at all.
GHCR retention. Versioned tags accumulate alongside per-SHA tags; whether cleanup
policy needs to change is unknown.
Out of scope
main,k8s,Komodo,Semaphore,Caddy,Garage*,MCP-Gateway,Docs,Homelab-Skills,Dotfiles, andMonitoring's config side. ArgoCDand Komodo reconcile these at HEAD by design; a version number there is one nothing reads.