v2.0.0: remove pi, relocate npm-global prefix, bump opencode 1.17.2->1.17.4
Validate / base-change-warning (push) Successful in 14s
Validate / docs-check (push) Successful in 13s
Publish Docker Image / resolve-versions (push) Successful in 8s
Publish Docker Image / base-decide (push) Successful in 13s
Validate / validate-omos (push) Successful in 12m42s
Validate / validate-base (push) Successful in 13m39s
Publish Docker Image / build-base (push) Successful in 44m17s
Publish Docker Image / smoke-base (push) Successful in 3m46s
Publish Docker Image / smoke-omos (push) Successful in 5m54s
Publish Docker Image / build-variant-base (push) Successful in 18m11s
Publish Docker Image / build-variant-omos (push) Successful in 19m34s
Publish Docker Image / promote-base-latest (push) Successful in 9s
Publish Docker Image / update-description (push) Successful in 15s

PR-5 (per docs/CLEANUP-v2.0.0.md). Major release with two breaking changes:

1. pi fully removed (deprecated in v1.17.2). Gone: INSTALL_PI + all PI_*
   build args; with-pi/omos-with-pi/pi-only variants; base-pi-only publish
   job; all ~/.pi entrypoint wiring; the 3 pi smoke/validate/build-variant
   CI jobs. Only base + omos variants remain (4 tags/release).

2. NPM_CONFIG_PREFIX relocated ~/.pi/npm-global -> ~/.config/opencode/npm-global
   (persistent in both compose files). entrypoint-user.sh gains a one-time
   migration shim that copies old global npm packages forward.

Also: opencode 1.17.2->1.17.4; DOCKER_HUB.md gains {{OPENCODE_VERSION}}
placeholder filled by CI at publish time (mirrors pi-devbox); full docs
drift sweep across README/AGENTS/.gitea-README/.env.example/manual-host-publish;
DOCKER_HUB.md regenerated + --check passes; both workflows YAML-valid;
all shell scripts pass bash -n.
This commit is contained in:
pi
2026-06-13 16:59:40 +02:00
parent c8217814c8
commit 72298ae77e
17 changed files with 334 additions and 1210 deletions
+47 -47
View File
@@ -8,14 +8,16 @@ the build pipeline is shaped the way it is, you're in the right place.
| File | Trigger | Role |
|---|---|---|
| [`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 five 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 five variants + `DOCKER_HUB.md` sync check. ~30 min. Fires on every push to `main`. |
| [`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 two 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 both variants + `DOCKER_HUB.md` sync check. ~30 min. Fires on every push to `main`. |
## Why the split-base pipeline exists
opencode-devbox builds **five image variants** (`base`, `omos`, `with-pi`, `omos-with-pi`, `pi-only`) × **two architectures** (amd64, arm64). Four opencode-bearing variants publish under this repo (**eight tags per release** + the floating `base-latest`); the `pi-only` build is pushed into the separate `joakimp/pi-devbox` repo as `base-pi-only` (so no opencode-less tag appears here). 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).
opencode-devbox builds **two image variants** (`base`, `omos`) × **two architectures** (amd64, arm64), publishing **four tags per release** + the floating `base-latest`. 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).
The five variants share ~95% of their layers (Debian + apt + Node + AWS CLI + mempalace + dev tools + entrypoints). The original `Dockerfile` was a single multi-stage build with `INSTALL_*` build-args gating variant-specific RUNs. BuildKit's per-layer cache key is content-addressed, but as soon as a build-arg-gated `RUN` produces a different layer hash for variant A vs variant B, every subsequent layer also has a different parent → identical commands re-execute per variant. Result: minimal cross-variant cache reuse on a fresh build.
> pi was removed in v2.0.0; it now builds in its own `joakimp/pi-devbox` repo. Before v2.0.0 a fifth `pi-only` build was produced here and pushed into that repo as `base-pi-only` — that coupling is gone.
The two variants share ~95% of their layers (Debian + apt + Node + AWS CLI + mempalace + dev tools + entrypoints). The original `Dockerfile` was a single multi-stage build with `INSTALL_*` build-args gating variant-specific RUNs. BuildKit's per-layer cache key is content-addressed, but as soon as a build-arg-gated `RUN` produces a different layer hash for variant A vs variant B, every subsequent layer also has a different parent → identical commands re-execute per variant. Result: minimal cross-variant cache reuse on a fresh build.
Two improvements were considered:
@@ -32,8 +34,8 @@ The split-base architecture is what the `docker-publish-split.yml` workflow exer
│ │ probe Docker Hub.
│ hash inputs: │ (resolve-versions
│ Dockerfile.base│ runs in parallel:
│ rootfs/ │ npm view pi/omos
│ entrypoint*.sh │ → concrete versions)
│ rootfs/ │ npm view omos
│ entrypoint*.sh │ → concrete version)
└────────┬─────────┘
┌─────────────┴─────────────┐
@@ -47,18 +49,18 @@ The split-base architecture is what the `docker-publish-split.yml` workflow exer
└────────┬─────────┘ to Docker Hub.
┌───────────────────────┼───────────────────────┐
▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│smoke-base│ │smoke-omos│ ... │smoke-omos-pi │ amd64 only,
└────┬─────┘ └────┬─────┘ └──────┬───────┘ parallel.
│ │
▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│build- │ │build- │ │build- │ multi-arch,
│variant- │ │variant- │ ... │variant- parallel,
│base │ │omos │ │omos-with-pi │ tag push.
└────┬─────┘ └────┬─────┘ └──────┬───────┘
└───────────────────────┴──────────────────────┘
▼ ▼
┌──────────┐ ┌──────────┐
│smoke-base│ │smoke-omos│ amd64 only,
└────┬─────┘ └────┬─────┘ parallel.
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│build- │ │build- │ multi-arch,
│variant- │ │variant- │ parallel,
│base │ │omos │ tag push.
└────┬─────┘ └────┬─────┘
└──────────────────────┘
┌──────────────────────────┐
@@ -111,13 +113,12 @@ dependency between them) and resolves the floating npm packages whose
`*_VERSION` build-args default to `latest`:
```sh
PI_VERSION=$(npm view @earendil-works/pi-coding-agent version)
OMOS_VERSION=$(npm view oh-my-opencode-slim version)
```
The outputs (`pi_version`, `omos_version`) are consumed by every variant
smoke and build job that installs pi or omos. **Why this exists:** without
it, the `npm install -g` RUN layer in `Dockerfile.variant` hashes
The output (`omos_version`) is consumed by the omos variant smoke and
build jobs. **Why this exists:** without it, the `npm install -g` RUN
layer in `Dockerfile.variant` hashes
identically across builds (same ARG default, same command string), so
the registry buildcache silently reuses the layer from whatever upstream
version was current when the cache was first populated. This is the
@@ -125,9 +126,9 @@ cache-hit silent-regression class of bug that shipped pi-devbox v0.74.0
through v0.75.5 with identical image bytes (fixed in pi-devbox v0.75.5b
2026-05-23). Currently masked here by `OPENCODE_VERSION` bumping every
release (parent-chain cache-key invalidation), but masking would fail on
a `vN.N.Nb` opencode-version-unchanged release that only bumps pi or
omos. Smoke jobs additionally assert `EXPECTED_PI_VERSION` /
`EXPECTED_OMOS_VERSION` against the resolved values.
a `vN.N.Nb` opencode-version-unchanged release that only bumps omos.
Smoke jobs additionally assert `EXPECTED_OMOS_VERSION` against the
resolved value.
### Step 2: `build-base` (conditional)
@@ -139,23 +140,21 @@ when only one or two layers changed.
The base image is **not** tagged `base-latest` here — that promotion
happens at the very end after all variants succeed (see step 5).
### Step 3: `smoke-*` (×4, parallel)
### Step 3: `smoke-*` (×2, parallel)
For each variant: build amd64-only against the base tag, load into
local docker, run [`scripts/smoke-test.sh`](../scripts/smoke-test.sh).
Variant build-args:
| variant | INSTALL_OPENCODE | INSTALL_OMOS | INSTALL_PI |
|---|---|---|---|
| `base` | true | false | false |
| `omos` | true | true | false |
| `with-pi` | true | false | true |
| `omos-with-pi` | true | true | true |
| variant | INSTALL_OPENCODE | INSTALL_OMOS |
|---|---|---|
| `base` | true | false |
| `omos` | true | true |
Smoke runs `--variant <name>` to enable variant-specific assertions.
Gate the publish: a smoke failure for variant X blocks `build-variant-X`.
### Step 4: `build-variant-*` (×4, parallel)
### Step 4: `build-variant-*` (×2, parallel)
For each variant that passed smoke: multi-arch (amd64 + arm64) build of
`Dockerfile.variant`, pushed to Docker Hub with the user-facing release
@@ -165,8 +164,6 @@ tags:
|---|---|
| `build-variant-base` | `vX.Y.Z`, `latest` |
| `build-variant-omos` | `vX.Y.Z-omos`, `latest-omos` |
| `build-variant-with-pi` | `vX.Y.Z-with-pi`, `latest-with-pi` |
| `build-variant-omos-with-pi` | `vX.Y.Z-omos-with-pi`, `latest-omos-with-pi` |
The `latest*` aliases are only updated when `promote_latest=true` (the
manual dispatch input) — for test runs, `promote_latest=false` keeps the
@@ -174,7 +171,7 @@ production aliases pointing at the previous good release.
### Step 5: `promote-base-latest`
Once all five variants successfully publish, re-tag `base-<hash>` as
Once both variants successfully publish, re-tag `base-<hash>` as
`base-latest` using `crane copy`. This is a **manifest-level re-tag, not
a rebuild** — it touches only Docker Hub's image index, takes seconds,
and is atomic.
@@ -182,7 +179,7 @@ and is atomic.
The reason this happens *after* variants succeed (rather than alongside
`build-base`) is so a partial failure leaves `base-latest` pointing at
the previous known-good base. External consumers who pin to
`base-latest` (e.g. the planned pi-devbox repo) never see a broken base.
`base-latest` never see a broken base.
### Step 6: `update-description`
@@ -194,18 +191,21 @@ field via the Hub REST API. Same step as the production pipeline.
The base sets
```
ENV NPM_CONFIG_PREFIX=/home/developer/.pi/npm-global
ENV NPM_CONFIG_PREFIX=/home/developer/.config/opencode/npm-global
```
This is intentional — it makes `pi install npm:<pkg>` and `npm install -g`
land on the `devbox-pi-config` named volume at runtime, so user-installed
packages survive container recreate AND image rebuild.
This is intentional — it makes `npm install -g` land on the
`devbox-opencode-config` named volume at runtime, so user-installed
packages survive container recreate AND image rebuild. (Before v2.0.0
this prefix lived at `~/.pi/npm-global` on the now-removed
`devbox-pi-config` volume; `entrypoint-user.sh` migrates the old path
once.)
But the *variant build* inherits this prefix at build time. If left as-is,
`npm install -g opencode-ai@$VERSION` in `Dockerfile.variant` would
install opencode into `/home/developer/.pi/npm-global/...`, which is then
**shadowed by the volume mount at runtime** → opencode disappears from
PATH on first start.
install opencode into `/home/developer/.config/opencode/npm-global/...`,
which is then **shadowed by the volume mount at runtime** → opencode
disappears from PATH on first start.
Fix: each `npm install -g` in `Dockerfile.variant` overrides the prefix
per-RUN:
@@ -216,7 +216,7 @@ RUN NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION}
Baked binaries land on `/usr/bin/...` (system prefix), survive the volume
mount. Runtime-installed user packages still land on
`~/.pi/npm-global/...`. Both visible on PATH.
`~/.config/opencode/npm-global/...`. Both visible on PATH.
## Cache strategy
@@ -238,7 +238,7 @@ matters more.
| Scenario | Production pipeline | Split-base pipeline |
|---|---|---|
| Version-bump-only release (only opencode/pi/omos version changed) | ~165180 min | **~3040 min** (base cache hit) |
| Version-bump-only release (only opencode/omos version changed) | ~165180 min | **~3040 min** (base cache hit) |
| Base-touching release (apt/Node/Debian/entrypoint change) | ~165180 min | **~7090 min** (base rebuilds) |
The split-base pipeline pays its dues on base-touching releases (which are
@@ -252,7 +252,7 @@ on every push to `main` and on PRs. It:
1. Runs `scripts/generate-dockerhub-md.py --check` to enforce
`DOCKER_HUB.md` is in sync with `HUB_TEMPLATE`.
2. Builds each of the five variants amd64-only (no multi-arch, no push)
2. Builds each of the two variants amd64-only (no multi-arch, no push)
and runs `scripts/smoke-test.sh`.
This catches regressions before they reach a tag push. Wall clock ~30 min.