mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
image: key agent layers on upstream version, not hourly timestamp
The old AGENT_INSTALL_CACHEBUST=<hourly-timestamp> build-arg re-ran
all three agent-installer layers (+ the sanity check) on every hourly
cron build. Because `curl install.sh | bash` isn't reproducible, each
rebuild emitted fresh layer blobs -> a new ~200 MiB-of-agent-layers
image every hour even when no agent version changed, so :latest
consumers re-downloaded those layers hourly for nothing. (Confirmed
on published images: consecutive hourly builds share layers 1-9 but
get fresh digests for every agent layer, incl. the no-fs-change
sanity layer.)
Replace it with a per-agent version cache key: the workflow resolves
each agent's current upstream version (codex -> openai/codex
releases/latest, opencode -> sst/opencode releases/latest, claude ->
npm @anthropic-ai/claude-code latest) and passes AGENT_VERSION_{CODEX,
OPENCODE,CLAUDE}. Each installer RUN references its arg so BuildKit
rebuilds that layer ONLY when the version string changes; an unchanged
hourly build is now a pure cache hit (identical layer digests ->
nothing to re-pull). Resolution failure fails the build rather than
shipping a stale key and skipping a real update.
Also correct the layer-order rationale. #12 ordered the installers
"smallest first"; the real published sizes are codex 95 / opencode 50
/ claude 68 MiB compressed, and codex is also the most frequently
released (multiple stable cuts/day). Since a lower-layer change
cascades up through every layer above it, the topmost agent layer
re-emits on essentially every change -> the biggest/most-frequent
agent (codex) belongs at the BOTTOM. The existing order
(codex -> opencode -> claude) is already optimal across the plausible
frequency range; only the (wrong) comment is fixed, no reorder.
Follow-up to #12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gtxsKVWPrR8K1M1Te57MG
This commit is contained in:
parent
5c7aea9460
commit
2943cd940a
2 changed files with 100 additions and 37 deletions
56
.github/workflows/build-image.yml
vendored
56
.github/workflows/build-image.yml
vendored
|
|
@ -59,6 +59,39 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Resolve agent versions
|
||||
id: ver
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Each agent's installer fetches "latest" at build time. We
|
||||
# resolve that same "latest" version here and feed it to the
|
||||
# Dockerfile as a per-agent cache key (AGENT_VERSION_*), so an
|
||||
# installer layer is rebuilt only when its agent actually
|
||||
# released — not on every hourly cron run. Sources match what
|
||||
# each installer pulls:
|
||||
# - codex: github.com/openai/codex releases/latest
|
||||
# (install.sh is .../releases/latest/download/)
|
||||
# - opencode: github.com/sst/opencode releases/latest
|
||||
# (opencode.ai/install installs that release)
|
||||
# - claude: npm @anthropic-ai/claude-code `latest`
|
||||
# (claude.ai/install.sh installs that version)
|
||||
# Fail the build if any lookup fails rather than silently
|
||||
# shipping a stale key (which would skip a real agent update).
|
||||
codex=$(gh api repos/openai/codex/releases/latest --jq .tag_name)
|
||||
opencode=$(gh api repos/sst/opencode/releases/latest --jq .tag_name)
|
||||
claude=$(curl -fsSL https://registry.npmjs.org/@anthropic-ai/claude-code/latest | jq -r .version)
|
||||
for v in "$codex" "$opencode" "$claude"; do
|
||||
[ -n "$v" ] && [ "$v" != "null" ] || { echo "::error::failed to resolve an agent version (codex=$codex opencode=$opencode claude=$claude)"; exit 1; }
|
||||
done
|
||||
{
|
||||
echo "codex=$codex"
|
||||
echo "opencode=$opencode"
|
||||
echo "claude=$claude"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
echo "resolved agent versions: codex=$codex opencode=$opencode claude=$claude"
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v7
|
||||
|
|
@ -79,17 +112,20 @@ jobs:
|
|||
# microsandbox's `tar_ingest.rs:427` already accepts the
|
||||
# `application/vnd.oci.image.layer.v1.tar+zstd` media type.
|
||||
outputs: type=registry,push=true,compression=zstd,compression-level=3,force-compression=true
|
||||
# Cache-bust the three agent-installer RUN layers each run
|
||||
# by passing the per-build timestamp as a build-arg the
|
||||
# Dockerfile references right before those RUNs. Without
|
||||
# this, `cache-from: type=gha` matches the invariant
|
||||
# `RUN curl … install.sh | bash` instruction strings and
|
||||
# the hourly cron silently reused the same baked-in
|
||||
# agent binaries instead of fetching new releases (the
|
||||
# whole point of the schedule). The heavy apt/chromium/
|
||||
# docker/node layers above the ARG stay cached.
|
||||
# Per-agent version cache keys. `Resolve agent versions`
|
||||
# above looks up each agent's current upstream version; the
|
||||
# Dockerfile references these in the matching installer RUN,
|
||||
# so a layer is rebuilt ONLY when that agent actually
|
||||
# released — not on every hourly cron run. This keeps an
|
||||
# unchanged hourly build a pure cache hit (identical
|
||||
# agent-layer digests → `:latest` consumers re-pull nothing)
|
||||
# instead of re-emitting ~200 MiB of fresh-but-identical
|
||||
# agent layers every hour. The heavy apt/chromium/docker/
|
||||
# node layers below the args stay cached regardless.
|
||||
build-args: |
|
||||
AGENT_INSTALL_CACHEBUST=${{ steps.ts.outputs.tag }}
|
||||
AGENT_VERSION_CODEX=${{ steps.ver.outputs.codex }}
|
||||
AGENT_VERSION_OPENCODE=${{ steps.ver.outputs.opencode }}
|
||||
AGENT_VERSION_CLAUDE=${{ steps.ver.outputs.claude }}
|
||||
# `cache-from`/`cache-to` keep layer rebuilds fast when only
|
||||
# the agent-version layers at the top of the Dockerfile
|
||||
# change (the common hourly case).
|
||||
|
|
|
|||
|
|
@ -15,13 +15,15 @@
|
|||
# deliberately deferred too.
|
||||
#
|
||||
# Layer policy: the file is ordered so the LAST RUN steps are the
|
||||
# smallest and the most frequently invalidated. The hourly cron
|
||||
# bumps the three agent installers (and the sanity check), and the
|
||||
# `AGENT_INSTALL_CACHEBUST` ARG ensures Docker rebuilds those
|
||||
# layers. Everything earlier — Debian base, chromium, docker
|
||||
# engine, Python lint pins, codestyle clone — stays cached across
|
||||
# hourly rebuilds, so users pulling `:latest` after a cron run
|
||||
# only re-download ~tens of MB instead of the full image.
|
||||
# agent installers (the most frequently updated content). Each is
|
||||
# keyed on its agent's upstream version via a per-agent
|
||||
# `AGENT_VERSION_*` ARG (see below), so the hourly cron rebuilds an
|
||||
# installer layer ONLY when that agent actually released — an
|
||||
# unchanged build is a pure cache hit. Everything earlier — Debian
|
||||
# base, chromium, docker engine, Python lint pins, codestyle clone —
|
||||
# stays cached across rebuilds, so users pulling `:latest` re-download
|
||||
# only the layer(s) for whichever agent actually changed (and nothing
|
||||
# at all when none did).
|
||||
#
|
||||
# Published to ghcr.io/wirenboard/agent-vm-template:latest by the
|
||||
# hourly CI workflow (.github/workflows/build-image.yml). Source-
|
||||
|
|
@ -487,18 +489,23 @@ RUN git clone --depth=1 --branch "${CODESTYLE_REF}" \
|
|||
|| { msg="==> wirenboard/codestyle clone FAILED (ref=${CODESTYLE_REF}) — re-clone in-VM with: git clone https://github.com/wirenboard/codestyle.git /opt/wirenboard/codestyle"; \
|
||||
if [ -n "${AGENT_INSTALL_SOFT_FAIL:-}" ]; then echo "$msg (soft-fail mode)"; else echo "$msg" >&2; exit 1; fi; }
|
||||
|
||||
# Cache-bust for the three agent installers below. The workflow
|
||||
# passes the hourly timestamp tag as the value so each scheduled
|
||||
# build invalidates exactly these RUN layers (and the sanity check
|
||||
# that follows) while leaving every earlier layer — Debian base,
|
||||
# chromium, docker engine, WB build deps, cross toolchains, Python
|
||||
# lint pins, codestyle clone — cached. Without this the GHA layer
|
||||
# cache reuses the `curl … install.sh | bash` layers indefinitely,
|
||||
# so hourly cron runs never picked up new claude-code/codex/
|
||||
# opencode releases. Local `images/build.sh` builds leave the
|
||||
# default empty value and get the normal Docker layer-cache
|
||||
# behaviour.
|
||||
ARG AGENT_INSTALL_CACHEBUST=
|
||||
# Per-agent cache keys (the three `ARG AGENT_VERSION_*` below, one
|
||||
# per installer RUN). The workflow resolves each agent's current
|
||||
# UPSTREAM version and passes it in, so an installer layer is rebuilt
|
||||
# only when that agent actually released — not on every hourly cron
|
||||
# run.
|
||||
#
|
||||
# This replaces the old single `AGENT_INSTALL_CACHEBUST=<timestamp>`
|
||||
# arg, which changed every hour and re-emitted all three
|
||||
# `curl … install.sh | bash` layers (plus the sanity check) on every
|
||||
# build. Because those installs aren't reproducible, each rebuild
|
||||
# produced fresh layer blobs → a brand-new ~200 MiB-of-agent-layers
|
||||
# image every hour even when no agent version had changed, so
|
||||
# `:latest` consumers re-downloaded those layers hourly for nothing.
|
||||
# Keying on the real version makes an unchanged hourly build a pure
|
||||
# cache hit (identical layer digests → nothing to re-pull). Local
|
||||
# `images/build.sh` builds leave the defaults empty and get normal
|
||||
# Docker layer-cache behaviour.
|
||||
|
||||
# Shared helper for the three agent installer RUNs below. Downloads
|
||||
# the upstream `install.sh`, runs it under the requested shell, and
|
||||
|
|
@ -546,11 +553,25 @@ RUN printf '%s\n' \
|
|||
&& chmod 0755 /usr/local/bin/agent-vm-install \
|
||||
&& test -s /usr/local/bin/agent-vm-install
|
||||
|
||||
# The three agents each get their own RUN so a single agent
|
||||
# release only invalidates one layer (~30 MB per agent). Order is
|
||||
# rough size order (smallest first) so the steady-state worst-
|
||||
# case download — a fresh release for Claude Code (the biggest)
|
||||
# — only re-pulls the topmost layer.
|
||||
# The three agents each get their own RUN + their own version arg so
|
||||
# a single agent's release rebuilds only that layer (and, by Docker's
|
||||
# layer cascade, the layers stacked ABOVE it).
|
||||
#
|
||||
# Layer order is deliberate and is NOT by size. A change to any layer
|
||||
# forces every layer above it to rebuild, so the TOPMOST agent layer
|
||||
# is re-emitted on essentially every build that changes anything,
|
||||
# while the BOTTOM agent layer is re-emitted only when it itself
|
||||
# bumps. The agent that is both largest and most frequently released
|
||||
# therefore belongs at the BOTTOM, not the top:
|
||||
# - codex ~95 MiB, multiple stable cuts/day → bottom (first)
|
||||
# - opencode ~50 MiB, several per week → middle
|
||||
# - claude ~68 MiB, ~daily → top (last)
|
||||
# This minimizes expected re-pushed bytes per changed build across
|
||||
# the plausible release-frequency range; putting the big codex layer
|
||||
# on top (so it re-pushes on every change) is the worst case. Each
|
||||
# RUN references its version arg (`: "${AGENT_VERSION_*}"`) so
|
||||
# BuildKit ties the layer's cache key to the version string and
|
||||
# rebuilds it ONLY when that string changes.
|
||||
#
|
||||
# Agent installers HARD-FAIL by default — if an upstream installer
|
||||
# is broken or removed, the hourly cron should NOT silently ship an
|
||||
|
|
@ -559,20 +580,26 @@ RUN printf '%s\n' \
|
|||
# sets it) turns the three RUNs into warnings instead.
|
||||
|
||||
# Codex CLI official installer.
|
||||
RUN agent-vm-install codex sh \
|
||||
ARG AGENT_VERSION_CODEX=
|
||||
RUN : "codex ${AGENT_VERSION_CODEX}" \
|
||||
&& agent-vm-install codex sh \
|
||||
https://github.com/openai/codex/releases/latest/download/install.sh
|
||||
|
||||
# OpenCode official installer. Installs into ~/.opencode/bin; PATH already
|
||||
# includes it. Symlink into /usr/local/bin so non-login shells find it too.
|
||||
# The symlink is best-effort — under soft-fail mode the install may have
|
||||
# been skipped, in which case there's nothing to link.
|
||||
RUN agent-vm-install opencode bash https://opencode.ai/install \
|
||||
ARG AGENT_VERSION_OPENCODE=
|
||||
RUN : "opencode ${AGENT_VERSION_OPENCODE}" \
|
||||
&& agent-vm-install opencode bash https://opencode.ai/install \
|
||||
&& { [ -x /root/.opencode/bin/opencode ] \
|
||||
&& ln -sf /root/.opencode/bin/opencode /usr/local/bin/opencode \
|
||||
|| true; }
|
||||
|
||||
# Claude Code official installer.
|
||||
RUN agent-vm-install claude bash https://claude.ai/install.sh
|
||||
ARG AGENT_VERSION_CLAUDE=
|
||||
RUN : "claude ${AGENT_VERSION_CLAUDE}" \
|
||||
&& agent-vm-install claude bash https://claude.ai/install.sh
|
||||
|
||||
# Sanity check at build time. Every command is invoked directly
|
||||
# (`tool --version >/dev/null`) so a present-but-broken binary —
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue