fix(build): stop the catalogue clone layer from freezing a floating ref #963

Merged
coilysiren merged 1 commit from fix/catalogue-clone-cache into main 2026-08-18 16:40:22 +00:00
Member

The compose stage clones the composed catalogue with

RUN git clone --depth 1 --branch "${AOS_CATALOG_REF}" \
    https://forgejo.coilysiren.me/coilyco-flight-deck/agentic-os.git /tmp/aos-catalog

That instruction's text never changes, so Docker reuses the layer and
AOS_CATALOG_REF=main silently freezes at whenever the layer was first
built. docs/sirens-echo-compose.md says the ref "floats on main by design".
It did not float.

Measured, not inferred

tooling-sirens-dowel-contract merged to coilyco-flight-deck/agentic-os main
at 15:37:26Z. The image-build job on #961 ran at 16:07:22Z, thirty
minutes later, and failed:

role "engineer": pattern "tooling-sirens-dowel-contract" matches nothing in /tmp/aos-catalog

The skill was on main. The clone was not. test passed in that same run, so
this is the image path alone.

The fix

Ref resolution moves to the caller. scripts/lib/catalog-head.sh resolves a ref
to its current commit via git ls-remote, branch first and tag second since
git clone --branch accepts either, and fails loudly when a ref names neither.
Callers pass it as AOS_CATALOG_HEAD, which the clone RUN references, so the
layer's cache key moves when the catalogue does.

The RUN also re-reads the clone's HEAD and fails when it does not match what
the caller resolved. A catalogue that moves between resolve and clone becomes a
build failure rather than an image whose bundle nobody can account for.

Why a build arg rather than ADD <api url>

Keying the layer on an ADD of /repos/.../branches/${AOS_CATALOG_REF} also
works and is one line. I verified that endpoint: 200, carries the commit id, and
is byte-stable between calls.

It was rejected because publish-image.sh builds behind FORGEJO_EGRESS_PROXY,
passed as HTTP_PROXY and HTTPS_PROXY build args. A RUN honours those. A
builder-side ADD fetch is performed by the daemon and may not, so the publish
lane would be the one place it broke, and that is the lane that ships images.
Resolving in the caller keeps every network call somewhere the proxy already
applies. It also keeps the tag-override path working, which /branches/<tag>
would 404 on.

No silent path left

AOS_CATALOG_HEAD has no default and the RUN refuses an empty one, so a caller
that forgets it gets a build failure naming the helper rather than a stale
catalogue. All three build paths are wired:

  • scripts/ci-image-build.sh (the PR check)
  • scripts/publish-image.sh (the publisher)
  • just image, now through scripts/build-image-local.sh, so a workstation image
    is not quietly baked from a frozen layer either

The override path is unchanged: AOS_CATALOG_REF still names another branch or
tag to reproduce an older bundle.

Verification

  • pre-commit run --files green on all six, including shellcheck and
    code comment discipline.
  • Resolver exercised against a branch (main -> cc3cf4b1), a tag
    (v0.99.0 -> e1ff4799), and a bad ref (fails with the ref named).
  • bash -n clean on all three scripts, so just image-publish-check still holds.
  • The real proof is this PR's own image-build job, which now clones a live
    catalogue. No local daemon was available to me, so CI is the test.

Why separate

This blocks any change that adds a catalogue source, so it is deliberately not
folded into #961 which found it, and #956 needs it too. Landing this first
unblocks both.

The compose stage clones the composed catalogue with ```dockerfile RUN git clone --depth 1 --branch "${AOS_CATALOG_REF}" \ https://forgejo.coilysiren.me/coilyco-flight-deck/agentic-os.git /tmp/aos-catalog ``` That instruction's text never changes, so Docker reuses the layer and `AOS_CATALOG_REF=main` **silently freezes** at whenever the layer was first built. `docs/sirens-echo-compose.md` says the ref "floats on `main` by design". It did not float. ## Measured, not inferred `tooling-sirens-dowel-contract` merged to `coilyco-flight-deck/agentic-os` `main` at **15:37:26Z**. The `image-build` job on #961 ran at **16:07:22Z**, thirty minutes later, and failed: ``` role "engineer": pattern "tooling-sirens-dowel-contract" matches nothing in /tmp/aos-catalog ``` The skill was on `main`. The clone was not. `test` passed in that same run, so this is the image path alone. ## The fix Ref resolution moves to the caller. `scripts/lib/catalog-head.sh` resolves a ref to its current commit via `git ls-remote`, branch first and tag second since `git clone --branch` accepts either, and fails loudly when a ref names neither. Callers pass it as `AOS_CATALOG_HEAD`, which the clone `RUN` references, so the layer's cache key moves when the catalogue does. The `RUN` also re-reads the clone's `HEAD` and fails when it does not match what the caller resolved. A catalogue that moves between resolve and clone becomes a build failure rather than an image whose bundle nobody can account for. ### Why a build arg rather than `ADD <api url>` Keying the layer on an `ADD` of `/repos/.../branches/${AOS_CATALOG_REF}` also works and is one line. I verified that endpoint: 200, carries the commit id, and is byte-stable between calls. It was rejected because `publish-image.sh` builds behind `FORGEJO_EGRESS_PROXY`, passed as `HTTP_PROXY` and `HTTPS_PROXY` build args. A `RUN` honours those. A builder-side `ADD` fetch is performed by the daemon and may not, so the publish lane would be the one place it broke, and that is the lane that ships images. Resolving in the caller keeps every network call somewhere the proxy already applies. It also keeps the tag-override path working, which `/branches/<tag>` would 404 on. ## No silent path left `AOS_CATALOG_HEAD` has no default and the `RUN` refuses an empty one, so a caller that forgets it gets a build failure naming the helper rather than a stale catalogue. All three build paths are wired: - `scripts/ci-image-build.sh` (the PR check) - `scripts/publish-image.sh` (the publisher) - `just image`, now through `scripts/build-image-local.sh`, so a workstation image is not quietly baked from a frozen layer either The override path is unchanged: `AOS_CATALOG_REF` still names another branch or tag to reproduce an older bundle. ## Verification - `pre-commit run --files` green on all six, including `shellcheck` and `code comment discipline`. - Resolver exercised against a branch (`main` -> `cc3cf4b1`), a tag (`v0.99.0` -> `e1ff4799`), and a bad ref (fails with the ref named). - `bash -n` clean on all three scripts, so `just image-publish-check` still holds. - **The real proof is this PR's own `image-build` job**, which now clones a live catalogue. No local daemon was available to me, so CI is the test. ## Why separate This blocks any change that adds a catalogue source, so it is deliberately not folded into #961 which found it, and #956 needs it too. Landing this first unblocks both.
fix(build): stop the catalogue clone layer from freezing a floating ref
All checks were successful
ci / image-build (pull_request) Successful in 6s
ci / test (pull_request) Successful in 1m1s
ci / publish-echo-image (pull_request) Has been skipped
ci / publish-observed (pull_request) Has been skipped
785e85348a
The compose stage clones the composed catalogue with

    RUN git clone --depth 1 --branch "${AOS_CATALOG_REF}" ...

and that instruction's text never changes, so Docker reuses the layer and
AOS_CATALOG_REF=main silently freezes at whenever the layer was first built.
docs/sirens-echo-compose.md says the ref floats on main by design. It did not
float.

Measured rather than inferred. tooling-sirens-dowel-contract merged to
coilyco-flight-deck/agentic-os main at 15:37:26Z. The image-build job on
sirens-echo#961 ran at 16:07:22Z, thirty minutes later, and failed with

    role "engineer": pattern "tooling-sirens-dowel-contract" matches nothing in /tmp/aos-catalog

The skill was on main. The clone was not.

The fix moves ref resolution to the caller. scripts/lib/catalog-head.sh resolves
a ref to its current commit through git ls-remote, branch first and tag second
since `git clone --branch` accepts either, and fails loudly when a ref names
neither. Callers pass it as AOS_CATALOG_HEAD, which the clone RUN references, so
the layer's cache key moves when the catalogue does.

WHY A BUILD ARG RATHER THAN `ADD <api url>`. Keying the layer on an ADD of the
branches API would also work and is one line, but publish-image.sh builds behind
FORGEJO_EGRESS_PROXY passed as HTTP_PROXY and HTTPS_PROXY build args. A RUN
honours those. A builder-side ADD fetch is performed by the daemon and may not,
so the publish lane would be the one place it broke. Resolution in the caller
keeps every network call somewhere the proxy already applies.

The RUN also re-reads the clone's HEAD and fails when it does not match what the
caller resolved, so a catalogue that moves between resolve and clone is a build
failure rather than an image whose bundle nobody can account for.

AOS_CATALOG_HEAD has no default and the RUN refuses an empty one. Every build
path is wired, so there is no silent path left: ci-image-build.sh,
publish-image.sh, and `just image` through the new scripts/build-image-local.sh.
A caller that forgets it gets a build failure naming the helper.

The override path is unchanged. AOS_CATALOG_REF still names another branch or
tag to reproduce an older bundle, and the resolver handles both.

This blocks any change adding a catalogue source, so it is separate from the
change that found it (sirens-echo#961) and from sirens-echo#956, which needs it
too.

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-gaming/sirens-echo!963
No description provided.