2 Commits

Author SHA1 Message Date
pi ed49b8d97a fix(ci): resolve-versions needs shell: bash for 'set -o pipefail'
Publish Docker Image / smoke (push) Successful in 9m0s
Publish Docker Image / build-variant-studio (push) Successful in 17m41s
Publish Docker Image / build-variant (push) Successful in 19m1s
Publish Docker Image / update-description (push) Successful in 7s
Publish Docker Image / promote-base-latest (push) Successful in 10s
Publish Docker Image / resolve-versions (push) Successful in 6s
Publish Docker Image / base-decide (push) Successful in 11s
Publish Docker Image / build-base (push) Successful in 45m54s
Publish Docker Image / smoke-studio (push) Successful in 3m43s
The default run shell is 'sh -e {0}' (dash on the act runner), which
rejects 'set -o pipefail' ('Illegal option -o pipefail') — failing the
resolve-versions job on line 2 and cascading every dependent job to
skipped (v1.1.6 run 401). The heavy build steps already declare
'shell: bash'; the resolve step did not. Added it.
2026-06-19 18:26:04 +02:00
pi 9eff3f3c48 release: v1.1.6 — build provenance + reproducibility hardening; pi 0.79.7 → 0.79.8
Publish Docker Image / resolve-versions (push) Failing after 52s
Publish Docker Image / base-decide (push) Has been skipped
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke-studio (push) Has been skipped
Publish Docker Image / build-variant (push) Has been skipped
Publish Docker Image / build-variant-studio (push) Has been skipped
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / smoke (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
Adds OCI labels + /etc/pi-devbox/build-manifest.json so a published tag is
self-describing and reconstructable after CI logs rotate (manifest is
written from the actual checked-out HEAD of each /opt clone + live
pi --version, not just the intended build-args).

Hardens the build plumbing:
- scripts/check-base-hash.sh guards the base-rebuild invariant: every
  floating ARG *_REF in Dockerfile.base must be folded into the base_tag
  hash, else a ref-only change silently fails to rebuild the base
  (v1.1.2-class staleness footgun). Runs in base-decide and locally.
- resolve-versions now fails loud instead of falling back to a floating
  main/master on a transient API failure — validates each ref is a 40-hex
  SHA (and pi a real semver) and aborts the release otherwise.
- The three gitea companions (pi-toolkit, pi-extensions, mempalace-toolkit)
  gained overridable *_REPO build-args (defaulting to the canonical gitea
  origin) so a relocated/forked build can repoint them without editing the
  Dockerfiles — matching the existing PI_FORK_REPO/PI_OBSMEM_REPO pattern.

README documents the forked/relocated build-arg trick and how to read the
labels + manifest. smoke-test asserts the manifest + labels. pi bumps
0.79.7 → 0.79.8 (auto-resolved at build).
2026-06-19 18:23:11 +02:00
7 changed files with 336 additions and 36 deletions
+73 -33
View File
@@ -58,6 +58,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Guard — base *_REF args must be folded into the base hash
run: bash scripts/check-base-hash.sh
- name: Compute base tag from Dockerfile.base + dependencies
id: compute
run: |
@@ -126,53 +129,72 @@ jobs:
steps:
- name: Resolve pi version + companion refs
id: resolve
shell: bash
run: |
set -eu
# Query npm registry directly; catthehacker/ubuntu:act-latest's npm
# is not reliably on PATH in act_runner job containers.
PI_VERSION=$(curl -sf "https://registry.npmjs.org/@earendil-works%2Fpi-coding-agent/latest" | jq -r '.version')
set -euo pipefail
AUTH_HEADER="Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}"
# Fail loud rather than silently shipping a floating branch. A
# transient network/API failure must ABORT the release, not bake
# an unpinned ref that defeats both cache-busting AND after-the-
# fact reproducibility. (Previously each lookup fell back to
# `main`/`master` via `|| echo`.)
require_sha() { # $1=label $2=value
if ! printf '%s' "${2:-}" | grep -qiE '^[0-9a-f]{40}$'; then
echo "::error::Could not resolve $1 to a commit SHA (got '${2:-<empty>}'). Refusing to fall back to a floating ref — published images must stay reproducible. Check connectivity and GITEA_BUILD_TOKEN/GITHUB_TOKEN."
exit 1
fi
}
# pi version from npm (catthehacker/ubuntu:act-latest's npm is not
# reliably on PATH in act_runner job containers, so query directly).
PI_VERSION=$(curl -sf "https://registry.npmjs.org/@earendil-works%2Fpi-coding-agent/latest" | jq -r '.version' 2>/dev/null || true)
if ! printf '%s' "${PI_VERSION:-}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
echo "::error::Could not resolve pi version from npm (got '${PI_VERSION:-<empty>}')."
exit 1
fi
echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT"
# Resolve pi-fork / pi-observational-memory git refs to commit
# SHAs so the build-arg string changes whenever upstream moves.
# pi-fork / pi-observational-memory (GitHub) → commit SHAs.
FORK_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || echo "master")
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || true)
require_sha PI_FORK_REF "$FORK_REF"
OBSMEM_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || echo "master")
[ -n "$FORK_REF" ] || FORK_REF=master
[ -n "$OBSMEM_REF" ] || OBSMEM_REF=master
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || true)
require_sha PI_OBSMEM_REF "$OBSMEM_REF"
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
echo "obsmem_ref=${OBSMEM_REF}" >> "$GITHUB_OUTPUT"
# Also resolve pi-toolkit / pi-extensions main HEADs to SHAs so a
# workflow_dispatch re-run produces byte-identical images when
# those repos haven't moved (and a clean diff in build-arg strings
# when they have, defeating the registry buildcache footgun).
# Gitea API requires auth even for public-repo commit listing.
TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. Gitea API
# requires auth even for public-repo commit listing.
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
| jq -r '.[0].sha // "main"' 2>/dev/null || echo "main")
EXTENSIONS_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
| jq -r '.[0].sha // empty' 2>/dev/null || true)
require_sha PI_TOOLKIT_REF "$TOOLKIT_REF"
EXTENSIONS_REF=$(curl -sf -H "$AUTH_HEADER" \
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
| jq -r '.[0].sha // "main"' 2>/dev/null || echo "main")
[ -n "$TOOLKIT_REF" ] || TOOLKIT_REF=main
[ -n "$EXTENSIONS_REF" ] || EXTENSIONS_REF=main
| jq -r '.[0].sha // empty' 2>/dev/null || true)
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
# Resolve mempalace-toolkit main HEAD to a SHA. UNLIKE the others,
# mempalace-toolkit is cloned in Dockerfile.base, so this SHA is
# ALSO folded into the base-decide hash to force a base rebuild
# when the toolkit moves (without it, a toolkit-only fix silently
# fails to land unless Dockerfile.base itself changes).
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
# mempalace-toolkit (Gitea) → commit SHA. UNLIKE the others this
# is cloned in Dockerfile.base, so the SAME SHA is ALSO folded
# into the base-decide hash (see that job) to force a base rebuild
# when the toolkit moves — otherwise a toolkit-only fix silently
# fails to land unless Dockerfile.base itself changes.
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
| jq -r '.[0].sha // "main"' 2>/dev/null || echo "main")
[ -n "$MEMPALACE_TOOLKIT_REF" ] || MEMPALACE_TOOLKIT_REF=main
| jq -r '.[0].sha // empty' 2>/dev/null || true)
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
# Resolve pi-studio (omaclaren/pi-studio) main HEAD to a SHA for
# the :latest-studio variant — same cache-busting rationale.
# pi-studio (omaclaren/pi-studio) → commit SHA for :latest-studio.
STUDIO_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || echo "main")
[ -n "$STUDIO_REF" ] || STUDIO_REF=main
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || true)
require_sha PI_STUDIO_REF "$STUDIO_REF"
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
echo "Resolved PI_VERSION=${PI_VERSION}"
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
echo "Resolved PI_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
@@ -299,6 +321,9 @@ jobs:
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
MEMPALACE_TOOLKIT_REF=${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
RELEASE_TAG=smoke
SOURCE_REVISION=${{ github.sha }}
- name: Smoke test (amd64)
env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
@@ -355,6 +380,9 @@ jobs:
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
INSTALL_STUDIO=true
PI_STUDIO_REF=${{ needs.resolve-versions.outputs.studio_ref }}
MEMPALACE_TOOLKIT_REF=${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
RELEASE_TAG=smoke-studio
SOURCE_REVISION=${{ github.sha }}
- name: Smoke test studio (amd64)
env:
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
@@ -406,10 +434,12 @@ jobs:
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
run: |
set -euo pipefail
TAG_FLAGS=()
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# 3-attempt retry (see build-base step for rationale).
for attempt in 1 2 3; do
echo "==> Build+push attempt ${attempt}/3"
@@ -423,6 +453,10 @@ jobs:
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
--build-arg "BUILD_DATE=${BUILD_DATE}" \
--build-arg "SOURCE_REVISION=${GITHUB_SHA:-}" \
"${TAG_FLAGS[@]}" \
.; then
echo "==> Attempt ${attempt} succeeded"
@@ -487,10 +521,12 @@ jobs:
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
run: |
set -euo pipefail
TAG_FLAGS=()
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# 3-attempt retry (see build-base step for rationale).
for attempt in 1 2 3; do
echo "==> Build+push attempt ${attempt}/3"
@@ -504,8 +540,12 @@ jobs:
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
--build-arg "INSTALL_STUDIO=true" \
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
--build-arg "BUILD_DATE=${BUILD_DATE}" \
--build-arg "SOURCE_REVISION=${GITHUB_SHA:-}" \
"${TAG_FLAGS[@]}" \
.; then
echo "==> Attempt ${attempt} succeeded"
+66
View File
@@ -13,6 +13,72 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
## Unreleased
## v1.1.6 — 2026-06-19
Build provenance + reproducibility hardening, plus pi `0.79.7``0.79.8`
(auto-resolved at build). Companion refs are auto-resolved to SHAs at build
as before.
### Bumped: pi 0.79.7 → 0.79.8
Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.8)):
- **Selective provider base entry points** — SDK users can pair
`@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` with
explicit provider registration to keep bundled apps from including unused
provider transports.
- **Mistral prompt caching** — Mistral sessions use provider-side prompt
caching keyed on the pi session ID, with cached-token usage/cost
accounting.
- **Post-compaction token estimates** — compact results and compaction
events now include estimated post-compaction token counts.
- **OpenRouter Fusion alias** — `openrouter/fusion` available as a built-in
OpenRouter model alias.
### Added
- **Self-describing images: OCI labels + on-disk build manifest.** The
variant build now records exactly which pi version and companion-repo
commits were baked into each image. Previously the SHAs resolved by CI
only ever reached the build log (which rotates), so a published tag was
not reconstructable after the fact — confirming what shipped meant
triangulating from `git`, `pi --version`, and extension source.
- OCI labels: `org.opencontainers.image.{version,revision,created}` plus
`se.jordbo.pi-devbox.{pi,pi-toolkit,pi-extensions,pi-fork,pi-obsmem,mempalace-toolkit,pi-studio}-*ref`
inspect with `docker inspect`.
- `/etc/pi-devbox/build-manifest.json` written from **ground truth** (the
actual checked-out `HEAD` of each `/opt` clone + live `pi --version`),
not just the intended build-args, so it also exposes a clone that
silently resolved to the wrong ref. The provenance ARGs are declared
last so a changing `BUILD_DATE` never invalidates the expensive
install/clone layers.
- **`scripts/check-base-hash.sh` — base-rebuild invariant guard.** Every
floating `ARG *_REF` consumed by `Dockerfile.base` must be folded into the
`base_tag` hash, or a ref-only change won't trigger a base rebuild (the
v1.1.2 mempalace-toolkit staleness footgun). The guard fails CI the moment
someone adds an `ARG *_REF` to `Dockerfile.base` without folding it in; it
runs in the `base-decide` job and locally. Smoke-test gained assertions for
the manifest (present, no `"unknown"` components) and the OCI labels.
- **Overridable companion repo URLs.** The three gitea-hosted companions
(`pi-toolkit`, `pi-extensions`, `mempalace-toolkit`) gained `*_REPO`
build-args defaulting to their canonical `gitea.jordbo.se` origin —
matching the existing `PI_FORK_REPO` / `PI_OBSMEM_REPO` / `PI_STUDIO_REPO`
pattern. A relocated or forked build can now repoint a companion at a
mirror, another host, or a local path (`--build-arg PI_EXTENSIONS_REPO=...`)
without editing the Dockerfiles. Defaults are unchanged, so the canonical
CI build is byte-identical.
### Changed
- **`resolve-versions` now fails loud instead of falling back to a floating
branch.** Each pi-version / companion-ref lookup previously degraded to
`main`/`master` on a transient API/network failure (`|| echo "main"`),
silently shipping an unpinned ref that defeats both cache-busting and
reproducibility. Resolution now validates each result is a 40-hex commit
SHA (and pi a real semver) and aborts the release otherwise.
---
## v1.1.5 — 2026-06-18
Patch release: SSH ControlMaster read-only-socket fix + pi `0.79.6``0.79.7`
+6 -1
View File
@@ -351,6 +351,11 @@ RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
ARG INSTALL_MEMPALACE_TOOLKIT=true
ARG MEMPALACE_TOOLKIT_REF=main
# MEMPALACE_TOOLKIT_REPO defaults to the canonical gitea origin but is
# overridable so a relocated/forked build can clone from a mirror or a
# different host without editing this Dockerfile (mirrors the
# PI_FORK_REPO / PI_OBSMEM_REPO / PI_STUDIO_REPO pattern in the variant).
ARG MEMPALACE_TOOLKIT_REPO=https://gitea.jordbo.se/joakimp/mempalace-toolkit.git
# MEMPALACE_TOOLKIT_REF accepts EITHER a branch name OR a commit SHA. CI
# resolves it to a SHA (resolve-versions job) and folds that SHA into the
# base-decide hash so the base rebuilds when the toolkit moves. `git clone
@@ -360,7 +365,7 @@ ARG MEMPALACE_TOOLKIT_REF=main
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
rm -rf /opt/mempalace-toolkit && mkdir -p /opt/mempalace-toolkit && \
git -C /opt/mempalace-toolkit init -q && \
git -C /opt/mempalace-toolkit remote add origin https://gitea.jordbo.se/joakimp/mempalace-toolkit.git && \
git -C /opt/mempalace-toolkit remote add origin "${MEMPALACE_TOOLKIT_REPO}" && \
ok=0; for i in 1 2 3 4 5; do \
if git -C /opt/mempalace-toolkit fetch --depth 1 origin "${MEMPALACE_TOOLKIT_REF}" && \
git -C /opt/mempalace-toolkit checkout -q FETCH_HEAD; then ok=1; break; fi; \
+64 -2
View File
@@ -41,6 +41,12 @@ ARG USER_NAME=developer
ARG PI_VERSION=latest
ARG PI_TOOLKIT_REF=main
ARG PI_EXTENSIONS_REF=main
# Repo URLs default to the canonical gitea origin but are overridable so a
# relocated/forked build can clone from a mirror or a different host
# without editing this Dockerfile — same pattern as PI_FORK_REPO /
# PI_OBSMEM_REPO / PI_STUDIO_REPO below.
ARG PI_TOOLKIT_REPO=https://gitea.jordbo.se/joakimp/pi-toolkit.git
ARG PI_EXTENSIONS_REPO=https://gitea.jordbo.se/joakimp/pi-extensions.git
# pi-fork (fork tool) + pi-observational-memory (recall tool) live on GitHub
# under elpapi42. CI resolves these to commit SHAs to defeat the same
# cache-hit footgun that affects PI_VERSION.
@@ -77,8 +83,8 @@ RUN set -e && \
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \
fi && \
pi --version && \
git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-toolkit.git" "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-extensions.git" "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
git_fetch_ref "${PI_TOOLKIT_REPO}" "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
git_fetch_ref "${PI_EXTENSIONS_REPO}" "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
git_fetch_ref "${PI_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \
git_fetch_ref "${PI_OBSMEM_REPO}" "${PI_OBSMEM_REF}" /opt/pi-observational-memory && \
(cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \
@@ -154,4 +160,60 @@ RUN if [ "${INSTALL_GO}" = "true" ]; then \
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
fi
# ── Build provenance: OCI labels + on-disk build manifest ────────────
# Records exactly which pi version and companion-repo commits were baked
# into THIS image, so a published tag is self-describing and reproducible
# after the fact (CI logs rotate; a released image must not depend on
# them). Previously the resolved SHAs only ever reached the CI build log.
#
# These ARGs are declared LAST, immediately before the layer that uses
# them, so a changing BUILD_DATE / RELEASE_TAG / SOURCE_REVISION never
# invalidates the expensive pi-install / clone layers above.
ARG RELEASE_TAG=dev
ARG BUILD_DATE=
ARG SOURCE_REVISION=
# MEMPALACE_TOOLKIT_REF is consumed in Dockerfile.base; re-declared here
# only so its intended ref lands in the label set alongside the others.
ARG MEMPALACE_TOOLKIT_REF=main
LABEL org.opencontainers.image.version="${RELEASE_TAG}" \
org.opencontainers.image.revision="${SOURCE_REVISION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
se.jordbo.pi-devbox.pi-version="${PI_VERSION}" \
se.jordbo.pi-devbox.pi-toolkit-ref="${PI_TOOLKIT_REF}" \
se.jordbo.pi-devbox.pi-extensions-ref="${PI_EXTENSIONS_REF}" \
se.jordbo.pi-devbox.pi-fork-ref="${PI_FORK_REF}" \
se.jordbo.pi-devbox.pi-obsmem-ref="${PI_OBSMEM_REF}" \
se.jordbo.pi-devbox.mempalace-toolkit-ref="${MEMPALACE_TOOLKIT_REF}" \
se.jordbo.pi-devbox.pi-studio-ref="${PI_STUDIO_REF}"
# The manifest is written from GROUND TRUTH — the actual checked-out HEAD
# of each /opt clone and the live `pi --version` — not merely the intended
# build-args. That way it also exposes a clone that silently resolved to
# something other than the requested ref. pi-studio is present only in the
# studio variant (JSON null otherwise).
RUN set -e; \
mkdir -p /etc/pi-devbox; \
rev() { git -C "$1" rev-parse HEAD 2>/dev/null || echo "unknown"; }; \
PI_V="$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')"; \
STUDIO_REV='null'; \
if [ -d /opt/pi-studio/.git ]; then STUDIO_REV="\"$(rev /opt/pi-studio)\""; fi; \
{ \
echo '{'; \
echo " \"release_tag\": \"${RELEASE_TAG}\","; \
echo " \"build_date\": \"${BUILD_DATE}\","; \
echo " \"source_revision\": \"${SOURCE_REVISION}\","; \
echo " \"pi_version\": \"${PI_V}\","; \
echo " \"components\": {"; \
echo " \"pi-toolkit\": \"$(rev /opt/pi-toolkit)\","; \
echo " \"pi-extensions\": \"$(rev /opt/pi-extensions)\","; \
echo " \"pi-fork\": \"$(rev /opt/pi-fork)\","; \
echo " \"pi-observational-memory\": \"$(rev /opt/pi-observational-memory)\","; \
echo " \"mempalace-toolkit\": \"$(rev /opt/mempalace-toolkit)\","; \
echo " \"pi-studio\": ${STUDIO_REV}"; \
echo " }"; \
echo '}'; \
} > /etc/pi-devbox/build-manifest.json; \
echo "── build manifest ──"; cat /etc/pi-devbox/build-manifest.json
# WORKDIR / ENTRYPOINT / CMD inherited from base.
+62
View File
@@ -537,6 +537,68 @@ pi-coding-agent@latest` (the build-arg string would otherwise be
byte-identical across releases and the layer would silently reuse the
previous version's bytes).
### Building a fork / relocated build
The canonical build clones its companions from `gitea.jordbo.se`. Every
companion repo URL is an overridable build-arg (defaulting to the canonical
origin), so a fork or a build on a host that can't reach that gitea can
repoint each one at a mirror, another host, or a local `file://` path
**without editing the Dockerfiles**:
| Build-arg | Default | Dockerfile |
|---|---|---|
| `PI_TOOLKIT_REPO` | `https://gitea.jordbo.se/joakimp/pi-toolkit.git` | variant |
| `PI_EXTENSIONS_REPO` | `https://gitea.jordbo.se/joakimp/pi-extensions.git` | variant |
| `MEMPALACE_TOOLKIT_REPO` | `https://gitea.jordbo.se/joakimp/mempalace-toolkit.git` | base |
| `PI_FORK_REPO` | `https://github.com/elpapi42/pi-fork.git` | variant |
| `PI_OBSMEM_REPO` | `https://github.com/elpapi42/pi-observational-memory.git` | variant |
| `PI_STUDIO_REPO` | `https://github.com/omaclaren/pi-studio.git` | variant |
Each has a matching `*_REF` arg (branch name or commit SHA). Example — build
the variant against forked toolkit/extensions and a pinned pi:
```bash
# base first (mempalace-toolkit lives here)
docker build -f Dockerfile.base -t myorg/pi-devbox:base-dev \
--build-arg MEMPALACE_TOOLKIT_REPO=https://github.com/myorg/mempalace-toolkit.git .
# then the variant FROM that base
docker build -f Dockerfile.variant -t myorg/pi-devbox:dev \
--build-arg BASE_IMAGE=myorg/pi-devbox:base-dev \
--build-arg PI_VERSION=0.79.7 \
--build-arg PI_TOOLKIT_REPO=https://github.com/myorg/pi-toolkit.git \
--build-arg PI_EXTENSIONS_REPO=https://github.com/myorg/pi-extensions.git .
```
Note: the gitea companions clone anonymously (no token needed); only the
`resolve-versions` CI job calls the gitea *API* (which needs a token even
for public repos). A plain `docker build` like the above skips that job
entirely, so no credentials are required for a local/forked build.
Provenance build-args (all optional; populate the OCI labels and
`/etc/pi-devbox/build-manifest.json` — see below): `RELEASE_TAG`,
`BUILD_DATE`, `SOURCE_REVISION`. CI sets these automatically; a manual build
leaves them at harmless defaults.
### Build provenance (labels + manifest)
Every published image is self-describing. Inspect the OCI labels without
pulling the filesystem:
```bash
docker inspect --format '{{json .Config.Labels}}' joakimp/pi-devbox:latest | jq .
```
`org.opencontainers.image.{version,revision,created}` plus
`se.jordbo.pi-devbox.*-ref` record the intended pi version and companion
refs. The on-disk `/etc/pi-devbox/build-manifest.json` records **ground
truth** — the actual checked-out commit of each `/opt` clone and the live
`pi --version` — so a tag is reconstructable after CI logs rotate:
```bash
docker run --rm --entrypoint= joakimp/pi-devbox:latest cat /etc/pi-devbox/build-manifest.json
```
## Troubleshooting
### Image grew unexpectedly
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# check-base-hash.sh — guard the base-rebuild invariant.
#
# Every floating `ARG *_REF` consumed by Dockerfile.base MUST be folded
# into the base_tag hash in the docker-publish workflow. Otherwise a
# ref-only change to that dependency does not change the base hash, the
# Docker Hub probe finds the old base tag, and the base is NOT rebuilt —
# the dependency fix silently fails to land. This is the v1.1.2-class
# staleness footgun (then it was mempalace-toolkit; this guard stops the
# next one before it ships).
#
# Runs in CI (base-decide job) and locally: bash scripts/check-base-hash.sh
set -euo pipefail
cd "$(dirname "$0")/.."
WF=".gitea/workflows/docker-publish.yml"
DF="Dockerfile.base"
# Extract the hash-compute block: the `HASH=$( … ) | sha256sum | cut`
# brace-group in the "Compute base tag" step. This lives in a separate
# file from the workflow, so scanning $WF here is free of the self-match
# hazard an inline workflow step would have.
block=$(awk '/HASH=\$\(/{f=1} f{print} f && /cut -c1-12/{exit}' "$WF")
if [ -z "$block" ]; then
echo "::error::could not locate the HASH=\$( … ) | sha256sum block in $WF"
exit 1
fi
refs=$(grep -oE '^ARG [A-Z0-9_]+_REF' "$DF" | awk '{print $2}' | sort -u)
fail=0
for r in $refs; do
lc=$(printf '%s' "$r" | tr '[:upper:]' '[:lower:]')
if ! printf '%s' "$block" | grep -q "outputs.$lc"; then
echo "::error::Dockerfile.base declares '$r' but it is NOT folded into the base_tag hash in $WF."
echo "::error::Add echo \"\${{ needs.resolve-versions.outputs.$lc }}\" inside the HASH=\$( … ) | sha256sum block, or a $r-only change will silently fail to rebuild the base."
fail=1
fi
done
if [ "$fail" = 0 ]; then
echo "OK: all Dockerfile.base *_REF args are folded into base_tag (${refs:-none})."
fi
exit $fail
+22
View File
@@ -113,6 +113,28 @@ else
echo " ️ pi-studio not present (non-studio variant) — skipping studio clone checks"
fi
# ── Build provenance (manifest + OCI labels) ─────────────────────────
echo ""
echo "── Build provenance ──"
run "/etc/pi-devbox/build-manifest.json present" \
"test -f /etc/pi-devbox/build-manifest.json"
run_expect "manifest records pi-extensions component" \
"cat /etc/pi-devbox/build-manifest.json" '"pi-extensions"'
run_expect "manifest records pi_version" \
"cat /etc/pi-devbox/build-manifest.json" '"pi_version"'
# Every component must be a resolved commit (or null for pi-studio in the
# non-studio variant) — 'unknown' means a clone silently failed to resolve.
run "manifest has no unresolved ('unknown') components" \
"! grep -q '\"unknown\"' /etc/pi-devbox/build-manifest.json"
# OCI labels live in the image config, not the container fs — inspect them
# from the host docker rather than via `docker run`.
LBL=$(docker inspect --format '{{ index .Config.Labels "se.jordbo.pi-devbox.pi-extensions-ref" }}' "$IMAGE" 2>/dev/null || true)
if [ -n "$LBL" ] && [ "$LBL" != "<no value>" ]; then
printf " ✅ OCI label se.jordbo.pi-devbox.pi-extensions-ref=%s\n" "$LBL"; PASS=$((PASS+1))
else
printf " ❌ OCI label se.jordbo.pi-devbox.pi-extensions-ref missing or empty\n"; FAIL=$((FAIL+1))
fi
# ── Runtime deployment (needs entrypoint to run) ──────────────────────
echo ""
echo "── Runtime deployment ──"