feat: add pi-only variant (pi without opencode) as basis for pi-devbox
Validate / docs-check (push) Successful in 10s
Validate / base-change-warning (push) Successful in 23s
Validate / validate-omos (push) Successful in 4m36s
Validate / validate-omos-with-pi (push) Failing after 5m40s
Validate / validate-with-pi (push) Failing after 7m35s
Validate / validate-pi-only (push) Failing after 3m45s
Validate / validate-base (push) Failing after 16m12s
Validate / docs-check (push) Successful in 10s
Validate / base-change-warning (push) Successful in 23s
Validate / validate-omos (push) Successful in 4m36s
Validate / validate-omos-with-pi (push) Failing after 5m40s
Validate / validate-with-pi (push) Failing after 7m35s
Validate / validate-pi-only (push) Failing after 3m45s
Validate / validate-base (push) Failing after 16m12s
All opencode-devbox variants set INSTALL_OPENCODE=true, so pointing pi-devbox at with-pi dragged opencode along and made it ~a re-tag of latest-with-pi. Add a 5th variant pi-only (INSTALL_OPENCODE=false, INSTALL_PI=true): pi + companions (toolkit, extensions, fork, recall) + base tooling, no opencode (~145 MB lighter than with-pi). - Dockerfile.variant: document pi-only in the variant table. - CI docker-publish-split.yml: new smoke-pi-only + build-variant-pi-only jobs (tags :VERSION-pi-only / :latest-pi-only, multi-arch); wired into promote-base-latest and update-description needs. - validate.yml: new validate-pi-only main-branch gate job. - smoke-test.sh: accept --variant pi-only; threshold 2750 MB; opencode-absent path already handled. - Docs: HUB_TEMPLATE (regenerated DOCKER_HUB.md), README, AGENTS (variant/tag counts 4->5, 8->10 tags), .gitea/README, manual-host-publish.sh (5 variants), plan doc implementation note. This is the single source of truth for joakimp/pi-devbox, which now FROMs latest-pi-only. Versions unchanged (opencode 1.15.13, pi 0.78.0).
This commit is contained in:
+5
-5
@@ -8,14 +8,14 @@ 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 four parallel variant deltas. ~40–80 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`. |
|
||||
| [`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. ~40–80 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`. |
|
||||
|
||||
## 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 (~3–5x slower than native).
|
||||
|
||||
The four 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.
|
||||
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.
|
||||
|
||||
Two improvements were considered:
|
||||
|
||||
@@ -174,7 +174,7 @@ production aliases pointing at the previous good release.
|
||||
|
||||
### Step 5: `promote-base-latest`
|
||||
|
||||
Once all four variants successfully publish, re-tag `base-<hash>` as
|
||||
Once all five 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.
|
||||
@@ -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 four variants amd64-only (no multi-arch, no push)
|
||||
2. Builds each of the five 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.
|
||||
|
||||
@@ -432,6 +432,53 @@ jobs:
|
||||
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos-with-pi --variant omos-with-pi
|
||||
|
||||
smoke-pi-only:
|
||||
needs: [base-decide, build-base, resolve-versions]
|
||||
if: |
|
||||
always() &&
|
||||
needs.base-decide.result == 'success' &&
|
||||
needs.resolve-versions.result == 'success' &&
|
||||
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||
- run: |
|
||||
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
|
||||
docker system prune -af --volumes || true
|
||||
docker builder prune -af || true
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
with: {driver-opts: network=host}
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.variant
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
tags: opencode-devbox:smoke-pi-only
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||
INSTALL_OPENCODE=false
|
||||
INSTALL_OMOS=false
|
||||
INSTALL_PI=true
|
||||
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
||||
PI_FORK_REF=${{ needs.resolve-versions.outputs.fork_ref }}
|
||||
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
- env:
|
||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-pi-only --variant pi-only
|
||||
|
||||
# ── Phase 4: multi-arch publish per variant ────────────────────────
|
||||
|
||||
build-variant-base:
|
||||
@@ -727,6 +774,81 @@ jobs:
|
||||
echo "==> All 3 build+push attempts failed"
|
||||
exit 1
|
||||
|
||||
build-variant-pi-only:
|
||||
needs: [base-decide, smoke-pi-only, resolve-versions]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||
- run: |
|
||||
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
|
||||
docker system prune -af --volumes || true
|
||||
docker builder prune -af || true
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
with: {platforms: arm64}
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
with: {driver-opts: network=host}
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Compute version-specific tags
|
||||
id: tags
|
||||
run: |
|
||||
VERSION="${{ env.RELEASE_TAG }}"
|
||||
{ echo "tags<<EOF"
|
||||
echo "${IMAGE}:${VERSION}-pi-only"
|
||||
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||
echo "${IMAGE}:latest-pi-only"
|
||||
fi
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Build and push variant (with retry)
|
||||
shell: bash
|
||||
env:
|
||||
TAGS: ${{ steps.tags.outputs.tags }}
|
||||
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||
FORK_REF: ${{ needs.resolve-versions.outputs.fork_ref }}
|
||||
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG_FLAGS=()
|
||||
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
||||
# 3-attempt retry (see build-base step for rationale). Variant: pi-only.
|
||||
for attempt in 1 2 3; do
|
||||
echo "==> Build+push attempt ${attempt}/3"
|
||||
if docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--file Dockerfile.variant \
|
||||
--push \
|
||||
--build-arg "BASE_IMAGE=${BASE_IMAGE_FULL}" \
|
||||
--build-arg "INSTALL_OPENCODE=false" \
|
||||
--build-arg "INSTALL_OMOS=false" \
|
||||
--build-arg "INSTALL_PI=true" \
|
||||
--build-arg "PI_VERSION=${PI_VERSION}" \
|
||||
--build-arg "PI_FORK_REF=${FORK_REF}" \
|
||||
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||
"${TAG_FLAGS[@]}" \
|
||||
.; then
|
||||
echo "==> Attempt ${attempt} succeeded"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${attempt}" -lt 3 ]]; then
|
||||
backoff=$(( attempt * 15 ))
|
||||
echo "==> Attempt ${attempt} failed, sleeping ${backoff}s before retry"
|
||||
sleep "${backoff}"
|
||||
fi
|
||||
done
|
||||
echo "==> All 3 build+push attempts failed"
|
||||
exit 1
|
||||
|
||||
# ── Phase 5: promote base-<hash> → base-latest (manifest copy only) ─
|
||||
promote-base-latest:
|
||||
needs:
|
||||
@@ -735,6 +857,7 @@ jobs:
|
||||
- build-variant-omos
|
||||
- build-variant-with-pi
|
||||
- build-variant-omos-with-pi
|
||||
- build-variant-pi-only
|
||||
# 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.
|
||||
@@ -787,6 +910,7 @@ jobs:
|
||||
- build-variant-omos
|
||||
- build-variant-with-pi
|
||||
- build-variant-omos-with-pi
|
||||
- build-variant-pi-only
|
||||
# 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.
|
||||
|
||||
@@ -312,3 +312,62 @@ jobs:
|
||||
- name: Smoke test
|
||||
run: |
|
||||
bash scripts/smoke-test.sh opencode-devbox:ci-omos-with-pi --variant omos-with-pi
|
||||
|
||||
validate-pi-only:
|
||||
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 pi-only image (amd64, load to local daemon)
|
||||
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_OPENCODE=false
|
||||
INSTALL_PI=true
|
||||
tags: opencode-devbox:ci-pi-only
|
||||
|
||||
- name: Smoke test
|
||||
run: |
|
||||
bash scripts/smoke-test.sh opencode-devbox:ci-pi-only --variant pi-only
|
||||
|
||||
Reference in New Issue
Block a user