Build and publish the image on merge to main #100

Merged
coilysiren merged 1 commit from build-publish-image into main 2026-08-12 11:43:33 +00:00
Owner

Closes #97.

agent-proxy had no publish lane of its own. The image was built from coilyco-bridge/deploy, which cloned this repository at a resolved sha — so the source repository could not produce its own artifact.

Copies the fleet build-publish pattern (node-stats-mcp, bluesky-mcp, reddit-mcp): a gate job followed by a publish job on the deploy runner calling scripts/publish-image.sh.

Two deliberate differences from a literal copy

The gate uses this repo's own scripts and scripts/telegram_alert.py rather than the inline python3 -c 'exec("...")' blob the sibling repos carry. agent-proxy already has that helper wired into ci.yml, and the escaped one-liner is unreadable and unmaintainable.

The gate repeats the ci.yml checks rather than depending on them. Separate Forgejo workflows do not wait for one another, so a publish job that depended on ci.yml would race it and could push an image whose tests never passed. The sibling repos avoid this because build-publish.yml is their CI; agent-proxy already has a separate ci.yml, so a merge to main now runs the suite twice. That is deliberate — correctness over a minute of runner time. Say the word if you would rather fold ci.yml into this workflow instead.

Tag and immutability

forgejo.coilysiren.me/coilyco-flight-deck/agent-proxy:<sha>, full 40 characters. The script rejects anything that is not lowercase hexadecimal of exactly that length, so a short sha or a branch name can never become a tag. No latest. This matches what deploy's rollout_resolve_sha already resolves and pulls, so nothing downstream needs to change.

The build runs from the checkout rather than re-cloning the source repo at a sha the way the deploy-side script does. The checkout already is the immutable commit being published, so the clone is redundant here.

Credential handling

REGISTRY_TOKEN is scoped to the publish step only. The script fails closed when it is absent, logs in through a private DOCKER_CONFIG in a mktemp -d, and removes that directory via trap on exit, so no credential survives the job.

Check before merging: this repo needs a REGISTRY_TOKEN secret and a runner labelled deploy. Both exist for the sibling repos; I could not verify from here whether they are attached to this repository.

One thing the hooks caught

My first commit attempt was rejectedactionlint does not know the deploy runner label, which is only declared per-repo. .github/actionlint.yaml now lists it alongside docker. Worth noting the pre-commit gate did its job: the commit never landed, and I verified HEAD rather than assuming it had.

Boundary preserved

This repository builds and publishes. It does not deploy. Rollout stays driven from coilyco-bridge/deploy/services/agent-proxy per the source-to-deploy layer invariant, and that repo keeps its own publish lane as the rollback path. Documented in docs/image-publishing.md.

Verification

  • pre-commit run --all-files — every hook passes, actionlint and forgejo-runner-validate included
  • bash -n scripts/publish-image.sh — clean; executable bit set
  • pytest 274 passed
  • The publish path itself cannot be exercised locally: it needs the deploy runner, a Docker daemon, and the registry credential. First real proof is the next merge to main.
Closes #97. agent-proxy had no publish lane of its own. The image was built from `coilyco-bridge/deploy`, which cloned this repository at a resolved sha — so the source repository could not produce its own artifact. Copies the fleet `build-publish` pattern (`node-stats-mcp`, `bluesky-mcp`, `reddit-mcp`): a **gate** job followed by a **publish** job on the `deploy` runner calling `scripts/publish-image.sh`. ## Two deliberate differences from a literal copy **The gate uses this repo's own scripts and `scripts/telegram_alert.py`** rather than the inline `python3 -c 'exec("...")'` blob the sibling repos carry. agent-proxy already has that helper wired into `ci.yml`, and the escaped one-liner is unreadable and unmaintainable. **The gate repeats the `ci.yml` checks rather than depending on them.** Separate Forgejo workflows do not wait for one another, so a publish job that depended on `ci.yml` would race it and could push an image whose tests never passed. The sibling repos avoid this because `build-publish.yml` *is* their CI; agent-proxy already has a separate `ci.yml`, so a merge to main now runs the suite twice. That is deliberate — correctness over a minute of runner time. Say the word if you would rather fold `ci.yml` into this workflow instead. ## Tag and immutability `forgejo.coilysiren.me/coilyco-flight-deck/agent-proxy:<sha>`, full 40 characters. The script rejects anything that is not lowercase hexadecimal of exactly that length, so a short sha or a branch name can never become a tag. **No `latest`.** This matches what `deploy`'s `rollout_resolve_sha` already resolves and pulls, so nothing downstream needs to change. The build runs from the checkout rather than re-cloning the source repo at a sha the way the deploy-side script does. The checkout already *is* the immutable commit being published, so the clone is redundant here. ## Credential handling `REGISTRY_TOKEN` is scoped to the publish step only. The script fails closed when it is absent, logs in through a private `DOCKER_CONFIG` in a `mktemp -d`, and removes that directory via `trap` on exit, so no credential survives the job. **Check before merging:** this repo needs a `REGISTRY_TOKEN` secret and a runner labelled `deploy`. Both exist for the sibling repos; I could not verify from here whether they are attached to *this* repository. ## One thing the hooks caught My first commit attempt was **rejected** — `actionlint` does not know the `deploy` runner label, which is only declared per-repo. `.github/actionlint.yaml` now lists it alongside `docker`. Worth noting the pre-commit gate did its job: the commit never landed, and I verified HEAD rather than assuming it had. ## Boundary preserved This repository builds and publishes. It does not deploy. Rollout stays driven from `coilyco-bridge/deploy/services/agent-proxy` per the source-to-deploy layer invariant, and that repo keeps its own publish lane as the rollback path. Documented in `docs/image-publishing.md`. ## Verification - `pre-commit run --all-files` — every hook passes, `actionlint` and `forgejo-runner-validate` included - `bash -n scripts/publish-image.sh` — clean; executable bit set - `pytest` 274 passed - The publish path itself cannot be exercised locally: it needs the `deploy` runner, a Docker daemon, and the registry credential. **First real proof is the next merge to main.**
Build and publish the image on merge to main
All checks were successful
ci / smoke (pull_request) Successful in 6s
ci / quality (pull_request) Successful in 21s
44f7f8fef1
agent-proxy had no publish lane of its own. The image was built from
coilyco-bridge/deploy, which cloned this repository at a resolved sha, so the
source repository could not produce its own artifact.

Adds the fleet build-publish pattern used by node-stats-mcp, bluesky-mcp, and
reddit-mcp: a gate job followed by a publish job on the deploy runner calling
scripts/publish-image.sh. The deploy runner label is declared in
.github/actionlint.yaml alongside docker, or actionlint rejects the workflow.

Two deliberate differences from a literal copy. The gate reuses this repo's own
scripts and scripts/telegram_alert.py rather than the inline exec blob the other
repos carry, because agent-proxy already has that helper. The gate also repeats
the ci.yml checks rather than depending on them, since separate workflows do not
wait for one another and a publish that raced ci.yml could push an image whose
tests never passed.

The tag is the full 40-character commit sha, matching what deploy resolves and
pulls. The script rejects any sha that is not lowercase hexadecimal of exactly
that length, so a short sha or a branch name can never become a tag. There is no
latest tag.

The build happens from the checkout rather than by re-cloning the source repo at
a sha, which is what the deploy-side script does. The checkout is already the
immutable commit being published, so the clone is redundant here.

REGISTRY_TOKEN is scoped to the publish step, the script fails closed without
it, and the docker login writes to a private temporary DOCKER_CONFIG that is
removed on exit.

This repository builds and publishes but does not deploy. Rollout stays driven
from coilyco-bridge/deploy per the source to deploy layer invariant, and that
repository keeps its own publish lane as the rollback path.

closes #97

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agent-proxy!100
No description provided.