Compare commits

...

20 Commits

Author SHA1 Message Date
joakimp a16da2f041 Cut v1.15.11b — pin setup-buildx-action@v4.0.0
Validate / docs-check (push) Successful in 6s
Validate / base-change-warning (push) Successful in 6s
Validate / validate-with-pi (push) Failing after 4m1s
Publish Docker Image / base-decide (push) Successful in 8s
Publish Docker Image / resolve-versions (push) Successful in 5s
Validate / validate-omos-with-pi (push) Failing after 4m52s
Validate / validate-omos (push) Failing after 6m41s
Validate / validate-base (push) Failing after 8m55s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Publish Docker Image / build-base (push) Failing after 37m43s
Publish Docker Image / smoke-base (push) Has been skipped
Publish Docker Image / smoke-omos (push) Has been skipped
Publish Docker Image / smoke-with-pi (push) Has been skipped
Publish Docker Image / build-variant-omos (push) Has been skipped
Publish Docker Image / build-variant-with-pi (push) Has been skipped
Publish Docker Image / smoke-omos-with-pi (push) Has been skipped
Publish Docker Image / build-variant-base (push) Has been skipped
Publish Docker Image / build-variant-omos-with-pi (push) Has been skipped
The v1.15.11 publish failed three times in a row (runs #332/333/334)
with identical '400 Bad request' from registry-1.docker.io on the
multi-arch buildx layer-blob PUT. Triage on 2026-05-27 confirmed:

  - Multi-arch buildx push from a developer host: succeeds in 25s
    (same Hub account, same multi-arch path)
  - Account / repo / Hub-CDN: all healthy
  - Last known-good Gitea-runner Hub push: 2026-05-23 ~20:26 UTC
    (pi-devbox v0.75.5b) — predates docker/setup-buildx-action v4.1.0
    by <24h
  - docker/setup-buildx-action@v4 floats to v4.1.0 (published
    2026-05-22 16:00 UTC), bundling a newer buildx/buildkit whose
    push protocol may trip Hub's CDN URI-length cap on the ~1.4 KB
    _state query string in resumable-upload PUT URLs.

Pinning all nine setup-buildx-action references to @v4.0.0 to
test the hypothesis. setup-qemu-action@v3 left floating since
QEMU wasn't in the suspected blast radius. If v4.0.0 publishes
cleanly we keep the pin and file an upstream buildkit/buildx
issue.

No source changes — same OPENCODE_VERSION=1.15.11, same Dockerfile.base
and Dockerfile.variant. v1.15.11 (original tag) is preserved as a
historical marker of the first publish attempt; v1.15.11b becomes
the canonical release.
2026-05-27 21:05:17 +00:00
joakimp 608304c3de Bump opencode 1.15.10 -> 1.15.11 + cut v1.15.11
Publish Docker Image / base-decide (push) Successful in 9s
Publish Docker Image / resolve-versions (push) Successful in 5s
Validate / base-change-warning (push) Successful in 5s
Validate / docs-check (push) Successful in 49s
Validate / validate-with-pi (push) Failing after 4m8s
Validate / validate-omos (push) Failing after 4m53s
Validate / validate-base (push) Failing after 5m22s
Validate / validate-omos-with-pi (push) Failing after 14m49s
Publish Docker Image / build-base (push) Failing after 30m39s
Publish Docker Image / smoke-base (push) Has been skipped
Publish Docker Image / smoke-omos (push) Has been skipped
Publish Docker Image / build-variant-omos (push) Has been skipped
Publish Docker Image / build-variant-base (push) Has been skipped
Publish Docker Image / smoke-with-pi (push) Has been skipped
Publish Docker Image / build-variant-with-pi (push) Has been skipped
Publish Docker Image / smoke-omos-with-pi (push) Has been skipped
Publish Docker Image / build-variant-omos-with-pi (push) Has been skipped
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
First release on opencode 1.15.11. Also ships the four devbox-side fixes
accumulated since v1.15.10:

  - 668592d Base: SSH ControlMaster default on a writable socket path
  - 73a7f96 Base: gitleaks added; git-crypt confirmed installed
  - 3cbcb44 CI: fix resolve-versions to use curl+jq instead of npm view
  - f7c3409 CI: preventative fix for PI_VERSION/OMOS_VERSION cache-hit regression

Downstream pi-devbox inherits all of these on its next build against
base-latest.

Upstream release notes:
  https://github.com/anomalyco/opencode/releases/tag/v1.15.11
2026-05-27 15:02:24 +00:00
joakimp 668592da0d Base: SSH ControlMaster default on a writable socket path
Validate / docs-check (push) Successful in 9s
Validate / base-change-warning (push) Successful in 11s
Validate / validate-with-pi (push) Failing after 4m6s
Validate / validate-omos (push) Failing after 4m31s
Validate / validate-omos-with-pi (push) Failing after 4m52s
Validate / validate-base (push) Failing after 13m20s
Devboxes typically mount ~/.ssh from the host read-only (security: keys
readable, but agents can't tamper with config / known_hosts /
authorized_keys / plant a malicious ProxyCommand). OpenSSH's default
ControlPath is ~/.ssh/cm/... which is unwritable on such mounts, so
any attempt to multiplex fails with:

  unix_listener: cannot bind to path .../cm/...: Read-only file system
  kex_exchange_identification: Connection closed by remote host

The second line is downstream — when ControlMaster fails, ssh falls
back to fresh TCP connections, and on residential CGNAT (most European
ISPs) the per-(src,dst) concurrent-flow cap (~4) silently drops further
SYNs once exceeded, manifesting as banner-exchange timeouts that look
like a remote problem.

Fix: bake /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf in the
base image with Host * defaults — ControlPath rooted at /tmp/sshcm/
(per-container, always writable), ControlMaster auto, ControlPersist
10m, ServerAlive{Interval=30,CountMax=6}. Companion entrypoint-user.sh
creates /tmp/sshcm mode 700 on each container start (/tmp is
per-container so the dir can't be baked into a layer; mode 700 is
required by OpenSSH for ControlPath dirs). Debian's stock ssh_config
sources ssh_config.d/*.conf before its own Host * block, so user
~/.ssh/config overrides still win.

Two smoke assertions catch regressions: (a) the conf file exists, (b)
ssh -G reports a controlpath rooted at /tmp/sshcm/ — second one catches
the silent case where something later in the config chain shadows the
bake-in.

Discovered while running a recon shell from inside pi-devbox to a
Proxmox node — fresh ssh hit banner-exchange timeout, debug output
pointed at the read-only socket dir as the actual root cause.

Cascades to all variants and to pi-devbox automatically on next build
against base-latest. No size/threshold impact (~250-byte conf file).
2026-05-24 19:51:38 +00:00
joakimp 3cbcb44cf5 CI: fix resolve-versions to use curl+jq instead of npm view
catthehacker/ubuntu:act-latest ships Node/npm under /opt/acttoolcache/
with PATH updated only in /etc/environment. act_runner (nektos/act) does
not source /etc/environment — it reads the Docker image's ENV instructions
(inspectResult.Config.Env) which only contain DEBIAN_FRONTEND=noninteractive.
So npm is NOT on PATH and 'npm view ...' would have CI-failed on first run.

Fix: query the npm registry HTTP API directly with curl+jq, both of which
are already used extensively by this workflow (curl for Hub auth/manifest
inspect, jq for token parsing). The endpoint
  https://registry.npmjs.org/<pkg>/latest
returns JSON with a 'version' field — equivalent to 'npm view <pkg> version'
but with no toolchain dependency.

Verified locally: both URLs resolve correctly to 0.75.5 (pi) and 1.1.1 (omos).

Evidence: nektos/act pkg/container/docker_run.go reads imageEnv from
inspectResult.Config.Env, not /etc/environment. DefaultPathVariable() in
linux_container_environment_extensions.go returns a hardcoded path with no
/opt/acttoolcache in it.
2026-05-24 15:59:53 +00:00
joakimp 73a7f96056 Base: add gitleaks; surface git-crypt in smoke + docs
Both tools are used as part of the secret-management setup in several
of the repos this devbox operates on (gitleaks pre-commit hook +
git-crypt for selectively-encrypted canonical config). Having them in
the container means hooks fire correctly inside instead of warning
'gitleaks not installed' on every commit.

git-crypt was already installed via apt in Dockerfile.base (line 58),
just unasserted by smoke and unmentioned in user-facing docs.

gitleaks is new: Go-compiled binary fetched from GitHub releases via
the same /releases/latest redirect-resolution pattern as gosu, fzf,
git-lfs, etc. Arch suffix is 'x64' (not 'x86_64' / 'amd64') on this
project — flagged in the Dockerfile comment and in AGENTS.md's
floated-binaries gotcha list.

Adds ~21 MB to the base layer (gitleaks 8.30.1 binary). No variant
threshold bumps needed (2500–3700 MB envelope, 21 MB is noise).

CHANGES

Dockerfile.base — new GITLEAKS_VERSION=latest ARG + install RUN
right after the git-lfs block. Multi-arch (linux/amd64=x64,
linux/arm64=arm64). Echoes resolved version + runs 'gitleaks version'
to fail the build on any install error.

scripts/smoke-test.sh — git-crypt and gitleaks added to the
'Resolved component versions' table (printed first thing in CI logs)
and to the 'Core binaries' assertion list (run helper). Smoke now
fails fast if either binary regresses.

README.md — 'What's in the image' tree line names gitleaks alongside
the existing git-crypt.

AGENTS.md — gitleaks added to the 'GitHub-sourced binaries float by
default' list with a new clause flagging project-specific arch-name
deviations (gitleaks=x64, bat/eza/zoxide=x86_64/aarch64, gosu=
amd64/arm64). Saves the next person from the 'why does this not
download' debugging session.

CHANGELOG.md — sub-entry under existing Unreleased, before the
PI_VERSION/OMOS_VERSION cache-hit fix entry.

DOWNSTREAM IMPACT

This is a base-layer change — base-decide will compute a fresh
base-<hash>, build-base will run (no cache hit), all four variants
will rebuild. First real base rebuild since v1.14.50b. Pi-devbox's
next FROM base-latest pull picks up gitleaks automatically with no
Dockerfile change there.

Verified end-to-end on host: gitleaks 8.30.1 21 MB binary extracts
cleanly from the URL the Dockerfile constructs and 'gitleaks version'
prints '8.30.1'.

Holding off on tagging — opencode + pi upstreams unchanged at 1.15.10
and 0.75.5 respectively. Will ride along with the next upstream-bump
release rather than burning a base rebuild on a no-upstream-change
container-only roll.
2026-05-24 15:49:38 +00:00
joakimp f7c34091b1 CI: preventative fix for PI_VERSION/OMOS_VERSION cache-hit silent regression
Mirrors the pi-devbox v0.75.5b fix (2026-05-23) onto the four-variant
pipeline here. The with-pi, omos, and omos-with-pi variants install
upstream npm packages whose *_VERSION build-args defaulted to 'latest'.
When the build-arg string is byte-identical across builds, the layer
hash is identical and the registry buildcache silently reuses the layer
from whatever upstream version was current when the cache was first
populated — same mechanism that shipped pi-devbox v0.74.0..v0.75.5 with
identical image bytes.

Currently masked here because OPENCODE_VERSION is a hard-coded ARG that
bumps every release; parent-chain cache invalidation flushes the
downstream pi/omos layers. Masking would fail on any vN.N.Nb opencode-
version-unchanged release that only bumps pi or omos. Filed last night
as parked followup; fixing preventatively now that #5 (AWS SSO inside
tor-ms22 container) cleared.

CHANGES

.gitea/workflows/docker-publish-split.yml — new resolve-versions job
running 'npm view @earendil-works/pi-coding-agent version' and
'npm view oh-my-opencode-slim version', exposing concrete strings as
job outputs. All six affected jobs (smoke-omos, smoke-with-pi,
smoke-omos-with-pi, build-variant-omos, build-variant-with-pi,
build-variant-omos-with-pi) now consume them as PI_VERSION /
OMOS_VERSION build-args. smoke-base / build-variant-base unaffected.

scripts/smoke-test.sh — new run_expect helper asserting an expected
substring in command output. The pi check uses EXPECTED_PI_VERSION;
the omos check uses EXPECTED_OMOS_VERSION against npm ls -g. Both env
vars are wired from resolve-versions outputs in the smoke jobs. Catches
this regression class on the next release, not four releases later.

Dockerfile.variant — comment blocks above OPENCODE_VERSION (source-
pinned, not subject to the bug), PI_VERSION (CI-resolved), and
OMOS_VERSION (CI-resolved) explaining the cache-hit footgun.

AGENTS.md — new convention bullet under 'Critical conventions' naming
the resolve-versions job + EXPECTED_*_VERSION wiring as the contract
to keep in lockstep when modifying variant build-args.

.gitea/README.md — Step 1 expanded to cover the parallel resolve-
versions job alongside base-decide; pipeline diagram updated.

CHANGELOG.md — Unreleased entry describing the fix, masking mechanism,
and audit footprint.

No image-content change expected on the next release vs what 'latest'
would have resolved to anyway. Purely makes the cache invalidate
correctly going forward.
2026-05-24 15:38:36 +00:00
joakimp 4cce39d167 AGENTS: add 'Upstream sources' section pointing at anomalyco/opencode
Validate / base-change-warning (push) Successful in 14s
Validate / docs-check (push) Successful in 16s
Validate / validate-base (push) Successful in 3m39s
Validate / validate-with-pi (push) Successful in 4m8s
Validate / validate-omos (push) Successful in 6m46s
Validate / validate-omos-with-pi (push) Successful in 13m35s
Publish Docker Image / build-base (push) Has been cancelled
Publish Docker Image / smoke-base (push) Has been cancelled
Publish Docker Image / smoke-omos (push) Has been cancelled
Publish Docker Image / smoke-with-pi (push) Has been cancelled
Publish Docker Image / smoke-omos-with-pi (push) Has been cancelled
Publish Docker Image / build-variant-base (push) Has been cancelled
Publish Docker Image / build-variant-omos (push) Has been cancelled
Publish Docker Image / build-variant-with-pi (push) Has been cancelled
Publish Docker Image / build-variant-omos-with-pi (push) Has been cancelled
Publish Docker Image / promote-base-latest (push) Has been cancelled
Publish Docker Image / update-description (push) Has been cancelled
Publish Docker Image / base-decide (push) Failing after 14m23s
Tonight's v1.15.10 release surfaced a documentation drift footgun: I
checked github.com/sst/opencode (a fork) for release notes instead of
the canonical github.com/anomalyco/opencode. Empty bodies on sst led
me to write 'upstream releases ship empty bodies and no CHANGELOG'
in the v1.15.10 CHANGELOG, which was wrong — anomalyco's release
pages have rich Core/TUI/Desktop/SDK sections.

Added a new 'Upstream sources — where to look up release notes'
section between 'Versioning scheme' and 'Critical conventions',
documenting:
  - Canonical upstream for opencode-ai (anomalyco/opencode), pi
    (npm tarball CHANGELOG.md), other floated tools.
  - The sst/opencode trap explicitly named so future-pi doesn't
    repeat the mistake.
  - Working fetch commands as muscle memory: 'npm view ... time'
    for latest stable filtering, 'curl /releases/tags/' for body,
    'npm pack' for pi's changelog.

No CI implications, doc-only.
2026-05-23 19:26:46 +02:00
joakimp 72d2c99885 docs: enrich v1.15.10 CHANGELOG with actual upstream release notes
The v1.15.10 release commit (80e57d7) said upstream releases ship
empty bodies — that was incorrect. I was checking sst/opencode which
is a fork; the canonical upstream this devbox tracks is
github.com/anomalyco/opencode (per user correction). Three of the
four versions (1.15.7, 1.15.9, 1.15.10) have rich release notes;
only 1.15.8 is empty.

Expanded the v1.15.10 CHANGELOG entry with per-version summaries:

- v1.15.7: Grok OAuth + device-code login, v2 session API safe-error
  responses with reference IDs, Codex OAuth refresh dedup, restored
  OpenAI OAuth + reasoning streams, friendly tool-schema errors,
  Grok PDF attachments, several TUI and desktop improvements.

- v1.15.8: empty release body (assumed internal/no user-visible).

- v1.15.9: redesigned diff viewer with file tree + enabled by default,
  MCP OAuth callbackPort and scope-in-clientMetadata, Vertex Anthropic
  multi-region endpoint fix, many 'show clearer error' improvements,
  native reasoning metadata preservation across turns, TUI worktree-
  copy shortcut, desktop titlebar tab navigation.

- v1.15.10: single fix — restored legacy production desktop flows
  for opening projects and starting sessions.

Doc-only commit on main. The v1.15.10 tag snapshot is unchanged
because CI is mid-flight against it (5 jobs running at the time of
this correction); cancelling and re-tagging would cost ~50 min of
CI re-run for changelog wording with no image-content effect.
The Hub description for v1.15.10 will reflect the thin tag-snapshot
CHANGELOG; main has the correct content for the next release and
for anyone reading current docs.
2026-05-23 19:19:19 +02:00
joakimp 80e57d732b Bump opencode 1.15.6 -> 1.15.10 + cut v1.15.10
Validate / validate-base (push) Waiting to run
Validate / validate-with-pi (push) Waiting to run
Validate / docs-check (push) Successful in 15s
Validate / base-change-warning (push) Successful in 59s
Validate / validate-omos (push) Successful in 4m33s
Validate / validate-omos-with-pi (push) Successful in 7m50s
Publish Docker Image / base-decide (push) Failing after 10m22s
Publish Docker Image / build-base (push) Has been cancelled
Publish Docker Image / smoke-base (push) Has been cancelled
Publish Docker Image / smoke-omos (push) Has been cancelled
Publish Docker Image / smoke-with-pi (push) Has been cancelled
Publish Docker Image / smoke-omos-with-pi (push) Has been cancelled
Publish Docker Image / build-variant-base (push) Has been cancelled
Publish Docker Image / build-variant-omos (push) Has been cancelled
Publish Docker Image / build-variant-with-pi (push) Has been cancelled
Publish Docker Image / build-variant-omos-with-pi (push) Has been cancelled
Publish Docker Image / promote-base-latest (push) Has been cancelled
Publish Docker Image / update-description (push) Has been cancelled
Four upstream patch releases over two days. Upstream releases ship
empty bodies and no CHANGELOG; the patch sequence (1.15.7-1.15.10)
is fixes only per typical sst/opencode cadence.

The with-pi and omos-with-pi variants will also implicitly bump
pi 0.75.4 -> 0.75.5 since PI_VERSION=latest resolves at build time.

omos-with-pi smoke threshold remains 3700 MB (set v1.15.4b 2026-05-18).
Four opencode patches plus one pi patch typically add only a few MB
across both, not expected to trip. If it does, recovery is the
well-worn letter-suffix pattern (v1.15.10b with threshold bump).

Cache hit expected on base-35ee5fe7861a since neither Dockerfile.base
nor rootfs/ have changed since v1.14.50b. Built on the same CI path
as v1.15.6 — pinned-crane install (T14), skip-promote-on-cache-hit
(T15), and update-description-always-on-base-success (v1.15.4b)
all expected to remain quiet on this cache-hit run.
2026-05-23 19:14:58 +02:00
joakimp 19f8c043bd Bump opencode 1.15.4 -> 1.15.6 + cut v1.15.6
Validate / docs-check (push) Successful in 14s
Validate / base-change-warning (push) Successful in 10s
Validate / validate-base (push) Successful in 3m33s
Validate / validate-omos-with-pi (push) Successful in 4m57s
Validate / validate-with-pi (push) Successful in 6m18s
Validate / validate-omos (push) Successful in 12m13s
Publish Docker Image / base-decide (push) Successful in 13s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke-base (push) Successful in 3m27s
Publish Docker Image / smoke-omos (push) Successful in 4m29s
Publish Docker Image / smoke-with-pi (push) Successful in 6m13s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 12m31s
Publish Docker Image / build-variant-base (push) Successful in 14m33s
Publish Docker Image / build-variant-omos (push) Successful in 19m38s
Publish Docker Image / build-variant-with-pi (push) Successful in 19m0s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 30m37s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Successful in 9s
Two upstream patch releases since v1.15.4b. Plus this release picks up
two workflow improvements that landed on main between v1.15.4b and
now (b6e4d89 pycache/DS_Store filter in base-decide, 90e5a1f doc-drift
sweep clause in AGENTS.md). No image-content changes beyond the
version bump; cache hit expected on base-35ee5fe7861a since neither
Dockerfile.base nor rootfs/ have changed.

The with-pi and omos-with-pi variants will also implicitly bump pi
0.75.3 -> 0.75.4 because PI_VERSION=latest resolves at build time.
The omos-with-pi smoke threshold (3700 MB after the v1.15.4b bump)
should accommodate two opencode patch versions plus one pi patch
without recurrence of the trip; a future bump-bump pattern would
push it again.

First release on the new CI path that exercises:
- pinned crane install (T14, v1.15.3) - only fires on real base rebuild,
  cache-hit on base means it stays unexercised this run too
- skip promote-base-latest on cache-hit (T15, v1.15.4) - active
- update-description always-and-success-of-base wrap (v1.15.4b) -
  active, will run since base variant publishes
2026-05-21 00:09:15 +02:00
joakimp 90e5a1f5d0 AGENTS.md: documentation-drift sweep as explicit pre-commit step
Validate / docs-check (push) Successful in 8s
Validate / base-change-warning (push) Successful in 12s
Validate / validate-omos (push) Successful in 4m20s
Validate / validate-omos-with-pi (push) Successful in 7m32s
Validate / validate-base (push) Successful in 9m25s
Validate / validate-with-pi (push) Failing after 14m46s
Companion to the same addition in the cloud-init and ansible repos.
Caught real drift in those repos in a recent session only because
the user explicitly asked. Codify the sweep with concrete, repo-
specific drift hotspots rather than a vague 'watch for drift' rule
that gets ignored.

Each AGENTS.md addition lists the doc files most likely to fall
behind code changes here, plus a quick-triage one-liner using
'git diff --name-only HEAD | xargs grep -l ...' so the rule is
actionable not aspirational.
2026-05-20 23:11:57 +02:00
joakimp b6e4d89a2c ci: filter __pycache__ and macOS metadata from base hash compute
Validate / docs-check (push) Successful in 14s
Validate / base-change-warning (push) Successful in 18s
Validate / validate-omos (push) Successful in 4m34s
Validate / validate-omos-with-pi (push) Successful in 4m57s
Validate / validate-with-pi (push) Successful in 6m9s
Validate / validate-base (push) Successful in 14m48s
Defensive against local-vs-CI hash divergence. `find rootfs -type f`
includes gitignored junk like rootfs/__pycache__/*.pyc and macOS
.DS_Store/._AppleDouble files, which CI's clean checkout never sees.

This bit us during v1.15.4 debugging when a stale generate-config.cpython-314.pyc
on the local rootfs/ produced base-3605aa6b6ab1 while CI computed
base-35ee5fe7861a. Took meaningful time to track down because git status
doesn't surface gitignored files.

Verified: same filter applied to current clean tree still produces
35ee5fe7861a (the published v1.15.4b base digest).
2026-05-20 22:45:27 +02:00
joakimp 8f2c9f5112 v1.15.4b: omos-with-pi threshold bump + update-description partial-publish fix
Validate / docs-check (push) Successful in 7s
Validate / base-change-warning (push) Successful in 20s
Validate / validate-base (push) Successful in 3m36s
Publish Docker Image / base-decide (push) Successful in 13s
Publish Docker Image / build-base (push) Has been skipped
Validate / validate-with-pi (push) Successful in 4m14s
Validate / validate-omos (push) Successful in 7m1s
Publish Docker Image / smoke-base (push) Successful in 3m37s
Publish Docker Image / smoke-omos (push) Successful in 4m39s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 5m7s
Publish Docker Image / smoke-with-pi (push) Successful in 6m24s
Validate / validate-omos-with-pi (push) Successful in 15m59s
Publish Docker Image / build-variant-base (push) Successful in 14m12s
Publish Docker Image / build-variant-omos (push) Successful in 19m29s
Publish Docker Image / build-variant-with-pi (push) Successful in 23m7s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 26m16s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Successful in 8s
Recovery for v1.15.4's partial publish (omos-with-pi exceeded 3500 MB
smoke threshold; other 3 variants published cleanly). Two changes:

1. omos-with-pi threshold 3500 -> 3700 MB. Compounded growth from
   opencode 1.15.0 -> 1.15.4 (4 patch versions) plus pi 0.74.0 -> 0.75.3
   (minor + 3 patches) summed in the omos-with-pi variant, just over
   the existing limit. Same pattern as prior threshold bumps (v1.14.31c,
   v1.15.0b). Restores ~150 MB headroom for routine apt-upgrade drift.

2. update-description workflow bug fix. Pre-existing latent bug exposed
   by v1.15.4's partial publish: update-description.needs includes all 4
   build-variant-* jobs, and gitea Actions' default behavior is
   'skipped need => skip dependent' \u2014 even when the job's own if:
   condition is satisfied. So when build-variant-omos-with-pi was
   skipped (because its smoke failed), update-description cascaded into
   a skip too, and Hub description didn't refresh on v1.15.4 despite
   3 variants publishing.

   Fix: wrap if: in always() + explicit success check on the base
   variant. Same fix applied to promote-base-latest preemptively (it
   has the same latent bug, currently masked by the cache-hit gate).

No image-side changes \u2014 cache hit on base-35ee5fe7861a.
2026-05-18 22:30:59 +02:00
joakimp 60eb49469e v1.15.4: bump opencode 1.15.3 -> 1.15.4
Publish Docker Image / base-decide (push) Successful in 14s
Validate / docs-check (push) Successful in 8s
Validate / base-change-warning (push) Successful in 7s
Validate / validate-base (push) Successful in 3m30s
Publish Docker Image / build-base (push) Has been skipped
Validate / validate-with-pi (push) Successful in 4m12s
Validate / validate-omos (push) Successful in 7m8s
Validate / validate-omos-with-pi (push) Successful in 5m7s
Publish Docker Image / smoke-omos (push) Successful in 4m23s
Publish Docker Image / smoke-base (push) Successful in 8m17s
Publish Docker Image / smoke-with-pi (push) Successful in 6m24s
Publish Docker Image / smoke-omos-with-pi (push) Failing after 11m14s
Publish Docker Image / build-variant-base (push) Successful in 14m38s
Publish Docker Image / build-variant-omos-with-pi (push) Has been skipped
Publish Docker Image / build-variant-omos (push) Successful in 19m40s
Publish Docker Image / build-variant-with-pi (push) Successful in 19m49s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Bundles with the CI hardening landed on main since v1.15.3 (T14/T15 in
the operator backlog):

- Pinned crane install in promote-base-latest (replaces flaky
  imjasonh/setup-crane@v0.4 that depends on api.github.com/releases/latest
  at runtime and periodically rate-limits)
- Skip promote-base-latest on cache-hit base builds (need_build='false')

These will be exercised on this release run \u2014 if the base hash hasn't
drifted since v1.15.3 (likely cache hit), promote-base-latest should
SKIP rather than RUN, and update-description picks up the new tag.
2026-05-18 21:51:15 +02:00
joakimp 18b9c9c549 CI: harden promote-base-latest (pinned crane + skip on cache-hit)
Validate / docs-check (push) Successful in 10s
Validate / base-change-warning (push) Successful in 16s
Validate / validate-with-pi (push) Successful in 4m10s
Validate / validate-omos (push) Successful in 4m34s
Validate / validate-base (push) Has been cancelled
Validate / validate-omos-with-pi (push) Has been cancelled
Two workflow-only changes for promote-base-latest, no image-side impact:

T14 \u2014 replace imjasonh/setup-crane@v0.4 with direct pinned crane install.
The action's bootstrap script calls api.github.com/.../releases/latest
at every run to discover the crane version. That call periodically
rate-limits and returns JSON without .tag_name, jq emits 'null', the
action then downloads .../releases/download/null/... \u2192 404 \u2192 'gzip:
unexpected end of file' \u2192 exit 2. We hit this on the v1.15.3 release
(2026-05-16) where it was cosmetic only \u2014 base-latest was already
correct from cache hit \u2014 but the red-X is annoying.

Replaced with curl + tar pinned to crane v0.21.6 (latest at time of
change). Same pattern as other GitHub-sourced binaries in the
Dockerfile layer (gosu, fzf, eza etc.); operator bumps CRANE_VERSION
deliberately when wanting updates.

T15 \u2014 gate promote-base-latest on need_build == 'true'. When the base
layer's content hash hasn't changed (cache hit on existing base-<hash>
from a prior run), base-latest already points at the correct digest.
The retag is a tautology, and any transient failure of it produces a
red-X for an operation that didn't need to happen. Skipping the job
entirely on cache-hit is correct and removes a whole class of cosmetic
failure. Manual workflow_dispatch with promote_latest=true still bypasses
the gate as an escape hatch (e.g., if base-latest got hand-deleted and
needs regeneration without rebuilding the base).

This will not trigger a CI publish run (main-branch commit, no tag).
2026-05-18 21:45:10 +02:00
joakimp ad4a12b3ab v1.15.3: bump opencode 1.15.0 -> 1.15.3
Validate / base-change-warning (push) Successful in 10s
Validate / docs-check (push) Successful in 17s
Validate / validate-omos (push) Successful in 4m29s
Validate / validate-with-pi (push) Successful in 4m17s
Validate / validate-omos-with-pi (push) Successful in 8m13s
Validate / validate-base (push) Successful in 8m48s
Publish Docker Image / base-decide (push) Successful in 12s
Publish Docker Image / promote-base-latest (push) Failing after 6s
Publish Docker Image / build-base (push) Has been cancelled
Publish Docker Image / smoke-base (push) Has been cancelled
Publish Docker Image / smoke-omos (push) Has been cancelled
Publish Docker Image / smoke-with-pi (push) Has been cancelled
Publish Docker Image / smoke-omos-with-pi (push) Has been cancelled
Publish Docker Image / build-variant-base (push) Has been cancelled
Publish Docker Image / build-variant-omos (push) Has been cancelled
Publish Docker Image / build-variant-with-pi (push) Has been cancelled
Publish Docker Image / build-variant-omos-with-pi (push) Has been cancelled
Publish Docker Image / update-description (push) Has been cancelled
2026-05-16 19:54:15 +02:00
joakimp fde5a89e8b README + DOCKER_HUB: lead with no-git-clone curl-template path
Validate / base-change-warning (push) Successful in 27s
Validate / docs-check (push) Successful in 39s
Validate / validate-omos (push) Successful in 4m39s
Validate / validate-with-pi (push) Successful in 4m14s
Validate / validate-omos-with-pi (push) Successful in 8m7s
Validate / validate-base (push) Successful in 9m50s
The previous Quick Start in both surfaces led with 'git clone',
which is overkill for users who just want to run the published image.
Match pi-devbox's pattern: lead with 'mkdir; curl docker-compose.yml;
curl .env.example; edit .env; docker compose run --rm devbox'. Keep
the git-clone path as 'for hackers/forkers'.

Required pre-step: make the gitea repo public so unauthenticated
curl to the raw URL works (done out of band — repo was private until
this commit landed).
2026-05-15 18:02:37 +02:00
joakimp 034830710c workflow: use github.ref_type directly in promote/update-description if-conditions
Validate / docs-check (push) Successful in 8s
Validate / base-change-warning (push) Successful in 10s
Validate / validate-with-pi (push) Successful in 4m23s
Validate / validate-omos-with-pi (push) Successful in 5m10s
Validate / validate-omos (push) Successful in 7m5s
Validate / validate-base (push) Successful in 10m5s
Gitea Actions evaluates 'env.PROMOTE_LATEST' as empty in YAML 'if:'
contexts even though the same env var substitutes correctly in
shell run: blocks. Result: on v1.15.0/v1.15.0b tag pushes, the
build-variant-* jobs correctly pushed latest-* aliases (shell context),
but promote-base-latest and update-description got skipped (YAML
context), so the Hub README description wasn't refreshed.

Switch to evaluating github.ref_type directly in the if-conditions —
matches the production-trigger semantics and avoids the env-var
indirection that gitea evaluates inconsistently.
2026-05-15 13:50:46 +02:00
joakimp d293ddc202 v1.15.0b: bump omos smoke threshold 3200->3300, omos-with-pi 3400->3500
Validate / base-change-warning (push) Successful in 9s
Validate / docs-check (push) Successful in 18s
Validate / validate-omos (push) Successful in 4m22s
Validate / validate-with-pi (push) Successful in 4m10s
Publish Docker Image / base-decide (push) Successful in 15s
Publish Docker Image / build-base (push) Has been skipped
Validate / validate-base (push) Successful in 5m20s
Publish Docker Image / smoke-base (push) Successful in 3m34s
Publish Docker Image / smoke-with-pi (push) Successful in 4m12s
Publish Docker Image / smoke-omos (push) Successful in 7m2s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 4m58s
Validate / validate-omos-with-pi (push) Successful in 17m33s
Publish Docker Image / build-variant-base (push) Successful in 14m18s
Publish Docker Image / build-variant-with-pi (push) Successful in 19m22s
Publish Docker Image / build-variant-omos (push) Successful in 18m50s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 31m58s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
opencode 1.15.0 grew the omos image to 3206 MB, 6 MB over the existing
3200 MB threshold, causing smoke-omos to fail and build-variant-omos
to be skipped in v1.15.0. Bump thresholds with ~100 MB headroom for
routine apt-get upgrade drift.

No image-side changes — pure smoke threshold update. v1.15.0b will hit
the base hash cache and run only the variant deltas.
2026-05-15 10:35:08 +02:00
joakimp 910378fe06 v1.15.0: opencode bump + git clone retry + pi-devbox sibling mention
Validate / docs-check (push) Successful in 11s
Validate / base-change-warning (push) Successful in 56s
Publish Docker Image / base-decide (push) Successful in 17s
Publish Docker Image / build-base (push) Has been skipped
Validate / validate-base (push) Successful in 3m23s
Publish Docker Image / smoke-base (push) Successful in 3m34s
Validate / validate-omos (push) Successful in 6m52s
Publish Docker Image / smoke-with-pi (push) Successful in 4m10s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 4m58s
Validate / validate-omos-with-pi (push) Failing after 10m27s
Validate / validate-with-pi (push) Failing after 10m38s
Publish Docker Image / smoke-omos (push) Failing after 9m35s
Publish Docker Image / build-variant-omos (push) Has been skipped
Publish Docker Image / build-variant-base (push) Successful in 15m36s
Publish Docker Image / build-variant-with-pi (push) Successful in 16m52s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 22m5s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
- Bump OPENCODE_VERSION 1.14.50 -> 1.15.0 in Dockerfile.variant.
- Wrap pi-toolkit/pi-extensions git clone in Dockerfile.variant in a
  5-attempt retry loop with linear backoff (matches pi-devbox pattern).
  gitea.jordbo.se occasionally returns transient HTTP 500s that
  previously broke with-pi/omos-with-pi variant builds.
- Add 'Sibling images' section to DOCKER_HUB.md mentioning
  joakimp/pi-devbox as the pi-only counterpart.
- CHANGELOG entry for v1.15.0 with full notes.
2026-05-15 09:56:01 +02:00
11 changed files with 561 additions and 75 deletions
+40 -8
View File
@@ -30,10 +30,10 @@ The split-base architecture is what the `docker-publish-split.yml` workflow exer
┌──────────────────┐
│ base-decide │ compute base-<hash>;
│ │ probe Docker Hub.
│ hash inputs: │
│ Dockerfile.base│
│ rootfs/ │
│ entrypoint*.sh │
│ hash inputs: │ (resolve-versions
│ Dockerfile.base│ runs in parallel:
│ rootfs/ │ npm view pi/omos
│ entrypoint*.sh │ → concrete versions)
└────────┬─────────┘
┌─────────────┴─────────────┐
@@ -73,19 +73,28 @@ The split-base architecture is what the `docker-publish-split.yml` workflow exer
└──────────────────────────┘
```
### Step 1: `base-decide`
### Step 1: `base-decide` (and `resolve-versions` in parallel)
Compute a SHA-256 hash over the inputs that determine the base image's
content:
**`base-decide`** computes a SHA-256 hash over the inputs that determine
the base image's content:
```sh
{
cat Dockerfile.base
find rootfs -type f -print0 | sort -z | xargs -0 cat
find rootfs -type f \
! -path '*/__pycache__/*' \
! -name '*.pyc' \
! -name '.DS_Store' \
! -name '._*' \
-print0 | sort -z | xargs -0 cat
cat entrypoint.sh entrypoint-user.sh
} | sha256sum | cut -c1-12
```
Junk filters keep the local recompute reproducible against CI's clean
checkout — `__pycache__/*.pyc` and macOS metadata files (`.DS_Store`,
`._AppleDouble`) are gitignored but still walked by `find -type f`.
The 12-character truncated hash becomes `base-<hash>`. Probe Docker Hub
for this tag via `docker manifest inspect`:
@@ -97,6 +106,29 @@ This is the core cache-reuse mechanism. Version-bump-only releases
that change anything in the base — apt packages, AWS CLI, Node version,
locale list, entrypoint scripts — pay the full base-build cost once.
**`resolve-versions`** runs alongside `base-decide` (no `needs:`
dependency between them) and resolves the floating npm packages whose
`*_VERSION` build-args default to `latest`:
```sh
PI_VERSION=$(npm view @earendil-works/pi-coding-agent version)
OMOS_VERSION=$(npm view oh-my-opencode-slim version)
```
The outputs (`pi_version`, `omos_version`) are consumed by every variant
smoke and build job that installs pi or omos. **Why this exists:** without
it, the `npm install -g` RUN layer in `Dockerfile.variant` hashes
identically across builds (same ARG default, same command string), so
the registry buildcache silently reuses the layer from whatever upstream
version was current when the cache was first populated. This is the
cache-hit silent-regression class of bug that shipped pi-devbox v0.74.0
through v0.75.5 with identical image bytes (fixed in pi-devbox v0.75.5b
2026-05-23). Currently masked here by `OPENCODE_VERSION` bumping every
release (parent-chain cache-key invalidation), but masking would fail on
a `vN.N.Nb` opencode-version-unchanged release that only bumps pi or
omos. Smoke jobs additionally assert `EXPECTED_PI_VERSION` /
`EXPECTED_OMOS_VERSION` against the resolved values.
### Step 2: `build-base` (conditional)
Only runs when `need_build=true`. Multi-arch (amd64 + arm64) build of
+123 -22
View File
@@ -63,10 +63,19 @@ jobs:
run: |
# Hash inputs that determine the base image's contents.
# Order is fixed via `find -print0 | sort -z` for reproducibility.
# Junk filters: __pycache__/*.pyc and macOS metadata (.DS_Store,
# ._AppleDouble) are gitignored locally but still picked up by
# `find rootfs -type f`, which would diverge the local hash from
# CI's clean checkout. Exclude them defensively here.
HASH=$(
{
cat Dockerfile.base
find rootfs -type f -print0 2>/dev/null | sort -z | xargs -0 cat 2>/dev/null
find rootfs -type f \
! -path '*/__pycache__/*' \
! -name '*.pyc' \
! -name '.DS_Store' \
! -name '._*' \
-print0 2>/dev/null | sort -z | xargs -0 cat 2>/dev/null
cat entrypoint.sh entrypoint-user.sh
} | sha256sum | cut -c1-12
)
@@ -93,6 +102,42 @@ jobs:
echo "Base tag ${IMAGE}:${{ steps.compute.outputs.base_tag }} missing — will build."
fi
# ── Phase 1b: resolve floating npm versions (pi, omos) to concrete
# versions so the variant build-args carry a different value when an
# upstream package bumps. Without this, when PI_VERSION / OMOS_VERSION
# default to 'latest', the docker/build-push-action build-arg string
# is byte-identical across builds, so the resulting layer-hash is
# identical, so the registry buildcache silently reuses the layer
# from whatever pi/omos version was current when the cache was first
# populated. Same class of bug as pi-devbox v0.74.0..v0.75.5 (fixed in
# v0.75.5b 2026-05-23). Currently masked here because OPENCODE_VERSION
# is hard-coded in Dockerfile.variant and bumps every release —
# invalidating the parent-chain cache key for the pi/omos layers — but
# that masking would fail the moment we cut a vN.N.Nb opencode-version-
# unchanged release that only bumps pi or omos. Fix is preventative.
resolve-versions:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
outputs:
pi_version: ${{ steps.resolve.outputs.pi_version }}
omos_version: ${{ steps.resolve.outputs.omos_version }}
steps:
- name: Resolve pi + omos versions from npm registry
id: resolve
run: |
set -eu
# Query the npm registry directly via curl+jq rather than `npm view`.
# catthehacker/ubuntu:act-latest ships Node/npm under /opt/acttoolcache/
# and adds it to PATH only via /etc/environment — which act_runner never
# sources (it reads the Docker image's ENV instructions, not /etc/environment).
# curl and jq are both guaranteed present in every job in this workflow.
PI_VERSION=$(curl -sf "https://registry.npmjs.org/@earendil-works%2Fpi-coding-agent/latest" | jq -r '.version')
OMOS_VERSION=$(curl -sf "https://registry.npmjs.org/oh-my-opencode-slim/latest" | jq -r '.version')
echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT"
echo "omos_version=${OMOS_VERSION}" >> "$GITHUB_OUTPUT"
echo "Resolved PI_VERSION=${PI_VERSION}, OMOS_VERSION=${OMOS_VERSION}"
# ── Phase 2: build & push base (multi-arch), only when needed ──────
build-base:
needs: [base-decide]
@@ -129,7 +174,7 @@ jobs:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v4.0.0
with:
driver-opts: network=host
@@ -178,7 +223,7 @@ jobs:
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -202,10 +247,11 @@ jobs:
run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
smoke-omos:
needs: [base-decide, build-base]
needs: [base-decide, build-base, resolve-versions]
if: |
always() &&
needs.base-decide.result == 'success' &&
needs.resolve-versions.result == 'success' &&
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
runs-on: ubuntu-latest
container:
@@ -221,7 +267,7 @@ jobs:
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -240,13 +286,17 @@ jobs:
INSTALL_OPENCODE=true
INSTALL_OMOS=true
INSTALL_PI=false
- run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
- env:
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
smoke-with-pi:
needs: [base-decide, build-base]
needs: [base-decide, build-base, resolve-versions]
if: |
always() &&
needs.base-decide.result == 'success' &&
needs.resolve-versions.result == 'success' &&
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
runs-on: ubuntu-latest
container:
@@ -262,7 +312,7 @@ jobs:
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -281,13 +331,17 @@ jobs:
INSTALL_OPENCODE=true
INSTALL_OMOS=false
INSTALL_PI=true
- run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
- env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi
smoke-omos-with-pi:
needs: [base-decide, build-base]
needs: [base-decide, build-base, resolve-versions]
if: |
always() &&
needs.base-decide.result == 'success' &&
needs.resolve-versions.result == 'success' &&
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
runs-on: ubuntu-latest
container:
@@ -303,7 +357,7 @@ jobs:
/usr/lib/jvm 2>/dev/null || true
docker system prune -af --volumes || true
docker builder prune -af || true
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -322,7 +376,12 @@ jobs:
INSTALL_OPENCODE=true
INSTALL_OMOS=true
INSTALL_PI=true
- run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
- env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
# ── Phase 4: multi-arch publish per variant ────────────────────────
@@ -344,7 +403,7 @@ jobs:
docker builder prune -af || true
- uses: docker/setup-qemu-action@v3
with: {platforms: arm64}
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -375,7 +434,7 @@ jobs:
tags: ${{ steps.tags.outputs.tags }}
build-variant-omos:
needs: [base-decide, smoke-omos]
needs: [base-decide, smoke-omos, resolve-versions]
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
@@ -392,7 +451,7 @@ jobs:
docker builder prune -af || true
- uses: docker/setup-qemu-action@v3
with: {platforms: arm64}
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -420,10 +479,11 @@ jobs:
INSTALL_OPENCODE=true
INSTALL_OMOS=true
INSTALL_PI=false
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
tags: ${{ steps.tags.outputs.tags }}
build-variant-with-pi:
needs: [base-decide, smoke-with-pi]
needs: [base-decide, smoke-with-pi, resolve-versions]
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
@@ -440,7 +500,7 @@ jobs:
docker builder prune -af || true
- uses: docker/setup-qemu-action@v3
with: {platforms: arm64}
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -468,10 +528,11 @@ jobs:
INSTALL_OPENCODE=true
INSTALL_OMOS=false
INSTALL_PI=true
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
tags: ${{ steps.tags.outputs.tags }}
build-variant-omos-with-pi:
needs: [base-decide, smoke-omos-with-pi]
needs: [base-decide, smoke-omos-with-pi, resolve-versions]
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
@@ -488,7 +549,7 @@ jobs:
docker builder prune -af || true
- uses: docker/setup-qemu-action@v3
with: {platforms: arm64}
- uses: docker/setup-buildx-action@v4
- uses: docker/setup-buildx-action@v4.0.0
with: {driver-opts: network=host}
- uses: docker/login-action@v3
with:
@@ -516,6 +577,8 @@ jobs:
INSTALL_OPENCODE=true
INSTALL_OMOS=true
INSTALL_PI=true
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
tags: ${{ steps.tags.outputs.tags }}
# ── Phase 5: promote base-<hash> → base-latest (manifest copy only) ─
@@ -526,12 +589,40 @@ jobs:
- build-variant-omos
- build-variant-with-pi
- build-variant-omos-with-pi
if: env.PROMOTE_LATEST == 'true'
# Skip on cache-hit base builds: when need_build=false, base-latest
# already points at the same digest as base-<hash>, so the retag is
# a tautology and any transient failure of it is purely cosmetic.
# Manual workflow_dispatch with promote_latest=true overrides this
# gate as an escape hatch (e.g., if base-latest got hand-deleted).
#
# `always()` wrapper + explicit base-variant success check protects
# against the gitea-Actions default of "skipped need => skip dependent":
# a partial-publish run (e.g., omos-with-pi smoke fails) shouldn't
# prevent the base-latest alias from advancing on a real base rebuild.
if: |
always() &&
needs.build-variant-base.result == 'success' &&
(inputs.promote_latest == 'true' ||
(github.ref_type == 'tag' && needs.base-decide.outputs.need_build == 'true'))
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- uses: imjasonh/setup-crane@v0.4
# Direct pinned install instead of imjasonh/setup-crane@v0.4. The
# action's bootstrap script calls api.github.com/.../releases/latest
# to discover the crane version, which periodically rate-limits and
# produces tag=null → download from .../download/null/... → 404 →
# 'gzip: unexpected end of file' → exit 2. Pinning removes the
# runtime dependency on GitHub API entirely. Bump CRANE_VERSION
# deliberately when you want updates.
- name: Install crane (pinned)
env:
CRANE_VERSION: v0.21.6
run: |
set -eux
curl -fsSL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \
| tar -xz -C /usr/local/bin crane
crane version
- name: Login (crane)
run: |
crane auth login docker.io \
@@ -550,7 +641,17 @@ jobs:
- build-variant-omos
- build-variant-with-pi
- build-variant-omos-with-pi
if: env.PROMOTE_LATEST == 'true'
# Run when at least the base variant published — don't let a single
# variant failure (e.g., omos-with-pi smoke threshold) prevent Hub
# description refresh for the other variants that did publish.
# Without this `always()` wrapper, gitea Actions' default behavior
# of "skipped need => skip dependent" cascades from any failed/
# skipped build-variant-* into update-description, and the Hub
# description goes stale on partial-publish releases.
if: |
always() &&
needs.build-variant-base.result == 'success' &&
(github.ref_type == 'tag' || inputs.promote_latest == 'true')
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
+29 -1
View File
@@ -32,6 +32,31 @@ CI produces eight Docker Hub tags per release: `vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]
When bumping the opencode version, bump `OPENCODE_VERSION` in `Dockerfile.variant` and update the comment in `.env.example` if it names a specific model/version for context.
## Upstream sources — where to look up release notes
When drafting a release CHANGELOG entry, pull notes from the **canonical upstream repo for each tracked package**. Getting this wrong leads to thin or wrong release notes; the image bytes are unaffected but the documentation suffers.
| Package | Canonical upstream | What you'll find there |
|---|---|---|
| `opencode-ai` (npm) | <https://github.com/anomalyco/opencode/releases> | Per-version release notes with Core / TUI / Desktop / SDK sections, contributor attributions. Some versions have empty bodies (internal/no-user-visible); most do not. |
| `@earendil-works/pi-coding-agent` (npm) | The `CHANGELOG.md` shipped inside the npm tarball: `npm pack @earendil-works/pi-coding-agent@<version>` then extract `package/CHANGELOG.md`. | Rich changelog with New Features / Added / Changed / Fixed sections per version. |
| Other floated tools (gosu, fzf, bat, eza, zoxide, uv, nvim, gitea-mcp, Go, oh-my-opencode-slim) | Each project's own GitHub releases page | Usually less material per release; quote selectively. |
**Trap to avoid:** there is a `github.com/sst/opencode` repo that some search results surface; that's a fork (and probably the historical name people associate with opencode given the upstream lineage). It does NOT track the same release timeline. Use `anomalyco/opencode` for opencode release notes.
Fetch pattern (saved here for muscle memory):
```bash
# Latest stable opencode-ai versions on npm
npm view opencode-ai time --json | python3 -c 'import sys,json,re; d=json.load(sys.stdin); print(*sorted([(v,t) for v,t in d.items() if re.fullmatch(r"\d+\.\d+\.\d+",v)], key=lambda x:x[1], reverse=True)[:6], sep="\n")'
# Release notes for a specific version
curl -s https://api.github.com/repos/anomalyco/opencode/releases/tags/v1.15.10 | python3 -c 'import sys,json; print(json.load(sys.stdin).get("body","(empty)"))'
# pi changelog
cd /tmp && npm pack @earendil-works/pi-coding-agent@0.75.5 && tar -xzf earendil-works-pi-coding-agent-0.75.5.tgz package/CHANGELOG.md && head -40 package/CHANGELOG.md
```
## Critical conventions
- **entrypoint.sh volume ownership loop** — when adding a new named volume mount point, add it to the `for dir in ...` loop in `entrypoint.sh` so root-owned volumes get chowned on startup. The loop writes a `.devbox-owner` sentinel after a successful chown so subsequent starts skip the recursive walk. Users should not touch these files.
@@ -43,8 +68,11 @@ When bumping the opencode version, bump `OPENCODE_VERSION` in `Dockerfile.varian
- `.env.example` must be hand-updated to match Dockerfile/entrypoint behavior — it is not auto-generated.
Release-day checklist: README → (regenerate DOCKER_HUB.md only if HUB_TEMPLATE changed) → promote CHANGELOG Unreleased → grep AGENTS.md for stale counts → commit → tag → push tag.
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
**Between releases the same coupling applies.** Doc drift is not just a release-day concern — a workflow tweak, entrypoint change, or `generate-config.py` refactor can leave any of these four files lying. Before committing a non-release change, grep the docs for references to what you touched: `git diff --name-only HEAD | xargs -I{} grep -l 'thing-you-changed' README.md AGENTS.md DOCKER_HUB.md .gitea/README.md .env.example`. If a doc says "four variants" / "two phases" / "runs on amd64 only" and your change made that no longer true, fix it in the same commit.
- **GitHub/Gitea-sourced binaries float by default** — gosu, fzf, git-lfs, gitleaks, nvim, bat, eza, zoxide, uv, gitea-mcp, Go, oh-my-opencode-slim all default to `latest`. Each build-time install step reads the `/releases/latest` Location redirect (or the go.dev JSON feed for Go) and derives the concrete version. Use the same `ARCH` case-switch pattern for multi-arch support (amd64/arm64) — mind project-specific arch-name deviations (gitleaks uses `x64`, bat/eza/zoxide use `x86_64`/`aarch64`, gosu uses `amd64`/`arm64`). Intentional pins: `OPENCODE_VERSION` (drives the image tag), `NODE_VERSION=22` (major pin), `DEBIAN_VERSION=trixie-slim` (OS base). Adding a new upstream tool: follow the existing floated-version pattern, don't hardcode a specific tag.
- **Resolved versions are logged by the smoke test** — `scripts/smoke-test.sh` prints a "Resolved component versions" table as its first step. CI logs always capture what got baked into a given image even when ARGs default to `latest`.
- **`PI_VERSION` and `OMOS_VERSION` MUST be passed by CI as concrete versions**, not left at the `latest` default. The npm install steps in `Dockerfile.variant` (`npm install -g @earendil-works/pi-coding-agent` / `oh-my-opencode-slim@${OMOS_VERSION}`) produce identical layer-hashes when the ARG values are byte-identical across builds; combined with the registry buildcache (`base-buildcache`) the layer gets reused even when `latest` would have resolved to a newer upstream. This is the same class of bug that bit pi-devbox v0.74.0 → v0.75.5 (silent same-bytes-across-releases regression discovered 2026-05-23, fixed in pi-devbox v0.75.5b). It is currently *masked* in opencode-devbox by `OPENCODE_VERSION` being a hard-coded ARG that bumps every release — that bump invalidates the parent-chain cache key for the downstream pi/omos layers — but the masking would fail the moment a `vN.N.Nb` opencode-version-unchanged release ships that only bumps pi or omos. Preventative fix: `.gitea/workflows/docker-publish-split.yml` has a `resolve-versions` job that runs `npm view @earendil-works/pi-coding-agent version` and `npm view oh-my-opencode-slim version`, exposing concrete values as outputs that every variant smoke + build job consumes via build-args. Smoke tests assert via `EXPECTED_PI_VERSION` / `EXPECTED_OMOS_VERSION` env vars — would catch the regression on the next release rather than four releases later. **If you change the variant build-args list, the resolve-versions job, or the smoke EXPECTED_*_VERSION wiring, audit all affected jobs in lockstep.**
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.jsonc` — system Python can't import from the uv venv.
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.jsonc` or legacy `opencode.json`. Config persists in the `devbox-opencode-config` named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
+156
View File
@@ -8,6 +8,162 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
## Unreleased
_(no changes since v1.15.11b)_
---
## v1.15.11b — 2026-05-27
Container-level rebuild of v1.15.11. The original v1.15.11 release-day publish failed three times in a row (CI runs #332/333/334) with identical `400 Bad request` responses from `registry-1.docker.io` on the buildx layer-blob PUT. Build itself succeeded 30/30 each time; only the multi-arch push failed. Triaged on 2026-05-27 evening:
- **Local multi-arch buildx push from a developer host succeeds in ~25s** — same Hub account, same multi-arch path. Account, repo, and Hub-CDN are all healthy.
- **Last known-good Gitea Actions Hub push: 2026-05-23 ~20:26 UTC** (`pi-devbox v0.75.5b`). All Gitea-runner-driven pushes since 2026-05-24 have failed identically.
- **Smoking gun candidate:** `docker/setup-buildx-action@v4` floats to `v4.1.0` (published 2026-05-22 16:00 UTC). Action-resolver caches on the runner appear to have rolled forward to v4.1.0 sometime between the May 23 success and the first May 24 failure. v4.1.0 ships a newer bundled buildx/buildkit which may be using a different push protocol that trips Hub's CDN URI-length cap (the failing `_state` query string is ~1.4 KB).
### Workflow change
- **`.gitea/workflows/docker-publish-split.yml`** — all nine `docker/setup-buildx-action@v4` uses pinned to `@v4.0.0`. `setup-qemu-action@v3` left floating since QEMU wasn't in the suspected blast radius and was working on May 23. If v4.0.0 publishes cleanly we keep the pin and file an upstream buildkit/buildx issue documenting the regression.
No other source changes — same `OPENCODE_VERSION=1.15.11`, same `Dockerfile.base` and `Dockerfile.variant`, same SSH-CM bake, same gitleaks. v1.15.11 (the original tag) is preserved in the repo as a historical marker of the first publish attempt; v1.15.11b is the canonical release.
### v1.15.11
First release on opencode 1.15.11. Also bakes in four devbox-side fixes accumulated since v1.15.10 (SSH ControlMaster on a writable path, gitleaks added to base, CI resolve-versions hardening, CI cache-hit regression fix). Downstream pi-devbox inherits all of these on its next build against `base-latest`.
### Bumped: opencode 1.15.10 → 1.15.11
`OPENCODE_VERSION` ARG bumped in `Dockerfile.variant`. Highlights from the upstream release (full notes: <https://github.com/anomalyco/opencode/releases/tag/v1.15.11>):
- **Core / Improvements** — new `headerTimeout` config for provider requests (10s default for default OpenAI setups); experimental background agents now push updates without polling; remote-backed projects resolve a stable project identity; `modalities.input` / `modalities.output` can be set independently.
- **Core / Bugfixes** — dynamically added MCP servers now disconnect cleanly on removal; Google tool calling fixed after upstream tool-ID regression; resumed sessions no longer continue orphaned interrupted tools; OpenAI reasoning summaries render as separate blocks; the `shell` tool now advertises its configured timeout to the model; config loading falls back cleanly when user info is unavailable.
- **TUI** — prompt resizes with terminal width (new prompt-size config); accelerated diff-viewer scrolling; external editors open from the worktree directory when available.
- **Desktop** — refined v2 home screen, prompt, status popover, and session controls; fixed V2 titlebar errors when a session sync cache was deleted; web deployments no longer run desktop health checks; duplicate server connections are merged.
- **Extensions** — new `dispose` hook for plugins; Codex plugin now sends the expected session-ID header.
No `opencode-devbox`-side changes were required to consume 1.15.11 — pure version bump.
### Base: SSH ControlMaster default on a writable socket path
Devboxes typically mount `~/.ssh` from the host as **read-only** (security: keys remain readable but agents can't tamper with config / known_hosts / authorized_keys / plant a malicious ProxyCommand). OpenSSH's default `ControlPath` lands inside `~/.ssh/cm/`, which is unwritable on such mounts — so any attempt to use `ControlMaster auto` (or anything that wants to multiplex) fails with:
```
unix_listener: cannot bind to path /home/.../.ssh/cm/...: Read-only file system
kex_exchange_identification: Connection closed by remote host
```
The second line is downstream: when ControlMaster fails the ssh client falls back to a fresh TCP connection, and on residential CGNAT (most European ISPs) the per-(src,dst) concurrent-flow cap (~4) silently drops further SYNs once exceeded — manifesting as banner-exchange timeouts that look like a remote problem.
- **`Dockerfile.base`** — new section right after the apt block bakes `/etc/ssh/ssh_config.d/00-devbox-controlmaster.conf` with `Host *` defaults: `ControlMaster auto`, `ControlPath /tmp/sshcm/%r@%h:%p`, `ControlPersist 10m`, plus `ServerAliveInterval 30` / `ServerAliveCountMax 6` for resilience to mid-stream NAT timeouts. `/tmp` is per-container and always writable, so the read-only `~/.ssh` mount is left untouched. Debian's stock `/etc/ssh/ssh_config` includes `ssh_config.d/*.conf` *before* its own `Host *` block, so user `~/.ssh/config` overrides still win.
- **`entrypoint-user.sh`** — creates `/tmp/sshcm` mode 700 on every container start. `/tmp` is per-container so the dir doesn't survive recreation; baking it into a Dockerfile layer would be wrong. Mode 700 is required — OpenSSH refuses to use a `ControlPath` directory others can write to.
- **`scripts/smoke-test.sh`** — two new assertions: (a) the conf file exists at the expected path; (b) `ssh -G example.invalid` reports a `controlpath` rooted at `/tmp/sshcm/`. The second catches the silent regression where something later in the SSH config chain shadows the bake-in.
- **No size/threshold impact:** the conf file is ~250 bytes.
Downstream pi-devbox and any other variant inherits this on its next build against `base-latest`. Discovered while running a recon-shell from inside pi-devbox to a Proxmox node — fresh ssh hit banner timeout, debug output pointed at the read-only socket dir.
_(Originally landed on `main` 2026-05-24 as commit `668592d`; first ships in v1.15.11.)_
### Base: gitleaks added; git-crypt confirmed already installed
`gitleaks` is now baked into `Dockerfile.base` (Go-compiled binary fetched from GitHub releases, same `/releases/latest` redirect-resolution pattern as gosu/fzf/git-lfs/etc.). It pairs with `git-crypt`, which has been installed via apt all along but wasn't asserted by smoke or called out in user-facing docs. Several of the user's repos use both as part of their secret-management setup (gitleaks pre-commit hook + git-crypt for selectively-encrypted canonical config); having them in the devbox means `pi install`-style hooks fire correctly inside the container instead of warning that gitleaks is missing.
- **`Dockerfile.base`** — new `GITLEAKS_VERSION=latest` ARG + install RUN block right after `git-lfs`. Arch suffix is `x64` (not `x86_64` or `amd64`) on this project; comment in the Dockerfile flags the deviation. Adds ~21 MB to the base layer.
- **`scripts/smoke-test.sh`** — adds `git-crypt` and `gitleaks` to the "Resolved component versions" table and to the "Core binaries" assertion list. Now fails fast if either binary disappears from the base.
- **`README.md`** — "What's in the image" tree updated to name `gitleaks` alongside `git-crypt` in the dev-tools line.
- **No threshold bumps:** 21 MB on a 25003700 MB envelope is noise; existing variant thresholds keep their headroom.
This is a base-layer change — `base-decide` will compute a fresh `base-<hash>`, `build-base` will run on the next release (no cache hit), and all four variants will rebuild against the new base. **Downstream pi-devbox** picks up gitleaks automatically on its next release that resolves `joakimp/opencode-devbox:base-latest` to the new digest — no Dockerfile change needed there.
### CI: preventative fix for PI_VERSION/OMOS_VERSION cache-hit silent regression
Mirrors the pi-devbox v0.75.5b fix (2026-05-23) onto the four-variant pipeline here. The `with-pi`, `omos`, and `omos-with-pi` variants all install upstream npm packages (`@earendil-works/pi-coding-agent`, `oh-my-opencode-slim`) whose `*_VERSION` build-args defaulted to `latest`. When the build-arg string is byte-identical across builds, the resulting layer-hash is identical, and the registry buildcache (`base-buildcache` / variant cache-from chain) silently reuses the layer from whatever upstream version was current when the cache was first populated — the same mechanism that caused pi-devbox v0.74.0 through v0.75.5 to ship the same image bytes.
Currently masked here because `OPENCODE_VERSION` is a hard-coded ARG that bumps every release — changing a parent layer invalidates the downstream cache key for the pi/omos install layers. Masking would fail the moment we cut a `vN.N.Nb` opencode-version-unchanged release that only bumps pi or omos. Filed as a parked followup that bedtime; fixing it preventatively now.
- **`.gitea/workflows/docker-publish-split.yml`** — new `resolve-versions` job runs `npm view @earendil-works/pi-coding-agent version` and `npm view oh-my-opencode-slim version`, exposing concrete strings as job outputs. All six affected jobs (`smoke-omos`, `smoke-with-pi`, `smoke-omos-with-pi`, `build-variant-omos`, `build-variant-with-pi`, `build-variant-omos-with-pi`) now `needs:` it and pass the concrete versions as `PI_VERSION` / `OMOS_VERSION` build-args. `smoke-base` and `build-variant-base` are unaffected (no pi or omos).
- **`scripts/smoke-test.sh`** — new `run_expect` helper asserts an expected substring in command output. The pi-version check uses `EXPECTED_PI_VERSION` when set; the omos check uses `EXPECTED_OMOS_VERSION` against `npm ls -g`. Both env vars are wired from `resolve-versions` outputs in the smoke jobs. Catches the regression on the next release rather than four releases later.
- **`Dockerfile.variant`** — comment block above each affected `ARG` (`OPENCODE_VERSION`, `PI_VERSION`, `OMOS_VERSION`) documenting the cache-hit footgun + which ones are CI-resolved vs source-pinned.
- **`AGENTS.md`** — new convention bullet explaining the cache-hit class of bug and naming the resolve-versions job + EXPECTED_*_VERSION wiring as the contract to keep in lockstep.
No image-content change expected on the next release vs what `latest` would have resolved to anyway — this is purely about making sure the cache invalidates correctly going forward.
## v1.15.10 — 2026-05-23
opencode 1.15.6 → 1.15.10 bump (four upstream patch releases over two days). Plus implicit pi 0.75.4 → 0.75.5 in the `with-pi` and `omos-with-pi` variants since `PI_VERSION=latest` resolves at build time.
No image-content changes beyond the version bumps; cache hit expected on `base-35ee5fe7861a` (no `Dockerfile.base` or `rootfs/` edits since v1.14.50b).
### Notable upstream opencode changes
Sourced from <https://github.com/anomalyco/opencode/releases> (the upstream this devbox tracks).
**v1.15.7** — Grok OAuth (SuperGrok) sign-in including device-code login (@Jaaneek). V2 session APIs gain safe error responses with reference IDs (UnknownError, SessionNotFoundError, ServiceUnavailableError) so generic 500s no longer leak config details. Codex OAuth refreshes deduped to avoid repeated refresh failures (@cooper-oai). Native OpenAI OAuth requests restored. Tool schema failures now surface as friendly tool errors. PDF attachment support for Grok. Restored OpenAI reasoning streams. TUI: clearer collapsed-thinking punctuation, new sessions default to local project, single-select question checkmarks no longer collide with labels. Desktop: pinch zoom, new home view + session entry flow + titlebar, log export.
**v1.15.8** — Upstream release body empty; assumed internal/no user-visible changes.
**v1.15.9** — Redesigned diff viewer with file tree, **enabled by default**. MCP OAuth configs can set callback port and include configured scopes in client metadata (@sebin). Vertex Anthropic provider uses working `.rep.googleapis.com` endpoints for US/EU multi-region (@JPFrancoia). Many "show clearer error" improvements (default model invalid, missing PTY session, skill invocation failure, installation upgrade failure, project not found via HTTP API, MCP server not found, session busy). Native reasoning continuation metadata preserved across turns. TUI: copy worktree path from command palette, refined diff viewer shortcuts, spinner color aligned with active agent (@OpeOginni). Desktop: tab navigation in titlebar, session status in titlebar, multi-colon callback URL fix (@OpeOginni), debounced VCS refreshes.
**v1.15.10** — Single fix: restored the legacy production desktop flows for opening projects and starting sessions.
### Devbox-side notes
- **Bump:** opencode 1.15.6 → 1.15.10 (`OPENCODE_VERSION` in `Dockerfile.variant`).
- **Implicit pi bump:** `with-pi` and `omos-with-pi` variants pick up pi 0.75.5 (one patch release with cleaner read-tool cards, async file tools, more reliable package updates, Bedrock token cap fix, etc.). See [pi-devbox v0.75.5 CHANGELOG](https://gitea.jordbo.se/joakimp/pi-devbox/src/branch/main/CHANGELOG.md) for the full list.
- **Smoke threshold check:** `omos-with-pi` threshold remains at 3700 MB (set v1.15.4b 2026-05-18). Four opencode patches plus one pi patch typically add only a few MB across both; not expected to trip. If it does, recovery is the well-worn letter-suffix pattern (v1.15.10b with threshold bump).
- Built on the same CI path as v1.15.6 (pinned-crane install on real-base-rebuild, skip-promote-on-cache-hit, update-description-always-on-base-success) — all expected to remain quiet on this cache-hit run.
### Note on this CHANGELOG vs the v1.15.10 tag snapshot
The v1.15.10 tag itself was pushed before the upstream release notes were located (originally I checked `sst/opencode` which is a fork; the canonical upstream is `anomalyco/opencode`). The image content under the tag is correct, but the CHANGELOG snapshot at the tag was thinner. This expanded version is on `main` going forward; the tag's snapshot will not be retroactively rewritten.
## v1.15.6 — 2026-05-21
opencode 1.15.4 → 1.15.6 bump (two upstream patch releases) plus two workflow improvements that landed on `main` between v1.15.4b and now. No image-content changes beyond the version bump; cache hit expected on `base-35ee5fe7861a` (no `Dockerfile.base` or `rootfs/` edits).
- **Bump:** opencode 1.15.4 → 1.15.6 (`OPENCODE_VERSION` in `Dockerfile.variant`). The `with-pi` and `omos-with-pi` variants will also implicitly pick up pi 0.75.3 → 0.75.4 since `PI_VERSION=latest` resolves at build time.
- **CI: defensive `__pycache__` and macOS-metadata filter in `base-decide` hash compute.** `find rootfs -type f` previously included gitignored junk like `rootfs/__pycache__/*.pyc`, `.DS_Store`, and `._AppleDouble` files — which CI's clean checkout never sees. This bit us during v1.15.4 debugging when a stale `generate-config.cpython-314.pyc` on the local rootfs/ produced `base-3605aa6b6ab1` while CI computed `base-35ee5fe7861a`. The filter is a no-op on a clean tree (verified to still produce `35ee5fe7861a` post-filter), but defends against future stale-pyc / Finder-touched-rootfs hash mismatches. `.gitea/README.md` updated in lockstep. (commit `b6e4d89`)
- **AGENTS.md: documentation drift sweep as explicit pre-commit workflow step.** Codifies the rule that non-release commits must also grep docs for stale claims about behaviour they change, with concrete repo-specific drift hotspots. Companion clause added across the wider repo set (cloud-init, ansible, pi-devbox, pi-extensions, pi-toolkit, cli_utils, proxmox) the same day. (commit `90e5a1f`)
- **First release that exercises both the pinned-crane install (T14, v1.15.3) and the skip-promote-on-cache-hit guard (T15, v1.15.4) on this CI run path** — still cache-hit on base, so `promote-base-latest` should remain skipped via T15 and the pinned crane install will only fire when a real base rebuild happens.
## v1.15.4b — 2026-05-18
Recovery release for v1.15.4 — the `omos-with-pi` variant landed at >3500 MB and tripped the smoke threshold, so `smoke-omos-with-pi` and `build-variant-omos-with-pi` were skipped. The other three variants (base, omos, with-pi) published cleanly. Plus a latent workflow bug fix exposed by the partial publish.
- **Smoke threshold bump:** `omos-with-pi` 3500 → 3700 MB. Compounded growth: opencode 1.15.0 → 1.15.4 (4 patch versions) plus pi 0.74.0 → 0.75.3 (minor + 3 patches) both added a few MB each, and they sum in the omos-with-pi variant. Same pattern as previous threshold bumps (v1.14.31c, v1.15.0b); restores ~150 MB headroom.
- **Workflow fix — `update-description` no longer skips on partial publish.** Pre-existing latent bug: `update-description.needs` includes all four `build-variant-*` jobs, and gitea Actions' default behavior is "skipped need ⇒ skip dependent". When `build-variant-omos-with-pi` got skipped (because its smoke failed), `update-description` cascaded into a skip even though the job's `if:` condition (`tag pushed`) was true. Result: Hub description wasn't refreshed on v1.15.4 despite three variants publishing. Fix: wrap the `if:` in `always() && needs.build-variant-base.result == 'success' && ...` so the job runs as long as the base variant published, regardless of what other variants did.
- **Same fix applied to `promote-base-latest`** — had the identical latent bug. Currently masked by the cache-hit skip, but would have surfaced on a real-base-rebuild release with a single failed variant.
- No image-side changes from v1.15.4. Cache hit on the same base hash (`base-35ee5fe7861a`).
## v1.15.4 — 2026-05-18
opencode 1.15.3 → 1.15.4 bump (one upstream patch release), bundled with the CI hardening that landed on main between v1.15.3 and now.
- **Bump:** opencode 1.15.3 → 1.15.4 (`OPENCODE_VERSION` in `Dockerfile.variant`).
- **CI: pinned crane install in `promote-base-latest`.** Replaced `imjasonh/setup-crane@v0.4` with a direct `curl + tar` install pinned to crane v0.21.6. The action's bootstrap script calls `api.github.com/.../releases/latest` to discover what crane version to install. That call periodically rate-limits and produces `tag=null` → the action downloads `releases/download/null/...` → 404 → `gzip: unexpected end of file` → exit 2. We hit this on v1.15.3 (cosmetic failure since base-latest was already correct from cache hit). Pinned install removes the runtime GitHub API dependency entirely. Bump `CRANE_VERSION` deliberately when wanting updates, same pattern as the other GitHub-sourced binaries in the Dockerfile layer.
- **CI: skip `promote-base-latest` on cache-hit base builds.** When the base layer hash hasn't changed (cache-hit on the existing `base-<hash>` from a previous run), `base-latest` already points at the correct digest, so the retag is a tautology. Job now skipped entirely when `needs.base-decide.outputs.need_build == 'false'`. Manual `workflow_dispatch` with `promote_latest: true` overrides the gate as an escape hatch for hand-recovery scenarios.
- No image-side changes from the v1.15.3 baseline beyond the opencode npm version. Smoke thresholds unchanged.
## v1.15.3 — 2026-05-16
opencode 1.15.0 → 1.15.3 bump (three upstream patch releases).
- **Bump:** opencode 1.15.0 → 1.15.3 (`OPENCODE_VERSION` in `Dockerfile.variant`).
- No container-side changes. Smoke thresholds from v1.15.0b unchanged.
## v1.15.0b — 2026-05-15
Rebuild of v1.15.0 with one fix — v1.15.0's `omos` variant landed at 3206 MB, 6 MB over the 3200 MB smoke threshold, so `smoke-omos` failed and `build-variant-omos` was skipped. opencode 1.15.0 grew slightly vs 1.14.50, leaving zero headroom on the existing threshold.
- **Smoke threshold bump:** `omos` 3200 → 3300 MB, `omos-with-pi` 3400 → 3500 MB. Restores ~100 MB headroom for routine apt-get upgrade drift between releases. Documented inline in `scripts/smoke-test.sh`. No image-side changes — cache hits across the board, just a re-publish on the bumped threshold.
## v1.15.0 — 2026-05-15
opencode 1.14.50 → 1.15.0 bump (upstream minor release).
- **Bump:** opencode 1.14.50 → 1.15.0 (`OPENCODE_VERSION` in `Dockerfile.variant`).
- **Resilience:** `git clone` for pi-toolkit and pi-extensions in `Dockerfile.variant` is now wrapped in a 5-attempt retry loop with linear backoff (5s, 10s, 15s, 20s, 25s = up to ~75s total). gitea.jordbo.se occasionally returns transient HTTP 500s on the first request after idle, which previously broke the with-pi and omos-with-pi variant builds. Same pattern landed in pi-devbox repo concurrently.
- **Docs:** `DOCKER_HUB.md` mentions `joakimp/pi-devbox` as a sibling image — the pi-only build that uses this image's base layer as its parent. Generator template (`scripts/generate-dockerhub-md.py`) updated and regenerated. Hub size: 5905 bytes (well under the 25 kB limit).
- **Recovery from v1.14.50c partial publish:** the `latest-omos`, `v1.14.50c-omos` Hub gap is closed by this release — `latest-omos` will move forward to v1.15.0 once all four variants publish cleanly. Users on the floating tag were unaffected (still pointing at v1.14.41b until now).
## v1.14.50c — 2026-05-14
Recovery release for v1.14.50b's missing variants. v1.14.50b shipped only the `base` variant; `omos`, `with-pi`, and `omos-with-pi` were lost to a runner-fleet incident (see postmortem below).
+20 -16
View File
@@ -17,6 +17,21 @@ All variants support `linux/amd64` and `linux/arm64`.
## Quick Start
For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files:
```bash
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
curl -O https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml
curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
# Edit .env — set OPENCODE_PROVIDER, the matching API key,
# WORKSPACE_PATH, GIT_USER_NAME, GIT_USER_EMAIL.
docker compose run --rm devbox
```
This drops you straight into opencode with your project mounted at `/workspace`. Use `bash` as the command (e.g. `docker compose run --rm devbox bash`) to land in a shell first — useful for `aws sso login`, `pi` (on `*-with-pi` variants), or multi-harness workflows.
**One-shot run, no persistence:**
```bash
docker run -it --rm \
-e ANTHROPIC_API_KEY=your-key \
@@ -28,22 +43,7 @@ docker run -it --rm \
joakimp/opencode-devbox:latest
```
Drops you straight into opencode with your project mounted at `/workspace`.
For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`):
```bash
docker run -it --rm \
-e ANTHROPIC_API_KEY=your-key \
-e OPENCODE_PROVIDER=anthropic \
-v ~/projects:/workspace \
-v ~/.ssh:/home/developer/.ssh:ro \
joakimp/opencode-devbox:latest bash
```
Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell.
For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired.
Full setup guide — authentication for each provider (Anthropic, OpenAI, Bedrock SSO + static), persistence model, build args, troubleshooting: <https://gitea.jordbo.se/joakimp/opencode-devbox#readme>
## What's Inside
@@ -86,6 +86,10 @@ Full persistence reference, including multi-user (`SIGNUM`) isolation and host b
- **Issues / source / docker-compose templates:** <https://gitea.jordbo.se/joakimp/opencode-devbox>
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/AGENTS.md>
## Sibling images
- **[`joakimp/pi-devbox`](https://hub.docker.com/r/joakimp/pi-devbox)** — pi-only image built on top of this image's base layer. Smaller (~700 MB) and version-tracks the [pi npm package](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) directly. Use this if you want pi without opencode. Source: <https://gitea.jordbo.se/joakimp/pi-devbox>
## License
MIT. See <https://gitea.jordbo.se/joakimp/opencode-devbox/src/branch/main/LICENSE>.
+56
View File
@@ -71,6 +71,44 @@ RUN apt-get update && \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# ── SSH client defaults: ControlMaster on a writable socket path ──────
# Why this exists: the devbox typically mounts ~/.ssh from the host as
# read-only (security: keys are readable, but agents can't tamper with
# config / known_hosts / authorized_keys / plant a malicious ProxyCommand).
# OpenSSH's default ControlPath is ~/.ssh/cm/... which is unwritable on
# such mounts, so any attempt to use ControlMaster fails. Symptoms:
# unix_listener: cannot bind to path /home/.../.ssh/cm/...: Read-only file system
# kex_exchange_identification: Connection closed by remote host
# The latter manifests downstream of CGNAT per-destination flow caps
# (~4 concurrent flows on most European residential ISPs) which silently
# drop further SYNs once exceeded — making fresh ssh attempts fail with
# banner-exchange timeouts that look like a remote problem.
#
# Fix: set a system-wide default ControlPath in /tmp (per-container,
# tmpfs-friendly, always writable) so multiplexing Just Works without
# touching the read-only ~/.ssh mount. Per-host overrides in user's
# ~/.ssh/config still win — Debian's default /etc/ssh/ssh_config has
# `Include /etc/ssh/ssh_config.d/*.conf` *before* the `Host *` block,
# so user config can override these defaults if desired.
#
# ControlPersist=10m means the master socket sticks around 10 min after
# the last session closes, so consecutive ssh calls in a workflow reuse
# the same TCP flow. Companion entrypoint-user.sh creates /tmp/sshcm
# (mode 700) on each container start.
RUN mkdir -p /etc/ssh/ssh_config.d && \
printf '%s\n' \
'# Devbox-baked default. See Dockerfile.base "SSH client defaults".' \
'# Override per-host in ~/.ssh/config if the master socket location' \
'# needs to differ.' \
'Host *' \
' ControlMaster auto' \
' ControlPath /tmp/sshcm/%r@%h:%p' \
' ControlPersist 10m' \
' ServerAliveInterval 30' \
' ServerAliveCountMax 6' \
> /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf && \
chmod 644 /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf
# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
#
# Version policy for the binaries below:
@@ -126,6 +164,24 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;;
git lfs install --system && \
git-lfs --version
# gitleaks — secret scanner (used as a pre-commit hook in several of the
# repos this devbox is meant to operate on; pairs with git-crypt below).
# Distributed as a Go-compiled tarball; arch suffix is `x64` (not `x86_64`
# or `amd64`) on this project — mind the deviation from the surrounding
# tools' naming.
ARG GITLEAKS_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x64" ;; arm64) echo "arm64" ;; *) echo "x64" ;; esac) && \
V="${GITLEAKS_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/gitleaks/gitleaks/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing gitleaks ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/gitleaks/gitleaks/releases/download/v${V}/gitleaks_${V}_linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin gitleaks && \
chmod +x /usr/local/bin/gitleaks && \
gitleaks version
# neovim — modern text editor
ARG NVIM_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
+34 -5
View File
@@ -31,8 +31,12 @@ ARG TARGETARCH
ARG USER_NAME=developer
# ── Install opencode via npm ─────────────────────────────────────────
# OPENCODE_VERSION is intentionally pinned in this Dockerfile (not
# 'latest'). It drives the release tag and gets bumped via a source
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
# v0.75.5 cannot apply here.
ARG INSTALL_OPENCODE=true
ARG OPENCODE_VERSION=1.14.50
ARG OPENCODE_VERSION=1.15.11
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
opencode --version ; \
@@ -42,21 +46,42 @@ RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
# pi-toolkit and pi-extensions are cloned into /opt/. entrypoint-user.sh
# runs each repo's install.sh on container start so symlinks land under
# ~/.pi/agent/ on the named volume.
# PI_VERSION should be passed explicitly by CI as a concrete version
# (resolved from `npm view @earendil-works/pi-coding-agent version`,
# see .gitea/workflows/docker-publish-split.yml § resolve-versions).
# The default `latest` is for local dev convenience only — it has a
# known cache-hit footgun when used in registry-cached CI builds: the
# resulting build-arg string is byte-identical across builds, the
# layer-hash is identical, and the registry buildcache silently reuses
# the layer from whatever pi version was current when the cache was
# first populated. Currently masked here because OPENCODE_VERSION (a
# parent layer) bumps every release; will manifest the moment a
# vN.N.Nb opencode-version-unchanged release ships. See pi-devbox
# v0.75.5b 2026-05-23 for the discovery + canonical fix.
ARG INSTALL_PI=false
ARG PI_VERSION=latest
ARG PI_TOOLKIT_REF=main
ARG PI_EXTENSIONS_REF=main
RUN if [ "${INSTALL_PI}" = "true" ]; then \
set -e && \
git_clone_retry() { \
url="$1"; ref="$2"; dest="$3"; \
for i in 1 2 3 4 5; do \
if git clone --depth 1 --branch "$ref" "$url" "$dest"; then return 0; fi; \
rm -rf "$dest"; \
echo "git clone $url failed (attempt $i/5), retrying in $((i*5))s..."; \
sleep $((i*5)); \
done; \
return 1; \
} && \
if [ "${PI_VERSION}" = "latest" ]; then \
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent ; \
else \
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \
fi && \
pi --version && \
git clone --depth 1 --branch "${PI_TOOLKIT_REF}" \
https://gitea.jordbo.se/joakimp/pi-toolkit.git /opt/pi-toolkit && \
git clone --depth 1 --branch "${PI_EXTENSIONS_REF}" \
https://gitea.jordbo.se/joakimp/pi-extensions.git /opt/pi-extensions && \
git_clone_retry https://gitea.jordbo.se/joakimp/pi-toolkit.git "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
git_clone_retry https://gitea.jordbo.se/joakimp/pi-extensions.git "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
echo "pi-toolkit at $(cd /opt/pi-toolkit && git rev-parse --short HEAD)" && \
echo "pi-extensions at $(cd /opt/pi-extensions && git rev-parse --short HEAD)" ; \
fi
@@ -80,6 +105,10 @@ RUN if [ "${INSTALL_GO}" = "true" ]; then \
# ── Optional: oh-my-opencode-slim (multi-agent orchestration) ────────
# Installs Bun runtime and the oh-my-opencode-slim npm package.
# OMOS_VERSION shares the same cache-hit footgun as PI_VERSION when
# left at the `latest` default in registry-cached CI builds. CI
# resolves it via `npm view oh-my-opencode-slim version` and passes
# the concrete value as a build-arg. See PI_VERSION block above.
ARG INSTALL_OMOS=false
ARG OMOS_VERSION=latest
RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
+23 -3
View File
@@ -8,8 +8,28 @@ The official `ghcr.io/anomalyco/opencode` image (now archived) was Alpine-based
## Quick Start
**Just want to run it?** No git clone needed — grab the two template files:
```bash
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
# Pull docker-compose.yml and the .env template
curl -O https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml
curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
# Edit .env — at minimum: OPENCODE_PROVIDER, the matching API key,
# WORKSPACE_PATH, GIT_USER_NAME, GIT_USER_EMAIL.
$EDITOR .env
# Pull and run
docker compose run --rm devbox
```
This pulls `joakimp/opencode-devbox:latest` from Docker Hub, mounts `WORKSPACE_PATH` at `/workspace`, and drops you straight into opencode. Use `bash` instead of (no command) to land in a shell first — useful for `aws sso login`, `pi`, `omos`, etc.
**Want to hack on the image itself, follow upstream changes, or rebuild from source?** Clone the repo:
```bash
# Clone
git clone ssh://gitea.jordbo.se:2222/joakimp/opencode-devbox.git
cd opencode-devbox
@@ -17,7 +37,7 @@ cd opencode-devbox
cp .env.example .env
# Edit .env with your provider, API key, workspace path, git config
# Install git hooks (secret scanning)
# Install git hooks (secret scanning) before committing
brew install gitleaks # macOS / Linuxbrew
./setup-hooks.sh
@@ -742,7 +762,7 @@ Container (Debian trixie)
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
├── AWS CLI v2 (SSO + Bedrock auth)
├── neovim 0.12, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
├── git, git-crypt, age, ssh, ripgrep, fd, fzf, jq, curl, tree
├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, curl, tree
├── Node.js (for MCP servers)
├── Bun (optional — included with oh-my-opencode-slim)
├── entrypoint.sh (UID adjustment, git config, provider setup)
+11
View File
@@ -1,6 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
# ── SSH ControlMaster socket dir ────────────────────────────────
# Companion to /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf in the
# base image — that file declares ControlPath=/tmp/sshcm/%r@%h:%p; this
# creates the directory with the right permissions on every container
# start. /tmp is per-container so the dir doesn't survive recreation;
# baking it into a Dockerfile layer would be wrong.
# Mode 700 is required — OpenSSH refuses to use a ControlPath dir that
# others can write to.
mkdir -p /tmp/sshcm
chmod 700 /tmp/sshcm
# ── Shell defaults: copy baked files from /etc/skel-devbox/ if absent
# Respects host bind-mounts and user customizations — existing files
# are never overwritten. To restore defaults: rm ~/.bash_aliases (or
+20 -16
View File
@@ -71,6 +71,21 @@ All variants support `linux/amd64` and `linux/arm64`.
## Quick Start
For a fully-configured environment with persistent state (opencode config, mempalace memory, neovim plugins, bash history) surviving container recreation, use docker-compose. **You don't need to clone the repo** — just grab two template files:
```bash
mkdir -p ~/opencode-devbox && cd ~/opencode-devbox
curl -O https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/docker-compose.yml
curl -fsSL https://gitea.jordbo.se/joakimp/opencode-devbox/raw/branch/main/.env.example -o .env
# Edit .env — set OPENCODE_PROVIDER, the matching API key,
# WORKSPACE_PATH, GIT_USER_NAME, GIT_USER_EMAIL.
docker compose run --rm devbox
```
This drops you straight into opencode with your project mounted at `/workspace`. Use `bash` as the command (e.g. `docker compose run --rm devbox bash`) to land in a shell first — useful for `aws sso login`, `pi` (on `*-with-pi` variants), or multi-harness workflows.
**One-shot run, no persistence:**
```bash
docker run -it --rm \\
-e ANTHROPIC_API_KEY=your-key \\
@@ -82,22 +97,7 @@ docker run -it --rm \\
joakimp/opencode-devbox:latest
```
Drops you straight into opencode with your project mounted at `/workspace`.
For an interactive shell first (useful for AWS SSO login, multi-harness workflows, or just `bash`):
```bash
docker run -it --rm \\
-e ANTHROPIC_API_KEY=your-key \\
-e OPENCODE_PROVIDER=anthropic \\
-v ~/projects:/workspace \\
-v ~/.ssh:/home/developer/.ssh:ro \\
joakimp/opencode-devbox:latest bash
```
Then run `opencode`, `pi` (on `*-with-pi` variants), or `aws sso login` from the shell.
For docker-compose users, the source repo provides `docker-compose.yml`, `.env.example`, and a one-liner `docker compose up -d` workflow with named volumes pre-wired.
Full setup guide — authentication for each provider (Anthropic, OpenAI, Bedrock SSO + static), persistence model, build args, troubleshooting: <{GITEA}#readme>
## What's Inside
@@ -140,6 +140,10 @@ Full persistence reference, including multi-user (`SIGNUM`) isolation and host b
- **Issues / source / docker-compose templates:** <{GITEA}>
- **Agent-facing internals** (for future maintainers / coding agents working in the repo): <{GITEA}/src/branch/main/AGENTS.md>
## Sibling images
- **[`joakimp/pi-devbox`](https://hub.docker.com/r/joakimp/pi-devbox)** — pi-only image built on top of this image's base layer. Smaller (~700 MB) and version-tracks the [pi npm package](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) directly. Use this if you want pi without opencode. Source: <https://gitea.jordbo.se/joakimp/pi-devbox>
## License
MIT. See <{GITEA}/src/branch/main/LICENSE>.
+49 -4
View File
@@ -43,6 +43,25 @@ run() {
fi
}
# Stricter version of `run` that also asserts an expected substring in
# the command's stdout. Used to catch the "image bytes silently identical
# to previous release" class of regression — Docker layer-cache hit on
# a bare `npm install -g <pkg>` (or @latest) because the build-arg
# string is identical across builds, even when 'latest' would have
# resolved differently. Discovered in pi-devbox 2026-05-23 (every
# release v0.74.0..v0.75.5 shipped the same image bytes); preventatively
# applied here for PI_VERSION + OMOS_VERSION.
run_expect() {
local label="$1"; local cmd="$2"; local expect="$3"
local out
out=$(docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" 2>&1) || true
if echo "$out" | grep -Fq "$expect"; then
pass "$label (got $expect)"
else
fail "$label — expected substring '$expect', got: $out"
fi
}
echo "=== Smoke test: $IMAGE (variant: $VARIANT) ==="
echo
echo "-- Resolved component versions --"
@@ -68,6 +87,8 @@ docker run --rm --entrypoint="" "$IMAGE" sh -c '
printf " %-15s %s\n" "rg" "$(rg --version | head -1)"
printf " %-15s %s\n" "gosu" "$(gosu --version)"
printf " %-15s %s\n" "git-lfs" "$(git-lfs --version)"
printf " %-15s %s\n" "git-crypt" "$(git-crypt --version 2>&1 | head -1)"
printf " %-15s %s\n" "gitleaks" "$(gitleaks version 2>&1 | head -1)"
printf " %-15s %s\n" "gitea-mcp" "$(gitea-mcp --version 2>&1 | head -1)"
printf " %-15s %s\n" "aws" "$(aws --version 2>&1)"
if command -v bun >/dev/null 2>&1; then
@@ -103,11 +124,20 @@ run "fzf" "fzf --version"
run "fd" "fd --version"
run "rg" "rg --version | head -1"
run "jq" "jq --version"
run "git-crypt" "git-crypt --version | head -1"
run "gitleaks" "gitleaks version"
run "aws" "aws --version"
run "gitea-mcp" "gitea-mcp --version"
run "gosu" "gosu --version"
run "tmux" "tmux -V"
# SSH ControlMaster baked defaults: the config file must exist (image-level)
# and ssh -G must report ControlPath rooted at /tmp/sshcm/ for an arbitrary
# host. Catches both regressions: someone removing the conf file, OR something
# else later in the config chain shadowing the ControlPath setting.
run "ssh-config-cm-file" "test -f /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf"
run_expect "ssh-config-cm-path" "ssh -G example.invalid 2>/dev/null | grep -i ^controlpath" "/tmp/sshcm/"
echo
echo "-- Optional / variant-gated --"
# mempalace: present unless built with INSTALL_MEMPALACE=false
@@ -134,7 +164,11 @@ fi
# entrypoint-user.sh on first start, so we test by running the entry
# point chain (not just `docker run --entrypoint=""`).
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&1; then
run "pi" "pi --version"
if [ -n "${EXPECTED_PI_VERSION:-}" ]; then
run_expect "pi version matches build-arg" "pi --version" "$EXPECTED_PI_VERSION"
else
run "pi" "pi --version"
fi
run "pi-toolkit clone" "test -d /opt/pi-toolkit && git -C /opt/pi-toolkit rev-parse --short HEAD"
run "pi-extensions clone" "test -d /opt/pi-extensions && git -C /opt/pi-extensions rev-parse --short HEAD"
@@ -192,6 +226,11 @@ if [ "$VARIANT" = "omos" ] || [ "$VARIANT" = "omos-with-pi" ]; then
# queries the user prefix and would miss the baked binaries even though
# they're correctly on PATH at /usr/bin.
run "oh-my-opencode-slim" "NPM_CONFIG_PREFIX=/usr npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
if [ -n "${EXPECTED_OMOS_VERSION:-}" ]; then
run_expect "omos version matches build-arg" \
"NPM_CONFIG_PREFIX=/usr npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim" \
"$EXPECTED_OMOS_VERSION"
fi
else
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v bun" >/dev/null 2>&1; then
fail "bun should NOT be in base image but was found"
@@ -289,14 +328,20 @@ SIZE_BYTES=$(docker image inspect --format='{{.Size}}' "$IMAGE")
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
echo " Uncompressed size: ${SIZE_MB} MB"
# Thresholds (uncompressed): base 2500 MB, omos 3200 MB, with-pi adds ~150 MB.
# Thresholds (uncompressed): base 2500 MB, omos 3300 MB, with-pi adds ~150 MB.
# omos bumped 3000→3200 on v1.14.31c — mempalace-toolkit bake-in pushed the
# baseline; bumped 3200→3300 on v1.15.0 — opencode 1.15.0 came in at
# 3206 MB, leaving zero headroom for routine apt-get upgrade drift.
# omos-with-pi bumped 3400→3500 on v1.15.0 alongside the omos bump.
# omos-with-pi bumped 3500→3700 on v1.15.4b — omos+pi compounded as both
# upstream packages grew (opencode 1.15.0→1.15.4, pi 0.74.0→0.75.3) and
# the variant landed just over 3500 in v1.15.4's smoke.
# omos variant to ~3.1 GB. Functional smoke checks all pass; this is a
# guardrail, not a performance limit.
THRESHOLD=2500
[ "$VARIANT" = "omos" ] && THRESHOLD=3200
[ "$VARIANT" = "omos" ] && THRESHOLD=3300
[ "$VARIANT" = "with-pi" ] && THRESHOLD=2700
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3400
[ "$VARIANT" = "omos-with-pi" ] && THRESHOLD=3700
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
else