Compare commits

...

20 Commits

Author SHA1 Message Date
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
joakimp f06a70a3bc v1.14.50c: tag-only retag to recover v1.14.50b's missing variants
Publish Docker Image / base-decide (push) Successful in 1m12s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke-omos (push) Successful in 4m39s
Publish Docker Image / smoke-with-pi (push) Successful in 7m22s
Publish Docker Image / smoke-base (push) Successful in 8m1s
Publish Docker Image / smoke-omos-with-pi (push) Successful in 10m56s
Publish Docker Image / build-variant-omos (push) Failing after 12m49s
Publish Docker Image / build-variant-with-pi (push) Successful in 17m0s
Publish Docker Image / build-variant-base (push) Successful in 17m33s
Publish Docker Image / build-variant-omos-with-pi (push) Successful in 26m46s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
CHANGELOG entry for v1.14.50c with full postmortem on the v1.14.50/50b
runner-fleet incident (AVX shadowing + containerd race + Proxmox CPU
default + base-latest auto-promote gap). No container-side code changes
\u2014 the rebuild on the now-healthy fleet is sufficient.
2026-05-14 23:32:46 +02:00
joakimp dba05da7d1 validate.yml: use Hub base-latest as variant parent + warn on base-input changes
Validate / docs-check (push) Successful in 9s
Validate / base-change-warning (push) Successful in 11s
Validate / validate-base (push) Failing after 21s
Validate / validate-omos (push) Failing after 1m49s
Validate / validate-with-pi (push) Failing after 1m46s
Validate / validate-omos-with-pi (push) Failing after 13m9s
The previous two-step approach (build Dockerfile.base \ then
Dockerfile.variant FROM the local image) doesn't work: each
docker/build-push-action@v7 invocation runs in its own buildx
container context, and an image loaded into the host docker daemon
by step N is not visible to step N+1's buildx invocation.

Variant builds in validate.yml now FROM joakimp/opencode-devbox:base-latest
on Docker Hub, matching the production smokes' parent. Trade-off:
PRs/pushes that change Dockerfile.base, rootfs/, or entrypoint*.sh
are not exercised here \u2014 only release tags rebuild the base via
docker-publish-split.yml.

The new base-change-warning job surfaces a runtime warning when a
commit modifies any base-image input, telling the author to run a
workflow_dispatch test if they want full validation before merging.
2026-05-14 20:53:19 +02:00
joakimp 8359fef949 Force fresh base rebuild for v1.14.50b
Validate / validate-base (push) Failing after 17s
Validate / docs-check (push) Successful in 20s
Publish Docker Image / base-decide (push) Successful in 13s
Validate / validate-with-pi (push) Failing after 2m57s
Validate / validate-omos (push) Failing after 9m29s
Validate / validate-omos-with-pi (push) Failing after 14m25s
Publish Docker Image / build-base (push) Successful in 13m58s
Publish Docker Image / smoke-omos-with-pi (push) Failing after 27s
Publish Docker Image / build-variant-omos-with-pi (push) Has been skipped
Publish Docker Image / smoke-omos (push) Failing after 1m51s
Publish Docker Image / build-variant-omos (push) Has been skipped
Publish Docker Image / smoke-base (push) Successful in 9m1s
Publish Docker Image / smoke-with-pi (push) Successful in 11m52s
Publish Docker Image / build-variant-base (push) Successful in 17m50s
Publish Docker Image / build-variant-with-pi (push) Failing after 16m10s
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Add BASE_REBUILD_DATE comment to Dockerfile.base to invalidate the
content hash and trigger a full base rebuild. Picks up ~5 days of
Debian trixie security updates since the previous base-bf9df274db7a
was built on 2026-05-09.

The comment also documents the pattern for future intentional
base-rebuilds without other code changes — recommended cadence is
once per release for security currency.

Required because v1.14.50 hash inputs were unchanged from v1.14.44,
hitting the existing base-bf9df274db7a cache and shipping stale apt
packages. v1.14.50 also failed mid-flight before promote-base-latest
could publish base-latest to Hub — pi-devbox and other downstream
images that FROM base-latest were blocked.
2026-05-14 20:17:37 +02:00
joakimp a438c67f06 fix: update validate.yml for split-base Dockerfiles
Validate / validate-omos (push) Failing after 20s
Validate / docs-check (push) Successful in 22s
Validate / validate-with-pi (push) Failing after 3m8s
Validate / validate-omos-with-pi (push) Failing after 3m6s
Validate / validate-base (push) Failing after 14m57s
Replace single-Dockerfile build with two-step: build Dockerfile.base
first (loads as opencode-devbox:validate-base), then build
Dockerfile.variant with BASE_IMAGE pointing at the local base image.
All four validate jobs updated.
2026-05-14 19:48:46 +02:00
joakimp 07e07ec611 Bump opencode 1.14.44 -> 1.14.50; cut over to split-base pipeline
Validate / validate-omos-with-pi (push) Waiting to run
Validate / docs-check (push) Successful in 1m7s
Validate / validate-with-pi (push) Failing after 3m16s
Validate / validate-omos (push) Failing after 3m15s
Validate / validate-base (push) Failing after 6m31s
Publish Docker Image / base-decide (push) Failing after 11m59s
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
- Bump OPENCODE_VERSION 1.14.44 -> 1.14.50 in Dockerfile.variant
- Cut over: docker-publish-split.yml now triggers on push: tags: v*
  (was workflow_dispatch only). RELEASE_TAG and PROMOTE_LATEST derived
  from github.ref_type/ref_name for tag-push; inputs still available
  for manual workflow_dispatch runs.
- Delete docker-publish.yml (retired, replaced by split-base pipeline)
- Delete Dockerfile (retired, replaced by Dockerfile.base + Dockerfile.variant)
- Update CHANGELOG: promote Unreleased -> v1.14.50
- Update AGENTS.md, .gitea/README.md, validate.yml: remove all references
  to the old single-Dockerfile pipeline and WIP migration plan
2026-05-14 19:39:45 +02:00
joakimp 7dc836ab66 fix: replace echo -e heredoc with brace-block in build-variant tags steps
Validate / docs-check (push) Successful in 13s
Validate / validate-base (push) Successful in 12m21s
Validate / validate-omos (push) Successful in 18m38s
Validate / validate-with-pi (push) Successful in 13m23s
Validate / validate-omos-with-pi (push) Successful in 16m34s
echo -e doesn't interpret \n in /bin/sh (dash), which is the default
shell in catthehacker/ubuntu:act-latest. This caused steps.tags.outputs.tags
to be empty, resulting in 'tag is needed when pushing to registry' from buildx.

Also fixes a secondary bug: TAGS='${TAGS}\n...' stored a literal backslash-n
rather than a real newline, which would have broken multi-tag output when
promote_latest=true.

Fix: replace with a brace block using plain echo, which produces actual newlines
and works in both sh and bash.
2026-05-10 11:59:04 +02:00
13 changed files with 411 additions and 1172 deletions
+19 -19
View File
@@ -8,11 +8,10 @@ the build pipeline is shaped the way it is, you're in the right place.
| File | Trigger | Role |
|---|---|---|
| [`workflows/docker-publish.yml`](workflows/docker-publish.yml) | `push: tags: v*` | **Production release pipeline.** Multi-arch build of all four variants (`base`, `omos`, `with-pi`, `omos-with-pi`), publish to Docker Hub, update Hub description. ~165180 min wall clock. |
| [`workflows/docker-publish-split.yml`](workflows/docker-publish-split.yml) | `workflow_dispatch` (manual) | **Experimental split-base pipeline.** Two-phase build: shared `base-<hash>` published once, then four thin variant deltas. Estimated ~3040 min on cache hit, ~7090 min when base needs rebuilding. Not yet validated end-to-end; once 12 dispatch test runs prove it, this will take over `on: push: tags: v*` and `docker-publish.yml` will be retired. |
| [`workflows/docker-publish-split.yml`](workflows/docker-publish-split.yml) | `push: tags: v*` | **Production release pipeline.** Two-phase split-base build: shared `base-<hash>` published once (skipped on cache hit), then four parallel variant deltas. ~4080 min wall clock depending on runner count and whether base needs rebuilding. |
| [`workflows/validate.yml`](workflows/validate.yml) | `push: branches: main` + PR | **Lightweight gate.** amd64-only smoke test of all four variants + `DOCKER_HUB.md` sync check. ~30 min. Fires on every push to `main`. |
## Why two release pipelines exist
## Why the split-base pipeline exists
opencode-devbox publishes **four image variants** (`base`, `omos`, `with-pi`, `omos-with-pi`) × **two architectures** (amd64, arm64) = **eight image tags per release**. Today's runners are 2 self-hosted gitea Actions runners. arm64 builds are emulated under QEMU, which is the dominant cost (~35x slower than native).
@@ -82,11 +81,20 @@ 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`:
@@ -263,26 +271,18 @@ This catches regressions before they reach a tag push. Wall clock ~30 min.
2. **Run a second dispatch** to confirm cache-hit behavior:
`base-decide` should set `need_build=false`, `build-base` should be
skipped entirely, total wall clock should drop to ~2540 min.
3. **Cut over.** In a single commit:
- Edit `docker-publish-split.yml`: change `on: workflow_dispatch:` to
`on: push: tags: v*` and wire `$GITHUB_REF` into the `release_tag`
input, set `promote_latest=true` for production runs.
- Delete `docker-publish.yml`.
- Delete the original `Dockerfile` (keep `Dockerfile.base` +
`Dockerfile.variant`).
- Update `CHANGELOG.md`: promote the "Build pipeline" Unreleased entry.
4. **Tag a release.** First production release on the new pipeline. Watch
it like a hawk for the first run.
3. **Cut over** *done as of v1.14.50.* `docker-publish-split.yml` now
triggers on `push: tags: v*`. `docker-publish.yml` and original
`Dockerfile` deleted.
4. **Tag a release.** First production release on the new pipeline.
## Related docs
- [`AGENTS.md`](../AGENTS.md) — domain facts, release-day checklist,
documentation coupling rules. Read first when modifying CI behavior.
- [`CHANGELOG.md`](../CHANGELOG.md) — the build pipeline rewrite is
recorded under `Unreleased` until the cutover lands.
- `Dockerfile`, `Dockerfile.base`, `Dockerfile.variant` — production
single-Dockerfile build and the split-base counterparts. Comments at
the top of each explain its role.
- [`CHANGELOG.md`](../CHANGELOG.md) — build pipeline rewrite landed in v1.14.50.
- `Dockerfile.base`, `Dockerfile.variant` — the split-base Dockerfiles.
Comments at the top of each explain their role.
- [`scripts/smoke-test.sh`](../scripts/smoke-test.sh) — invoked by all
three workflows; this is the single source of truth for "what does a
built image have to satisfy".
+91 -37
View File
@@ -1,13 +1,7 @@
name: Publish Docker Image (split-base)
name: Publish Docker Image
# Two-phase split-base build pipeline. Lives ALONGSIDE the original
# docker-publish.yml during the migration window. Triggers only on
# workflow_dispatch (manual) so it doesn't conflict with the production
# tag-trigger pipeline.
#
# Once we've validated 1-2 successful runs and verified output
# byte-for-byte against the original, this workflow takes over `on:
# push: tags: v*` and the original is retired.
# Two-phase split-base build pipeline. Replaces the original
# docker-publish.yml single-Dockerfile pipeline.
#
# Pipeline shape:
# 1. base-decide compute base hash from Dockerfile.base + rootfs/
@@ -22,14 +16,17 @@ name: Publish Docker Image (split-base)
# 6. update-description patch Docker Hub description (unchanged).
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag to publish (e.g. v1.14.42-split). Pushed to Docker Hub as both the literal tag and `latest*`-aliases.'
required: true
default: 'v0.0.0-split-test'
description: 'Release tag to publish (e.g. v1.14.50). Used only for workflow_dispatch runs — tag-triggered runs derive the tag from github.ref.'
required: false
default: ''
promote_latest:
description: 'Update latest/latest-omos/latest-with-pi/latest-omos-with-pi aliases (set false for test runs)'
description: 'Update latest/* aliases (default true for tag-push, set false for manual test runs)'
required: false
default: 'false'
@@ -40,6 +37,8 @@ concurrency:
env:
BUILDKIT_PROGRESS: plain
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox
RELEASE_TAG: ${{ github.ref_type == 'tag' && github.ref_name || inputs.release_tag }}
PROMOTE_LATEST: ${{ github.ref_type == 'tag' && 'true' || inputs.promote_latest }}
# ───────────────────────────────────────────────────────────────────
# Reusable disk-reclaim snippet — strips catthehacker toolchains and
@@ -64,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
)
@@ -354,12 +362,14 @@ jobs:
- name: Compute version-specific tags
id: tags
run: |
VERSION="${{ inputs.release_tag }}"
TAGS="${IMAGE}:${VERSION}"
if [ "${{ inputs.promote_latest }}" = "true" ]; then
TAGS="${TAGS}\n${IMAGE}:latest"
VERSION="${{ env.RELEASE_TAG }}"
{ echo "tags<<EOF"
echo "${IMAGE}:${VERSION}"
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
echo "${IMAGE}:latest"
fi
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v7
with:
context: .
@@ -400,12 +410,14 @@ jobs:
- name: Compute version-specific tags
id: tags
run: |
VERSION="${{ inputs.release_tag }}"
TAGS="${IMAGE}:${VERSION}-omos"
if [ "${{ inputs.promote_latest }}" = "true" ]; then
TAGS="${TAGS}\n${IMAGE}:latest-omos"
VERSION="${{ env.RELEASE_TAG }}"
{ echo "tags<<EOF"
echo "${IMAGE}:${VERSION}-omos"
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
echo "${IMAGE}:latest-omos"
fi
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v7
with:
context: .
@@ -446,12 +458,14 @@ jobs:
- name: Compute version-specific tags
id: tags
run: |
VERSION="${{ inputs.release_tag }}"
TAGS="${IMAGE}:${VERSION}-with-pi"
if [ "${{ inputs.promote_latest }}" = "true" ]; then
TAGS="${TAGS}\n${IMAGE}:latest-with-pi"
VERSION="${{ env.RELEASE_TAG }}"
{ echo "tags<<EOF"
echo "${IMAGE}:${VERSION}-with-pi"
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
echo "${IMAGE}:latest-with-pi"
fi
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v7
with:
context: .
@@ -492,12 +506,14 @@ jobs:
- name: Compute version-specific tags
id: tags
run: |
VERSION="${{ inputs.release_tag }}"
TAGS="${IMAGE}:${VERSION}-omos-with-pi"
if [ "${{ inputs.promote_latest }}" = "true" ]; then
TAGS="${TAGS}\n${IMAGE}:latest-omos-with-pi"
VERSION="${{ env.RELEASE_TAG }}"
{ echo "tags<<EOF"
echo "${IMAGE}:${VERSION}-omos-with-pi"
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
echo "${IMAGE}:latest-omos-with-pi"
fi
echo -e "tags<<EOF\n${TAGS}\nEOF" >> "$GITHUB_OUTPUT"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- uses: docker/build-push-action@v7
with:
context: .
@@ -519,12 +535,40 @@ jobs:
- build-variant-omos
- build-variant-with-pi
- build-variant-omos-with-pi
if: inputs.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 \
@@ -543,7 +587,17 @@ jobs:
- build-variant-omos
- build-variant-with-pi
- build-variant-omos-with-pi
if: inputs.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
-581
View File
@@ -1,581 +0,0 @@
name: Publish Docker Image
on:
push:
tags:
- 'v*'
# Serialize concurrent runs of the same workflow on the same ref so the
# build jobs can't race `docker system prune` in the smoke gates
# (pruning from one job can nuke another job's in-flight buildx cache).
# cancel-in-progress: false — tag pushes are release events, we never
# want to silently drop one.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
# Plain progress output from BuildKit — critical for diagnosing stalls
# inside arm64-under-QEMU builds where the default collapsed progress UI
# hides which step is stuck.
env:
BUILDKIT_PROGRESS: plain
# Runner disk pressure notes:
# Gitea Actions runners use `catthehacker/ubuntu:act-latest` on a shared host
# with limited overlay space (~40 GB, often 70%+ used at start). Two jobs
# per variant:
# * smoke gate (amd64 only, `load: true` into local dockerd for smoke
# testing) — peak disk = tarball + unpacked image + buildx cache. The
# `Reclaim runner disk` step below strips catthehacker-resident
# toolchains and prunes stale docker state before buildx starts.
# * build job (amd64 + arm64, `push-by-digest` streaming directly to
# Docker Hub, no local unpack). Peak disk on push-by-digest is
# BuildKit's content store only — much smaller than `load: true`.
# `docker/build-push-action@v7` with comma-separated platforms
# publishes a proper multi-arch manifest in one step.
#
# Why not matrix + digest artifacts?
# An earlier revision split each arch into its own matrix job and used
# `actions/upload-artifact` to pass digests to a merge job. On Gitea
# Actions, `actions/{upload,download}-artifact@v4+` fails with
# `GHESNotSupportedError` — v4 relies on a GitHub-specific Artifact
# API that Gitea doesn't implement. Rather than downgrade to @v3 (the
# last Gitea-compatible release) we collapsed back to single-job
# multi-arch push. The matrix only helps when the build literally
# cannot fit on one runner, which push-by-digest + reclaim no longer
# hits for this image.
#
# Gitea Actions gotchas baked into this file:
# * `actions/{upload,download}-artifact` must stay at @v3 on Gitea.
# * Step scripts run under /bin/sh (dash) — no bash-isms like
# ${VAR//a/b}. Use `tr` or explicit `shell: bash`.
# * `docker/build-push-action@v7` with `platforms: a,b` works for
# multi-arch push natively; no matrix/merge dance needed.
jobs:
# ── Smoke test (amd64 only, gates the push jobs) ────────────────────
smoke-base:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
# See docker-publish.yml preamble. `load: true` peak disk = tarball
# + unpacked image + buildx cache; the image now crosses the 40 GB
# runner overlay's starting headroom. Strip catthehacker-resident
# toolchains and any stale docker state up front.
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker system df || true
docker system prune -af --volumes || true
docker builder prune -af || true
df -h / || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Build and load amd64 image for smoke test
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: false
load: true
tags: opencode-devbox:smoke-base
- name: Smoke test (amd64)
run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
smoke-omos:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker system df || true
docker system prune -af --volumes || true
docker builder prune -af || true
df -h / || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Build and load amd64 image for smoke test
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: false
load: true
build-args: |
INSTALL_OMOS=true
tags: opencode-devbox:smoke-omos
- name: Smoke test (amd64)
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
smoke-with-pi:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker system df || true
docker system prune -af --volumes || true
docker builder prune -af || true
df -h / || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Build and load amd64 image for smoke test
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: false
load: true
build-args: |
INSTALL_PI=true
tags: opencode-devbox:smoke-with-pi
- name: Smoke test (amd64)
run: bash scripts/smoke-test.sh opencode-devbox:smoke-with-pi --variant with-pi
smoke-omos-with-pi:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker system df || true
docker system prune -af --volumes || true
docker builder prune -af || true
df -h / || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Build and load amd64 image for smoke test
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: false
load: true
build-args: |
INSTALL_OMOS=true
INSTALL_PI=true
tags: opencode-devbox:smoke-omos-with-pi
- name: Smoke test (amd64)
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
# ── Multi-arch push (single job per variant, comma-separated platforms) ─
build-base:
runs-on: ubuntu-latest
needs: smoke-base
timeout-minutes: 90
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
# Lighter reclaim than the smoke-gate version: push-by-digest
# doesn't write to host dockerd, so `docker system prune` adds
# little. BuildKit cache from prior runs is the thing to clear.
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker builder prune -af || true
df -h / || true
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build and push (multi-arch)
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest
build-omos:
runs-on: ubuntu-latest
needs: smoke-omos
timeout-minutes: 90
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker builder prune -af || true
df -h / || true
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build and push (multi-arch)
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
INSTALL_OMOS=true
tags: |
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos
build-with-pi:
runs-on: ubuntu-latest
needs: smoke-with-pi
timeout-minutes: 90
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker builder prune -af || true
df -h / || true
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build and push (multi-arch)
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
INSTALL_PI=true
tags: |
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-with-pi
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-with-pi
build-omos-with-pi:
runs-on: ubuntu-latest
needs: smoke-omos-with-pi
timeout-minutes: 90
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Force IPv4 for Docker Hub
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
- name: Reclaim runner disk
run: |
set -x
df -h / || true
rm -rf \
/opt/hostedtoolcache \
/opt/microsoft \
/opt/az \
/opt/ghc \
/usr/local/.ghcup \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/lib/android \
/usr/local/share/powershell \
/usr/local/share/chromium \
/usr/local/share/boost \
/usr/lib/jvm 2>/dev/null || true
apt-get clean || true
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* || true
docker builder prune -af || true
df -h / || true
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
with:
driver-opts: network=host
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build and push (multi-arch)
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
build-args: |
INSTALL_OMOS=true
INSTALL_PI=true
tags: |
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:${{ steps.version.outputs.version }}-omos-with-pi
${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox:latest-omos-with-pi
update-description:
runs-on: ubuntu-latest
needs: [build-base, build-omos, build-with-pi, build-omos-with-pi]
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Update Docker Hub description
run: |
TOKEN=$(curl -s -X POST https://hub.docker.com/v2/auth/token \
-H "Content-Type: application/json" \
-d '{"identifier":"${{ vars.DOCKERHUB_USERNAME }}","secret":"${{ secrets.DOCKERHUB_TOKEN }}"}' \
| jq -r .access_token)
if [ "$TOKEN" = "null" ] || [ -z "$TOKEN" ]; then
echo "::error::Failed to authenticate with Docker Hub API"
exit 1
fi
HTTP_CODE=$(jq -n \
--rawfile full DOCKER_HUB.md \
--arg short "Portable AI dev environment for opencode. Debian-based with git, Node.js, AWS CLI, and SSH support." \
'{"full_description": $full, "description": $short}' | \
curl -s -o /tmp/hub-response.txt -w "%{http_code}" -X PATCH \
"https://hub.docker.com/v2/repositories/${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox/" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @-)
echo "Docker Hub API returned: $HTTP_CODE"
if [ "$HTTP_CODE" != "200" ]; then
echo "Response body:"
cat /tmp/hub-response.txt
echo "::error::Docker Hub description update failed with HTTP $HTTP_CODE"
exit 1
fi
+54 -2
View File
@@ -2,8 +2,24 @@ name: Validate
# Lightweight validation on pushes to main. Builds single-arch (amd64),
# runs the smoke test, and checks image size — without pushing anything
# to Docker Hub. Tag pushes are handled by docker-publish.yml which
# does the full multi-arch build-and-push.
# to Docker Hub. Tag pushes are handled by docker-publish-split.yml which
# does the full multi-arch split-base build-and-push.
#
# Trade-off: variant builds here use the published `base-latest` image
# from Docker Hub as their parent, NOT a locally-built base. This is
# because `docker/build-push-action@v7` runs each invocation in its own
# buildx container context, so an image loaded into the host docker
# daemon by step N is not visible to step N+1's buildx invocation.
# Building base + variant in the same job would require either pushing
# the base to a registry or sharing a buildx instance across steps — both
# significantly more complex than just using the published base.
#
# Consequence: PRs/pushes that change Dockerfile.base, rootfs/, or
# entrypoint*.sh are NOT exercised by this workflow. The release path
# (docker-publish-split.yml on tag push) does build the new base, so
# release tags are the gate that fully validates base-image changes.
# The base-change-warning job below surfaces a runtime warning when this
# blind-spot applies.
on:
push:
@@ -34,6 +50,33 @@ jobs:
run: |
python3 scripts/generate-dockerhub-md.py --check
base-change-warning:
# Surfaces a warning when this commit changes base-image inputs
# (Dockerfile.base, rootfs/, entrypoint*.sh). validate.yml uses
# Hub's base-latest as the parent for variant builds, so changes to
# those files are NOT exercised here — only release tags rebuild the
# base via docker-publish-split.yml.
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect base-input changes
run: |
set -e
if ! git diff --name-only HEAD~1 HEAD 2>/dev/null \
| grep -qE '^(Dockerfile\.base|rootfs/|entrypoint.*\.sh)$'; then
echo "No base-image inputs changed in this commit — validate.yml fully exercises the published base-latest."
exit 0
fi
echo "::warning::This commit changes base-image inputs (Dockerfile.base, rootfs/, or entrypoint*.sh). validate.yml uses Hub's base-latest as the parent for variant builds, so the new base is NOT exercised by this workflow. Cut a release tag, or run a workflow_dispatch of docker-publish-split.yml against a test tag (e.g. v0.0.0-base-test, promote_latest=false) for end-to-end validation of the new base."
echo "Changed base-input files:"
git diff --name-only HEAD~1 HEAD | grep -E '^(Dockerfile\.base|rootfs/|entrypoint.*\.sh)$'
validate-base:
runs-on: ubuntu-latest
container:
@@ -83,9 +126,12 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.variant
platforms: linux/amd64
push: false
load: true
build-args: |
BASE_IMAGE=joakimp/opencode-devbox:base-latest
tags: opencode-devbox:ci-base
- name: Smoke test
@@ -137,10 +183,12 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.variant
platforms: linux/amd64
push: false
load: true
build-args: |
BASE_IMAGE=joakimp/opencode-devbox:base-latest
INSTALL_OMOS=true
tags: opencode-devbox:ci-omos
@@ -193,10 +241,12 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.variant
platforms: linux/amd64
push: false
load: true
build-args: |
BASE_IMAGE=joakimp/opencode-devbox:base-latest
INSTALL_PI=true
tags: opencode-devbox:ci-with-pi
@@ -249,10 +299,12 @@ jobs:
uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.variant
platforms: linux/amd64
push: false
load: true
build-args: |
BASE_IMAGE=joakimp/opencode-devbox:base-latest
INSTALL_OMOS=true
INSTALL_PI=true
tags: opencode-devbox:ci-omos-with-pi
+34 -8
View File
@@ -2,12 +2,12 @@
## Project overview
Docker image packaging [opencode](https://opencode.ai) into a production-ready dev container. Two image variants (base and omos) are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfile, entrypoint scripts, docker-compose, documentation).
Docker image packaging [opencode](https://opencode.ai) into a production-ready dev container. Image variants are published to Docker Hub via Gitea Actions CI. Not a library or application — this is infrastructure (Dockerfiles, entrypoint scripts, docker-compose, documentation).
## File roles
- `Dockerfile` — production single-Dockerfile build. Variants are gated by build args: `INSTALL_OMOS` (Bun + multi-agent layer), `INSTALL_OPENCODE` (default true), `INSTALL_PI` (default false), `INSTALL_MEMPALACE` (default true). All GitHub-sourced binaries are pinned with version ARGs.
- `Dockerfile.base` and `Dockerfile.variant`**WIP, branch `feat/split-build` only.** Two-Dockerfile split-base build: base contains all variant-independent layers; variant `FROM`s the base and adds only opencode/omos/pi installs. Used by `docker-publish-split.yml` (workflow_dispatch only) for parallel testing alongside the production pipeline. See CHANGELOG `Unreleased` for the migration plan and trade-offs.
- `Dockerfile.base` — variant-independent layers (apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints). Published as `joakimp/opencode-devbox:base-<sha12>`. Rebuilt only when its content hash changes.
- `Dockerfile.variant` `FROM`s the base and adds only opencode/omos/pi installs gated by build args: `INSTALL_OPENCODE` (default true), `INSTALL_OMOS`, `INSTALL_PI`, and `INSTALL_MEMPALACE`. All GitHub-sourced binaries are pinned with version ARGs.
- `entrypoint.sh` — runs as root: UID/GID adjustment, SSH permissions, volume ownership fixes (skipped via `.devbox-owner` sentinel when ownership is already correct). Then drops to developer via gosu. Volume ownership loop covers `~/.pi/` when `INSTALL_PI=true`.
- `entrypoint-user.sh` — runs as developer: git config, opencode.jsonc generation (delegated to `generate-config.py`), pi-toolkit + pi-extensions deploy (when pi installed), pi settings.json bootstrap, mempalace pi-bridge symlink, skillset auto-deploy from mounted skillset repo, OMOS setup.
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint).
@@ -17,21 +17,45 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth.
- `.gitea/README.md`**read this first** if you're touching CI. Architectural overview of the build pipeline (production vs split-base), wall-clock estimates, NPM_CONFIG_PREFIX gotcha, runner expectations, migration plan.
- `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
- `.gitea/workflows/docker-publish.yml` — production CI pipeline on tag push: smoke-test each variant on amd64, then full multi-arch (amd64 + arm64) build-and-push, then update Docker Hub description.
- `.gitea/workflows/docker-publish-split.yml`**WIP, branch `feat/split-build` only.** Two-phase split-base pipeline. Triggers on `workflow_dispatch` only so it runs alongside the production pipeline without conflict. Pushes to user-supplied `release_tag` input (e.g. `v0.0.0-split-test`); `latest*` aliases only updated when `promote_latest: true`. Compute base hash, conditionally build base, then 4 variant deltas in parallel.
- `.gitea/workflows/docker-publish-split.yml` — production CI pipeline on tag push (`v*`). Two-phase split-base: computes base hash, conditionally builds base, runs 4 parallel smoke tests, then 4 parallel multi-arch variant builds, promotes `base-latest` alias, updates Docker Hub description.
## Versioning scheme
Tags follow `v{opencode_version}[letter]` — e.g. `v1.14.20` for the first build on a new opencode release, and `v1.14.20b`, `v1.14.20c`, … for subsequent rebuilds on the same opencode version.
- The number tracks the opencode npm version (see `OPENCODE_VERSION` ARG in `Dockerfile`).
- The number tracks the opencode npm version (see `OPENCODE_VERSION` ARG in `Dockerfile.variant`).
- **No letter suffix** on the first build of a new opencode version — the bare `v{opencode_version}` tag is the canonical release.
- **Letter suffix is the build ordinal**, starting at `b` for the second build. The letter `a` is **never used** — think of the suffix as counting rebuilds: `b = 2nd, c = 3rd, d = 4th, …`. For opencode version `1.14.20`: first build `v1.14.20`, second `v1.14.20b`, third `v1.14.20c`, and so on.
- A letter suffix is only used for container-level rebuilds — tooling changes, CVE fixes, doc-driven rebuilds, entrypoint bugfixes — that don't change the underlying opencode version.
CI produces eight Docker Hub tags per release: `vX.Y.Z[n]`, `latest`, `vX.Y.Z[n]-omos`, `latest-omos`, `vX.Y.Z[n]-with-pi`, `latest-with-pi`, `vX.Y.Z[n]-omos-with-pi`, `latest-omos-with-pi` — one tag pair (versioned + floating alias) per build variant.
When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile` and update the comment in `.env.example` if it names a specific model/version for context.
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
@@ -44,6 +68,8 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
- `.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.
**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, 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.
- **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`.
- **Shell scripts use `set -euo pipefail`** — both entrypoints are strict. Errors in volume chown or SSH permission operations are intentionally suppressed with `|| true`.
@@ -66,7 +92,7 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
- **`docker/build-push-action@v7` with `platforms: linux/amd64,linux/arm64` handles multi-arch push natively in a single job** — produces a proper manifest list, no matrix or merge step needed. An earlier revision split into per-arch matrix jobs with digest artifacts, but that pattern requires `actions/{upload,download}-artifact@v4+` which Gitea Actions doesn't support (see below).
- **`actions/upload-artifact` and `actions/download-artifact` must stay at @v3 on Gitea.** v4+ uses a GitHub-Enterprise-specific Artifact API; runs fail with `GHESNotSupportedError`. If you need artifacts for a new reason (build logs, SBOMs, etc.), pin @v3 explicitly.
- **Step scripts run under `/bin/sh` (dash), not bash.** Avoid bash-isms like `${VAR//a/b}` parameter-pattern substitution; use POSIX alternatives (`tr`, `sed`) or declare `shell: bash` on the step.
- **`BUILDKIT_PROGRESS=plain`** is set at workflow level on `docker-publish.yml` so arm64-under-QEMU builds log each layer line-by-line. The default collapsed progress UI hides which step is stalled, which made diagnosing earlier hangs expensive.
- **`BUILDKIT_PROGRESS=plain`** is set at workflow level on `docker-publish-split.yml` so arm64-under-QEMU builds log each layer line-by-line. The default collapsed progress UI hides which step is stalled, which made diagnosing earlier hangs expensive.
## Testing changes
+117 -4
View File
@@ -8,13 +8,126 @@ Tags follow `v{opencode_version}[letter]` — bare tag for the first build on a
## Unreleased
Docs:
## v1.15.10 — 2026-05-23
- **New: `.gitea/README.md`** — architectural overview of the build pipeline. Documents the production single-Dockerfile path vs the merged-but-unvalidated split-base path, hash-driven base cache reuse, wall-clock estimates, the `NPM_CONFIG_PREFIX` variant-override pattern, runner expectations (catthehacker container, disk reclaim, concurrency, gitea-actions @v4 artifact gotcha), and the cutover plan. Auto-renders when navigating to `.gitea/` in the gitea web UI. Linked from `AGENTS.md` as the first thing to read when touching CI.
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.
Build pipeline (merged to main as `Dockerfile.base` + `Dockerfile.variant` + `.gitea/workflows/docker-publish-split.yml`, NOT yet validated end-to-end — the `workflow_dispatch` test against `:base-<hash>` + `:v0.0.0-split-test*` aliases is still the gating step before this can take over `on: push: tags: v*`):
No image-content changes beyond the version bumps; cache hit expected on `base-35ee5fe7861a` (no `Dockerfile.base` or `rootfs/` edits since v1.14.50b).
- **New: split-base build pipeline.** `Dockerfile.base` (variant-independent layers — apt, locales, AWS CLI, Node.js, mempalace, gitea-mcp, user setup, chromadb prewarm, ENVs, entrypoints) builds once and is published as `joakimp/opencode-devbox:base-<sha>`. `Dockerfile.variant` `FROM`s that base and adds only opencode/omos/pi installs (or skips them per build-args). Companion workflow `.gitea/workflows/docker-publish-split.yml` runs as a `workflow_dispatch`-only pipeline alongside the existing `docker-publish.yml` so they don't conflict. Hash-driven base reuse: a content hash of `Dockerfile.base + rootfs/ + entrypoint*.sh` becomes the base tag; if the tag already exists on Docker Hub, the base build is skipped entirely. Estimated wall clock: version-bump-only release ~3040 min (vs ~165180 min today); base-touching release ~6070 min. Trade-off: two Dockerfiles to maintain, and `npm install -g` in the variant must override `NPM_CONFIG_PREFIX=/usr` per-RUN to keep baked binaries off the volume-shadowed path. Once 12 successful workflow_dispatch runs validate the output against the existing pipeline, the new workflow takes over `on: push: tags: v*` and the original is retired.
### 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).
No container-side changes. This is a tag-only retag to re-run the build on a now-healthy runner fleet. Same `base-35ee5fe7861a` from v1.14.50b is reused via hash-cache hit; only the four variant deltas are rebuilt and published.
### Postmortem: v1.14.50 / v1.14.50b runner-fleet incident
Two orthogonal runner-host issues compounded across runs 285291:
1. **AVX-less runner shadowing the new fleet.** A pre-migration `act_runner` container on `nyvaken` (Sandy Bridge E3-12xx, has AVX but no AVX2; 4 weeks old, name `act_runner-runner-1`) collided with the orchestrator's freshly deployed `runner-1` VM (Broadwell-EP host, fully AVX2-capable). Gitea scheduled jobs to both. Jobs landing on the nyvaken container `npm install -g opencode-ai@1.14.50` succeeded, then ran `opencode --version` postinstall → the bundled Bun (v1.3.13 baseline) emitted `CPU lacks AVX support`, panicked, and SIGILLed (exit code 132).
2. **Containerd shared-state race at `capacity: 2`.** The new VM-based runners initially ran `act_runner` with `capacity: 2`, scheduling two concurrent jobs on a single host. Both jobs would invoke `docker/setup-buildx-action@v4`, which pulls `moby/buildkit:buildx-stable-1`. Containerd's content store raced on identical sha256 ingestion, surfacing as `commit failed: rename .../ingest/.../data .../blobs/sha256/...: no such file or directory` or `failed to extract layer: failed to Lchown ...`.
A secondary issue surfaced: **Proxmox VM `cpu:` field defaults mask AVX**. The newly-cloned runner VMs had no explicit `cpu:` line in `qm config` and inherited Proxmox's recent default `x86-64-v2-AES`, which excludes AVX even though the Broadwell-EP host silicon has full `avx2`. Fix: `qm set <vmid> --cpu x86-64-v3` (or `host` for full passthrough), then `qm shutdown` + `qm start` (live reboot is not enough). Verified inside guest with `grep -m1 -oE 'avx[2]?' /proc/cpuinfo`.
Additionally, when `promote-base-latest`'s `needs:` graph requires *all four* `build-variant-*` jobs to succeed, partial publishes leave the `base-latest` Hub alias never advancing. Workaround used during recovery: manually re-tag the new base hash via Docker Hub registry manifest API (`PUT /v2/<repo>/manifests/base-latest` with the body of `GET /v2/<repo>/manifests/base-<sha>`) using a granular Hub PAT. No blob copy needed since blobs are content-addressed.
### Recovery actions taken (orchestrator + this repo)
- Orchestrator (cloud-init + ansible repos): set explicit `cpu_type: x86-64-v3` in all runner host yaml files; provision.sh now applies `qm set --cpu` after clone; added runner-3 on proxmox003 for anti-affinity (one runner per Proxmox node); dropped `capacity: 2 → 1` on all runners; bumped `act_runner` 0.3.1 → 0.6.1 across the fleet; documented the CPU-type gotcha as gotcha #9 in cloud-init AGENTS.md and a section in proxmox-guide.md.
- User: retired the legacy `act_runner-runner-1` container on nyvaken; cleaned up stale runner registrations in Gitea Site Admin → Actions → Runners.
- This repo: no changes needed in Dockerfile.base / Dockerfile.variant; v1.14.50c is a tag-only retag.
### Fleet state at v1.14.50c
3 runners (runner-1@proxmox001, runner-2@proxmox002, runner-3@proxmox003), all `act_runner` v0.6.1, all `capacity: 1`, all expose AVX + AVX2 to the guest. No name collisions. Estimated wall clock for v1.14.50c (cache-hit base, 4 variant deltas across 3 runners with capacity:1): ~4050 min.
## v1.14.50b — 2026-05-14
Rebuild of v1.14.50 with two fixes — the v1.14.50 release was incomplete (smokes failed under containerd contention; build-variant jobs skipped; base-latest never promoted to Docker Hub).
- **Force fresh base rebuild.** Added a `BASE_REBUILD_DATE` comment header to `Dockerfile.base` to invalidate the content hash and trigger a full base rebuild. Picks up ~5 days of Debian trixie security updates and other apt-tracked packages. The comment also documents the pattern for future intentional base-rebuilds without other code changes (recommended cadence: once per release).
- **First publish of `base-latest` alias.** `promote-base-latest` runs unconditionally on tag push (`PROMOTE_LATEST=true`), so this release is the first to put `joakimp/opencode-devbox:base-latest` on Docker Hub. Required before pi-devbox (and any other downstream image FROMing the base) can build.
## v1.14.50 — 2026-05-14
opencode 1.14.44 → 1.14.50 bump. First release on the split-base build pipeline.
- **Bump:** opencode 1.14.44 → 1.14.50 (`OPENCODE_VERSION` in `Dockerfile.variant`).
- **Infrastructure: split-base pipeline cutover.** `Dockerfile.base` + `Dockerfile.variant` replace the single `Dockerfile`. `docker-publish-split.yml` (now renamed to `docker-publish.yml` in spirit — triggers on `push: tags: v*`) replaces the old `docker-publish.yml`. The original `Dockerfile` and `docker-publish.yml` are deleted. Hash-driven base reuse: version-bump-only releases skip the base build entirely (~4080 min wall clock with 4 runners vs ~165180 min previously). Validated across two `workflow_dispatch` test runs (`:v0.0.0-split-test` tags on Docker Hub).
- **Fix:** `echo -e` heredoc replaced with POSIX-compatible brace-block for multiline `$GITHUB_OUTPUT` writes in the four `build-variant-*` jobs. `echo -e` does not interpret `\n` in `/bin/sh` (dash), causing `steps.tags.outputs.tags` to be empty and buildx to fail with "tag is needed when pushing to registry".
- **Docs:** New `.gitea/README.md` — architectural overview of the split-base pipeline, hash logic, wall-clock estimates, runner expectations, and the migration plan.
## v1.14.44 — 2026-05-09
+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>.
-475
View File
@@ -1,475 +0,0 @@
# opencode-devbox — portable AI dev environment
# Debian-based container with opencode and configurable dev tools
ARG DEBIAN_VERSION=trixie-slim
FROM debian:${DEBIAN_VERSION} AS base
ARG TARGETARCH
ARG OPENCODE_VERSION=1.14.44
LABEL maintainer="joakimp"
LABEL description="Portable opencode developer container"
LABEL org.opencontainers.image.source="https://gitea.jordbo.se/joakimp/opencode-devbox"
# Avoid interactive prompts during build
ENV DEBIAN_FRONTEND=noninteractive
# ── Core system packages ─────────────────────────────────────────────
# apt-get upgrade picks up any security/CVE fixes published between
# debian:trixie-slim base-image rebuilds. Paired with the index update
# and the install in the same layer so we don't bloat image history.
RUN apt-get update && \
apt-get upgrade -y --no-install-recommends && \
apt-get install -y --no-install-recommends \
ca-certificates \
curl \
wget \
git \
openssh-client \
gnupg \
jq \
ripgrep \
fd-find \
tree \
less \
htop \
tmux \
make \
patch \
diffutils \
git-crypt \
age \
file \
sudo \
locales \
procps \
unzip \
gcc \
g++ \
rsync \
python3-pip \
python3-venv \
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
#
# Version policy for the binaries below:
# • Default is `latest` — resolved at build time by following the
# /releases/latest redirect on GitHub and reading the tag from the
# Location header. This means every tagged image picks up the newest
# upstream release, with no risk of running months-old CVE-affected
# binaries.
# • Explicit pins still work: pass `--build-arg GOSU_VERSION=1.19` etc.
# Useful for reproducibility or rolling back a bad upstream release.
# • Resolved versions are printed during build and re-checked by the
# smoke test (scripts/smoke-test.sh), so drift is visible in CI logs.
#
# The helper `resolve_latest` reads the redirected tag (e.g. "v0.26.1")
# and strips a leading "v" if present, yielding a plain version string.
# gosu — privilege de-escalation
ARG GOSU_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
V="${GOSU_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing gosu ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \
chmod +x /usr/local/bin/gosu && \
gosu --version
# fzf — fuzzy finder
ARG FZF_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
V="${FZF_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing fzf ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \
fzf --version
# git-lfs — Git Large File Storage
ARG GIT_LFS_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
V="${GIT_LFS_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing git-lfs ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \
install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \
rm -rf /tmp/git-lfs-${V} && \
git lfs install --system && \
git-lfs --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) && \
V="${NVIM_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing neovim ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
nvim --version | head -1
# bat — syntax-highlighted cat replacement
ARG BAT_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
V="${BAT_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing bat ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
rm -rf /tmp/bat-v${V}-* && \
bat --version
# eza — modern ls replacement
ARG EZA_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
V="${EZA_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing eza ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \
eza --version | head -1
# zoxide — smarter cd command
ARG ZOXIDE_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
V="${ZOXIDE_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing zoxide ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \
zoxide --version
# uv — fast Python package manager (replaces pip, venv, pyenv)
# Note: uv releases don't prefix tags with "v" (e.g. tag is "0.11.8").
ARG UV_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
V="${UV_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing uv ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \
rm -rf /tmp/uv-* && \
uv --version
# ── Optional: MemPalace — local-first AI memory system ───────────────
# Provides semantic search over conversation history via 29 MCP tools.
# Palace data persists via the devbox-palace named volume.
# The embedding model (~300 MB) is downloaded on first use and cached
# in the palace directory.
#
# Installed via `uv tool install` into an isolated venv at
# /opt/uv-tools/mempalace/. The `mempalace` CLI goes directly on PATH;
# the MCP server is reached via the /usr/local/bin/mempalace-mcp-server
# wrapper (rootfs/usr/local/bin/mempalace-mcp-server), since system
# python3 cannot import from the isolated venv.
#
# Disable with --build-arg INSTALL_MEMPALACE=false to shave ~300 MB off
# the image (chromadb, torch-adjacent deps).
ARG INSTALL_MEMPALACE=true
ENV UV_TOOL_DIR=/opt/uv-tools
ENV UV_TOOL_BIN_DIR=/usr/local/bin
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
mkdir -p /opt/uv-tools && \
uv tool install --no-cache mempalace && \
/opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
fi
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
# Thin wrappers (`mempalace-session`, `mempalace-docs`) that delegate to
# the mempalace Python CLI for two common scheduled tasks:
# - mempalace-session: mines opencode's SQLite session history into
# the palace (wing_conversations). Referenced by contrib/ scheduler
# templates (systemd user timer, cron) in the toolkit repo.
# - mempalace-docs: mines project docs into a per-project wing.
# Repo source of truth: https://gitea.jordbo.se/joakimp/mempalace-toolkit
#
# Requires INSTALL_MEMPALACE=true (wrappers shell out to `mempalace`).
# Disable with --build-arg INSTALL_MEMPALACE_TOOLKIT=false if you don't
# use the scheduled-mining workflow.
ARG INSTALL_MEMPALACE_TOOLKIT=true
ARG MEMPALACE_TOOLKIT_REF=main
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
git clone --depth 1 --branch "${MEMPALACE_TOOLKIT_REF}" \
https://gitea.jordbo.se/joakimp/mempalace-toolkit.git /opt/mempalace-toolkit && \
ln -sf /opt/mempalace-toolkit/bin/mempalace-session /usr/local/bin/mempalace-session && \
ln -sf /opt/mempalace-toolkit/bin/mempalace-docs /usr/local/bin/mempalace-docs && \
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs && \
mempalace-session --help >/dev/null && \
mempalace-docs --help >/dev/null && \
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
fi
# rustup — Rust toolchain manager
# Installs the rustup-init binary only. Users bootstrap Rust with:
# rustup-init -y && source ~/.cargo/env
# Toolchains persist via devbox-rustup and devbox-cargo volumes.
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \
chmod +x /usr/local/bin/rustup-init
# gitea-mcp — MCP server for Gitea API (official, Go binary, hosted on gitea.com)
ARG GITEA_MCP_VERSION=latest
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
V="${GITEA_MCP_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
fi && \
V="${V#v}" && \
[ -n "$V" ] && \
echo "Installing gitea-mcp ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \
| tar -xz -C /usr/local/bin/ gitea-mcp && \
chmod +x /usr/local/bin/gitea-mcp && \
gitea-mcp --version
# Set locale — generate common UTF-8 locales (override via LANG/LC_ALL env vars)
# To add more locales, run: sudo sed -i '/<locale>.UTF-8/s/^# //g' /etc/locale.gen && sudo locale-gen
RUN sed -i -E '/(en_US|en_GB|sv_SE|da_DK|nb_NO|fi_FI|de_DE|fr_FR|es_ES|it_IT|pt_BR|nl_NL|pl_PL|ja_JP|ko_KR|zh_CN)\.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US:en
ENV LC_ALL=en_US.UTF-8
ENV EDITOR=nvim
ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
# ── Node.js (required for opencode v1.x install + MCP servers) ──────
ARG NODE_VERSION=22
RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
rm -rf /var/lib/apt/lists/*
# ── Install opencode via npm ─────────────────────────────────────────
# v1.x is distributed as an npm package with platform-specific binaries.
# Disable with --build-arg INSTALL_OPENCODE=false to build a slimmer
# image without opencode (e.g. when only pi is needed). For a fully
# pi-only stripped image (no Bun, no opencode), see the pi-devbox repo.
ARG INSTALL_OPENCODE=true
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
npm install -g opencode-ai@${OPENCODE_VERSION} && \
opencode --version ; \
fi
# ── Optional: pi coding-agent ────────────────────────────────────────
# Installs pi as an alternative/complementary harness. Coexists with
# opencode in the same image — both share the mempalace install and
# palace path, so wing data is mutually visible to either harness.
#
# pi-toolkit (keybindings.json + pi-env.zsh + settings.example.json)
# and pi-extensions (confirm-destructive, ext-toggle, git-checkpoint,
# notify, ssh-controlmaster, todo, …) are cloned into /opt/ at build
# time. entrypoint-user.sh runs each repo's install.sh on container
# start so symlinks land under ~/.pi/agent/ on the named volume.
#
# Pi version is pinned by PI_VERSION (default: latest at build time).
# The baked pi binary lives at /usr/bin/pi (system npm prefix); the
# user-writable NPM_CONFIG_PREFIX (~/.pi/npm-global, set further down)
# is only consulted by `pi install npm:<pkg>` and `npm install -g` at
# runtime — it does NOT shadow the baked pi unless the user does
# `npm install -g @earendil-works/pi-coding-agent` themselves, in which
# case the user-installed copy on the volume wins via PATH order. Same
# contract as OPENCODE_VERSION otherwise: rebuild the image to upgrade
# the baked pi.
ARG INSTALL_PI=false
ARG PI_VERSION=latest
ARG PI_TOOLKIT_REF=main
ARG PI_EXTENSIONS_REF=main
RUN if [ "${INSTALL_PI}" = "true" ]; then \
if [ "${PI_VERSION}" = "latest" ]; then \
npm install -g @earendil-works/pi-coding-agent ; \
else \
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 && \
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
# ── AWS CLI v2 (for SSO/Bedrock authentication) ─────────────────────
RUN ARCH=$(case "${TARGETARCH}" in \
amd64) echo "x86_64" ;; \
arm64) echo "aarch64" ;; \
*) echo "x86_64" ;; \
esac) && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \
unzip -q /tmp/awscli.zip -d /tmp && \
/tmp/aws/install && \
rm -rf /tmp/aws /tmp/awscli.zip && \
aws --version
# ── Optional: Go ─────────────────────────────────────────────────────
# Latest stable Go is resolved from https://go.dev/dl/?mode=json when
# GO_VERSION=latest (default). Pass an explicit version like "1.26.2"
# to pin.
ARG INSTALL_GO=false
ARG GO_VERSION=latest
RUN if [ "${INSTALL_GO}" = "true" ]; then \
GOARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
V="${GO_VERSION}" && \
if [ "$V" = "latest" ]; then \
V=$(curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/?mode=json" | \
awk -F'"' '/"version":/ { sub(/^go/,"",$4); print $4; exit }'); \
fi && \
[ -n "$V" ] && \
echo "Installing Go ${V}" && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://go.dev/dl/go${V}.linux-${GOARCH}.tar.gz" | tar -C /usr/local -xz && \
ln -s /usr/local/go/bin/go /usr/local/bin/go && \
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
fi
# ── Optional: oh-my-opencode-slim (multi-agent orchestration) ────────
# Installs Bun runtime and the oh-my-opencode-slim npm package.
# Runtime activation is controlled by ENABLE_OMOS env var in entrypoint.
# Uses the baseline Bun build (SSE4.2 only) for compatibility with older
# CPUs that lack AVX2 (e.g. Sandy Bridge on OpenStack).
ARG INSTALL_OMOS=false
ARG OMOS_VERSION=latest
RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
ARCH=$(uname -m) && \
if [ "$ARCH" = "x86_64" ]; then \
BUN_ARCH="x64-baseline"; \
elif [ "$ARCH" = "aarch64" ]; then \
BUN_ARCH="aarch64"; \
fi && \
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-${BUN_ARCH}.zip" -o /tmp/bun.zip && \
unzip -o /tmp/bun.zip -d /tmp/bun && \
mv /tmp/bun/bun-linux-${BUN_ARCH}/bun /usr/local/bin/bun && \
chmod +x /usr/local/bin/bun && \
ln -sf bun /usr/local/bin/bunx && \
rm -rf /tmp/bun /tmp/bun.zip && \
bun --version && \
test -L /usr/local/bin/bunx && \
npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
fi
# ── Non-root user ────────────────────────────────────────────────────
ARG USER_NAME=developer
ARG USER_UID=1000
ARG USER_GID=1000
RUN groupadd --gid ${USER_GID} ${USER_NAME} && \
useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USER_NAME} && \
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${USER_NAME}
# Create standard directories
#
# ~/.pi/agent/extensions/ is created proactively so the named volume
# mount has a real owner from the first start. The directory is also
# what mempalace-toolkit's install_pi_extension probes to decide
# whether to deploy the pi↔mempalace bridge — must exist before that
# step runs in entrypoint-user.sh.
RUN mkdir -p /workspace \
/home/${USER_NAME}/.config/opencode/skills \
/home/${USER_NAME}/.pi/agent/extensions \
/home/${USER_NAME}/.agents/skills \
/home/${USER_NAME}/.local/share/opencode \
/home/${USER_NAME}/.cache/bash \
/home/${USER_NAME}/.ssh && \
chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}
# ── Pre-warm chromadb embedding model ──────────────────────────────
# Mempalace uses chromadb's ONNXMiniLM_L6_V2 embedding function, which
# downloads ~80 MB of all-MiniLM-L6-v2 ONNX weights from chromadb's CDN
# on first use. Without pre-warming this happens silently (output is
# suppressed by the entrypoint init step) and stalls first container
# start by minutes on a slow network. We bake the cache at build time
# under the developer user's home so the runtime first-start is fast.
#
# Cache path comes from chromadb's hardcoded `Path.home() / .cache /
# chroma / onnx_models / all-MiniLM-L6-v2`. Run as gosu developer so
# Path.home() resolves correctly and ownership is right from the start.
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
gosu ${USER_NAME} /opt/uv-tools/mempalace/bin/python -c "\
from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2; \
ef = ONNXMiniLM_L6_V2(); \
_ = ef(['warmup']); \
print('chromadb embedding model warmed: all-MiniLM-L6-v2')" && \
ls -lh /home/${USER_NAME}/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ ; \
fi
# ── User-writable npm global prefix on the devbox-pi-config volume ──
# By default npm's global prefix is /usr (writable only by root) so any
# `pi install npm:<pkg>` or `npm install -g <pkg>` invoked by the
# developer user would EACCES. Pointing the prefix into ~/.pi places
# user-installed packages on the named volume, which means they survive
# container recreation AND image rebuilds (complementing pi's auto-
# restore from settings.json with one less cold-start step).
#
# These ENVs land AFTER all build-time `npm install -g` calls
# (opencode, pi, oh-my-opencode-slim) so those still install to /usr at
# build time. They take effect for every runtime invocation regardless
# of shell init: docker compose run/exec, login shells, non-interactive
# commands. npm auto-creates the prefix directory on first install.
#
# Harmless when INSTALL_PI=false (and no named volume mounted at ~/.pi):
# the dir just lives on the container's writable layer.
ENV NPM_CONFIG_PREFIX=/home/${USER_NAME}/.pi/npm-global
ENV PATH="/home/${USER_NAME}/.pi/npm-global/bin:${PATH}"
# ── Shell defaults (bash history, aliases, readline) ─────────────────
# Shipped under /etc/skel-devbox/ rather than copied directly to the
# user's home. The entrypoint copies them to /home/developer/ only if
# the target file does not already exist, so host bind-mounts and
# previously-customized files are never overwritten. Users can restore
# the baked defaults anytime via:
# cp /etc/skel-devbox/.bash_aliases ~/.bash_aliases
# History itself persists via the devbox-shell-history named volume
# mounted at ~/.cache/bash (HISTFILE points there).
RUN mkdir -p /etc/skel-devbox
COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
# ── Entrypoint ────────────────────────────────────────────────────────
COPY rootfs/usr/local/lib/opencode-devbox/ /usr/local/lib/opencode-devbox/
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
/usr/local/lib/opencode-devbox/*.py
# Start as root — entrypoint adjusts UID/GID then drops to developer
WORKDIR /workspace
ENTRYPOINT ["entrypoint.sh"]
# Default to a login shell. `docker compose run --rm devbox` drops
# the user into bash to choose: `aws sso login`, then `opencode`
# or `pi`. To launch a harness directly, pass it explicitly:
# docker compose run --rm devbox opencode
# docker compose run --rm devbox pi
# `docker compose exec` bypasses the entrypoint and CMD entirely, so
# this default has no effect on attach-style workflows.
CMD ["bash", "-l"]
+7
View File
@@ -11,6 +11,13 @@
# changes (rootfs/, entrypoint*.sh). Version bumps to OPENCODE_VERSION,
# OMOS_VERSION, PI_VERSION, etc. do NOT trigger a base rebuild.
#
# To force a base rebuild for fresh apt packages without other code
# changes, bump the BASE_REBUILD_DATE comment below. The hash is
# content-addressed over this file, so any byte change invalidates the
# cache. Recommended cadence: once per release for security updates.
#
# BASE_REBUILD_DATE: 2026-05-14 (v1.14.50b — fresh apt + first promote-base-latest)
#
# See the project README's "Build pipeline" section for the rationale.
ARG DEBIAN_VERSION=trixie-slim
+14 -5
View File
@@ -32,7 +32,7 @@ ARG USER_NAME=developer
# ── Install opencode via npm ─────────────────────────────────────────
ARG INSTALL_OPENCODE=true
ARG OPENCODE_VERSION=1.14.44
ARG OPENCODE_VERSION=1.15.10
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
opencode --version ; \
@@ -47,16 +47,25 @@ 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
+22 -2
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
+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>.
+9 -3
View File
@@ -289,14 +289,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