Skip to content

Map: release + publish cycle for the estate's custom software #25

Description

@cshuttle

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 consumerdone. 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 consumerdone 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 :latestdeploys 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 workflowrelease-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: Topologylive: 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 pinlive: 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 cadencelive: 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 assetslive: 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 packagedan 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 releaselive: 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 sweeplive: 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 reposmain, 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions