Skip to content

fix(ci): publish estate stack builds to their own private package - #509

Merged
thedancingdeveloper merged 2 commits into
devfrom
fix/estate-image-separate-package
Sep 2, 2026
Merged

fix(ci): publish estate stack builds to their own private package#509
thedancingdeveloper merged 2 commits into
devfrom
fix/estate-image-separate-package

Conversation

@thedancingdeveloper

Copy link
Copy Markdown
Contributor

The problem

vogt-stack is now public — it is the generic public AIO, which was the point of #506. But GHCR visibility is per package, not per version, so publishing it also published every version in it: 173 dev-/prod- images that are the maintainer's own pods, carrying tailscale, infisical, the step CLI, the Cadastre MCP bridge and theclawbay.

No credential was exposed. The stack build passes no secret as a build arg and mounts none; the helper scripts that reference estate infrastructure are already in this public tree. But "anyone can pull and run the estate's pod image" is not a property anybody chose, and it is not one a tag convention can prevent.

build.yml had already named the adjacent failure mode — about pinning rather than disclosure:

one repository with two kinds of tag in it is how somebody eventually pins the wrong one

Visibility made the same structure leak instead of merely confuse.

The change

STACK_IMAGE now selects by ref:

Package Built by Visibility
vogt-stack release.yml (version tag), main public
vogt-stack-estate build.yml on dev / prod private

The boundary becomes something the registry enforces rather than something a reader has to infer. release.yml stays unconditional — a release is always the public generic artefact — and a test asserts that, because a conditional there could publish a release into the private package.

docs/DEPLOYMENT.md §7's table is updated: it previously said "two builds share the vogt-stack name", which this makes false.

Already done outside this PR

The 173 published estate versions and their 172 cosign signatures have been deleted from the public package (345 deletions, 0 failures).

Four were retained deliberately — they are the digests indexarr/ops currently pins:

Digest Pinned by
79c4a4e5… personal/vogt-dev/docker-compose.yml
cc456a9c… personal/vogt-dev/estate.overlay.yml
ab69dd2f… personal/vogt-prod/estate.overlay.yml
abeeb89e… personal/vogt-prod-candidate/docker-compose.yml

Deleting a digest a live deployment pins would break its next pull. All four still resolve, and the public release family (latest, 0.4.0, 0.4, 0.3.0) is intact — both verified after the deletion. They age out once those stacks are re-pinned onto the estate package.

⚠ First run

GHCR can create a new package with the visibility of the repository that pushed it, and this repository is public. Check vogt-stack-estate is private after the first dev build — there is no API for package visibility, only the settings page.

Verification

uv run pytest 1316 passed / 25 skipped / 91% · ruff clean · check_docs.py clean. The new test was confirmed to fail against a literal STACK_IMAGE before being kept.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RzFCTnWGff1k4FyX1HQCYM

`vogt-stack` is now public — it is the generic public AIO. GHCR visibility is
per *package*, not per version, so publishing it also published every version
in it: 173 `dev-`/`prod-` images that are the maintainer's own pods, carrying
tailscale, infisical, the step CLI, the Cadastre MCP bridge and theclawbay.

No credential was exposed. The stack build passes no secret as a build arg and
mounts none; the estate additions are packages, and the helper scripts that
reference estate infrastructure are already in this public tree. But "anyone
can pull and run the estate's pod image" is not a property anybody chose, and
it is not one a tag convention can prevent.

`build.yml` had already named the adjacent failure mode, about pinning rather
than disclosure: "one repository with two kinds of tag in it is how somebody
eventually pins the wrong one." Visibility made the same structure leak instead
of merely confuse.

So `STACK_IMAGE` now selects by ref: `dev` and `prod` publish to
`vogt-stack-estate`, `main` and every release to the public `vogt-stack`. The
boundary becomes something the registry enforces rather than something a reader
has to infer. `release.yml` stays unconditional — a release is always the
public generic artefact — and a test asserts it stays that way, because a
conditional there could publish a release into the private package.

The 173 already-published estate versions (and their 172 cosign signatures)
have been deleted from the public package. Four were retained deliberately:
they are the digests `indexarr/ops` currently pins for vogt-dev, vogt-prod and
vogt-prod-candidate, and deleting a digest a live deployment pins would break
its next pull. They age out once those stacks are re-pinned onto the estate
package.

FIRST RUN: GHCR can create a new package with the visibility of the repository
that pushed it, and this repository is public. Check `vogt-stack-estate` is
private after the first `dev` build — there is no API for package visibility,
only the settings page.
Splitting the packages splits the deploy path, and the first version of this
change moved only the publisher. `deploy-dev.yml` resolved
`vogt-stack:dev-<sha>` and `scripts/deploy_dev.py` rewrote the deployed
`estate.overlay.yml` with the same name — so the next `deploy dev` would have
looked for a `dev-` tag in the public package, which after this change never
has one.

Four sites, found by asking rather than reading: the digest resolve, the cosign
verify, the `STACK_IMAGE` constant that rewrites the compose, and the receipt
line that tells the operator which image was deployed. The last of those was
missed on the first pass and caught by the test added here, which is the
argument for the test.

`deploy-production.yml` is deliberately unchanged. It takes a release tag and
its cosign identity requires `release.yml@refs/tags/v[0-9]`, so it deploys the
signed public release artefact — pointing it at the estate package would be the
same bug mirrored, and the test asserts it stays off.

`ghcr-retention.yml` gains the new package: one that no retention policy names
accumulates every dev build for ever.
@thedancingdeveloper

Copy link
Copy Markdown
Contributor Author

Follow-up: the deploy path needed repointing too

The first version of this moved only the publisher. Checking downstream consumers before merging found that deploy dev would have broken: deploy-dev.yml resolved vogt-stack:dev-<sha> and scripts/deploy_dev.py rewrote the deployed estate.overlay.yml with the same name — and after this change the public package never has a dev- tag.

Four sites, now fixed:

Site Was
deploy-dev.yml digest resolve vogt-stack
deploy-dev.yml cosign verify vogt-stack
scripts/deploy_dev.py STACK_IMAGE vogt-stack
deploy-dev.yml receipt line vogt-stack

The fourth I missed on the first pass; the new test caught it, which is a fair argument for the test existing.

deploy-production.yml is deliberately unchanged — it takes a release tag and its cosign identity requires release.yml@refs/tags/v[0-9], so it deploys the signed public release artefact. Pointing it at the estate package would be this same bug mirrored, and the test asserts it stays off.

ghcr-retention.yml gains vogt-stack-estate: a package no retention policy names accumulates every dev build for ever.

Gate re-run: 1317 passed / 25 skipped / 91%, mypy + ruff + check_docs clean.

@thedancingdeveloper
thedancingdeveloper merged commit 144418e into dev Sep 2, 2026
15 checks passed
@thedancingdeveloper
thedancingdeveloper deleted the fix/estate-image-separate-package branch September 2, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant