Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ed49b8d97a | |||
| 9eff3f3c48 | |||
| a0abacaafb |
@@ -58,6 +58,9 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Compute base tag from Dockerfile.base + dependencies
|
||||||
id: compute
|
id: compute
|
||||||
run: |
|
run: |
|
||||||
@@ -126,53 +129,72 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Resolve pi version + companion refs
|
- name: Resolve pi version + companion refs
|
||||||
id: resolve
|
id: resolve
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -euo pipefail
|
||||||
# Query npm registry directly; catthehacker/ubuntu:act-latest's npm
|
AUTH_HEADER="Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||||
# 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')
|
# 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"
|
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" \
|
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" \
|
OBSMEM_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || echo "master")
|
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || true)
|
||||||
[ -n "$FORK_REF" ] || FORK_REF=master
|
require_sha PI_OBSMEM_REF "$OBSMEM_REF"
|
||||||
[ -n "$OBSMEM_REF" ] || OBSMEM_REF=master
|
|
||||||
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
|
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "obsmem_ref=${OBSMEM_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
|
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. Gitea API
|
||||||
# those repos haven't moved (and a clean diff in build-arg strings
|
# requires auth even for public-repo commit listing.
|
||||||
# when they have, defeating the registry buildcache footgun).
|
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||||
# Gitea API requires auth even for public-repo commit listing.
|
|
||||||
TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
|
||||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
|
"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")
|
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||||
EXTENSIONS_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
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" \
|
"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")
|
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||||
[ -n "$TOOLKIT_REF" ] || TOOLKIT_REF=main
|
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
|
||||||
[ -n "$EXTENSIONS_REF" ] || EXTENSIONS_REF=main
|
|
||||||
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "extensions_ref=${EXTENSIONS_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
|
# mempalace-toolkit (Gitea) → commit SHA. UNLIKE the others this
|
||||||
# ALSO folded into the base-decide hash to force a base rebuild
|
# is cloned in Dockerfile.base, so the SAME SHA is ALSO folded
|
||||||
# when the toolkit moves (without it, a toolkit-only fix silently
|
# into the base-decide hash (see that job) to force a base rebuild
|
||||||
# fails to land unless Dockerfile.base itself changes).
|
# when the toolkit moves — otherwise a toolkit-only fix silently
|
||||||
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
# 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" \
|
"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")
|
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||||
[ -n "$MEMPALACE_TOOLKIT_REF" ] || MEMPALACE_TOOLKIT_REF=main
|
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
|
||||||
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
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" \
|
STUDIO_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || echo "main")
|
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || true)
|
||||||
[ -n "$STUDIO_REF" ] || STUDIO_REF=main
|
require_sha PI_STUDIO_REF "$STUDIO_REF"
|
||||||
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
|
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
echo "Resolved PI_VERSION=${PI_VERSION}"
|
echo "Resolved PI_VERSION=${PI_VERSION}"
|
||||||
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
||||||
echo "Resolved PI_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_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_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
|
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_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)
|
- name: Smoke test (amd64)
|
||||||
env:
|
env:
|
||||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
@@ -355,6 +380,9 @@ jobs:
|
|||||||
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
INSTALL_STUDIO=true
|
INSTALL_STUDIO=true
|
||||||
PI_STUDIO_REF=${{ needs.resolve-versions.outputs.studio_ref }}
|
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)
|
- name: Smoke test studio (amd64)
|
||||||
env:
|
env:
|
||||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
@@ -406,10 +434,12 @@ jobs:
|
|||||||
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
|
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG_FLAGS=()
|
TAG_FLAGS=()
|
||||||
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
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).
|
# 3-attempt retry (see build-base step for rationale).
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "==> Build+push attempt ${attempt}/3"
|
echo "==> Build+push attempt ${attempt}/3"
|
||||||
@@ -423,6 +453,10 @@ jobs:
|
|||||||
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||||
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
||||||
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_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[@]}" \
|
"${TAG_FLAGS[@]}" \
|
||||||
.; then
|
.; then
|
||||||
echo "==> Attempt ${attempt} succeeded"
|
echo "==> Attempt ${attempt} succeeded"
|
||||||
@@ -487,10 +521,12 @@ jobs:
|
|||||||
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
|
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
|
||||||
|
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG_FLAGS=()
|
TAG_FLAGS=()
|
||||||
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
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).
|
# 3-attempt retry (see build-base step for rationale).
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "==> Build+push attempt ${attempt}/3"
|
echo "==> Build+push attempt ${attempt}/3"
|
||||||
@@ -504,8 +540,12 @@ jobs:
|
|||||||
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||||
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
||||||
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
||||||
|
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
|
||||||
--build-arg "INSTALL_STUDIO=true" \
|
--build-arg "INSTALL_STUDIO=true" \
|
||||||
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
|
--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[@]}" \
|
"${TAG_FLAGS[@]}" \
|
||||||
.; then
|
.; then
|
||||||
echo "==> Attempt ${attempt} succeeded"
|
echo "==> Attempt ${attempt} succeeded"
|
||||||
|
|||||||
+117
@@ -13,6 +13,123 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
## Unreleased
|
## 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`
|
||||||
|
(auto-resolved at build). The `pi-extensions` ref is auto-resolved to `main`
|
||||||
|
HEAD at build, so the `ssh-controlmaster` fix below lands automatically.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`pi --ssh <host>` no longer fails with "Read-only file system" when the
|
||||||
|
user's `~/.ssh/config` sets a per-host `ControlPath` under the read-only
|
||||||
|
`~/.ssh` mount** (e.g. the common CGNAT idiom `ControlPath ~/.ssh/cm/%r@%h:%p`).
|
||||||
|
Root cause: SSH precedence means a user's per-host `ControlPath` always wins
|
||||||
|
over the baked `/etc/ssh/ssh_config.d` default, so the master socket tried to
|
||||||
|
bind under the RO `~/.ssh` and `ssh … pwd` exited 255 ("Could not resolve
|
||||||
|
remote pwd"). The `ssh-controlmaster` extension (pulled from `pi-extensions`
|
||||||
|
`main` via `PI_EXTENSIONS_REF`) now (a) resolves the remote pwd with a direct
|
||||||
|
connection (`-o ControlPath=none -o ControlMaster=no`), and (b) tests whether
|
||||||
|
the system `ControlPath` dir is actually writable — falling back to its own
|
||||||
|
`/tmp` master (whose command-line `-o ControlPath` overrides the user's path)
|
||||||
|
when it is not. OS-agnostic and independent of whether the user uses
|
||||||
|
ControlMaster, so the majority of configs (no ControlMaster at all) are
|
||||||
|
unaffected.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`setup-lan-access.sh` now renders the writable SSH sidecar
|
||||||
|
(`~/.ssh-local/config`) on every host OS, not just VM-backed ones.**
|
||||||
|
Previously the whole script no-oped on native Linux, so a Linux host that
|
||||||
|
also bind-mounts `~/.ssh` read-only got no `ControlPath` redirect. The
|
||||||
|
`ControlPath` redirect + `Include ~/.ssh/config` (and `dssh`/`dscp` usability)
|
||||||
|
now work on Linux too; only the host-jump block (`Host host mac`), its key
|
||||||
|
generation, and the authorize hints remain gated on VM-backed detection
|
||||||
|
(`DEVBOX_LAN_ACCESS=auto`) or `=jump`.
|
||||||
|
|
||||||
|
### Bumped: pi 0.79.6 → 0.79.7
|
||||||
|
|
||||||
|
Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.7)):
|
||||||
|
|
||||||
|
- **Automatic theme mode** — `/settings` can choose separate light and dark
|
||||||
|
themes and follow terminal color-scheme changes (`/` is now reserved in
|
||||||
|
theme names for this).
|
||||||
|
- **Self-only `pi update` by default** — bare `pi update` updates pi only;
|
||||||
|
`pi update --all` updates pi and packages together.
|
||||||
|
- **Extension API helpers** — `CONFIG_DIR_NAME` exported so extensions resolve
|
||||||
|
project config paths without hardcoding `.pi`; edit-diff helpers
|
||||||
|
(`generateDiffString`, `generateUnifiedPatch`, `EditDiffResult`) exported.
|
||||||
|
- **Warp inline images** via Kitty graphics capability detection.
|
||||||
|
- Fixes: RPC unknown-command errors now include the request id (clients no
|
||||||
|
longer hang); `/model` autocomplete matches provider/model regardless of
|
||||||
|
token order; tree navigator horizontally pans deep entries.
|
||||||
|
|
||||||
## v1.1.4 — 2026-06-17
|
## v1.1.4 — 2026-06-17
|
||||||
|
|
||||||
Patch release: config and shell-quality fixes on a preserved volume. No pi
|
Patch release: config and shell-quality fixes on a preserved volume. No pi
|
||||||
|
|||||||
+1
-1
@@ -97,7 +97,7 @@ The entrypoint deploys/registers all of these on first container start. Re-runni
|
|||||||
|
|
||||||
### SSH and networking
|
### SSH and networking
|
||||||
|
|
||||||
- OpenSSH client with **ControlMaster auto** preconfigured on a writable socket path (`/tmp/sshcm/`). Mitigates ssh banner-exchange failures behind CGNAT-restricted residential ISPs (~4-flow caps).
|
- OpenSSH client with **ControlMaster auto** preconfigured on a writable socket path (`/tmp/sshcm/`). Mitigates ssh banner-exchange failures behind CGNAT-restricted residential ISPs (~4-flow caps). A read-only `~/.ssh` carrying a per-host `ControlPath` (common CGNAT configs) is handled too — redirected to a writable socket dir for both `pi --ssh` and `dssh`/`dscp`.
|
||||||
- A **LAN-access helper** that auto-configures ssh jump-via-host on VM-backed hosts (OrbStack / Docker Desktop on macOS) so the container can reach the host's directly-attached LAN peers (`dssh <peer>` alias; `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER`).
|
- A **LAN-access helper** that auto-configures ssh jump-via-host on VM-backed hosts (OrbStack / Docker Desktop on macOS) so the container can reach the host's directly-attached LAN peers (`dssh <peer>` alias; `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER`).
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|||||||
+15
-1
@@ -130,6 +130,15 @@ RUN printf '%s\n' \
|
|||||||
# `Include /etc/ssh/ssh_config.d/*.conf` *before* the `Host *` block,
|
# `Include /etc/ssh/ssh_config.d/*.conf` *before* the `Host *` block,
|
||||||
# so user config can override these defaults if desired.
|
# so user config can override these defaults if desired.
|
||||||
#
|
#
|
||||||
|
# CAVEAT (and why it is handled elsewhere): a user per-host override that
|
||||||
|
# points ControlPath BACK under the read-only ~/.ssh (e.g. the common CGNAT
|
||||||
|
# idiom `ControlPath ~/.ssh/cm/%r@%h:%p`) re-introduces the unwritable-socket
|
||||||
|
# failure — a system drop-in here can never override a user's per-host value.
|
||||||
|
# For `pi --ssh`, the ssh-controlmaster extension handles this by detecting an
|
||||||
|
# unwritable system ControlPath and falling back to its own /tmp master; for
|
||||||
|
# `ssh -F ~/.ssh-local/config` (dssh/dscp), setup-lan-access.sh redirects
|
||||||
|
# ControlPath into the writable ~/.ssh-local. See CHANGELOG "Unreleased".
|
||||||
|
#
|
||||||
# ControlPersist=10m means the master socket sticks around 10 min after
|
# ControlPersist=10m means the master socket sticks around 10 min after
|
||||||
# the last session closes, so consecutive ssh calls in a workflow reuse
|
# the last session closes, so consecutive ssh calls in a workflow reuse
|
||||||
# the same TCP flow. Companion entrypoint-user.sh creates /tmp/sshcm
|
# the same TCP flow. Companion entrypoint-user.sh creates /tmp/sshcm
|
||||||
@@ -342,6 +351,11 @@ RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|||||||
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
||||||
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
||||||
ARG MEMPALACE_TOOLKIT_REF=main
|
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
|
# 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
|
# 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
|
# base-decide hash so the base rebuilds when the toolkit moves. `git clone
|
||||||
@@ -351,7 +365,7 @@ ARG MEMPALACE_TOOLKIT_REF=main
|
|||||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
||||||
rm -rf /opt/mempalace-toolkit && mkdir -p /opt/mempalace-toolkit && \
|
rm -rf /opt/mempalace-toolkit && mkdir -p /opt/mempalace-toolkit && \
|
||||||
git -C /opt/mempalace-toolkit init -q && \
|
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 \
|
ok=0; for i in 1 2 3 4 5; do \
|
||||||
if git -C /opt/mempalace-toolkit fetch --depth 1 origin "${MEMPALACE_TOOLKIT_REF}" && \
|
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; \
|
git -C /opt/mempalace-toolkit checkout -q FETCH_HEAD; then ok=1; break; fi; \
|
||||||
|
|||||||
+64
-2
@@ -41,6 +41,12 @@ ARG USER_NAME=developer
|
|||||||
ARG PI_VERSION=latest
|
ARG PI_VERSION=latest
|
||||||
ARG PI_TOOLKIT_REF=main
|
ARG PI_TOOLKIT_REF=main
|
||||||
ARG PI_EXTENSIONS_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
|
# pi-fork (fork tool) + pi-observational-memory (recall tool) live on GitHub
|
||||||
# under elpapi42. CI resolves these to commit SHAs to defeat the same
|
# under elpapi42. CI resolves these to commit SHAs to defeat the same
|
||||||
# cache-hit footgun that affects PI_VERSION.
|
# 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} ; \
|
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \
|
||||||
fi && \
|
fi && \
|
||||||
pi --version && \
|
pi --version && \
|
||||||
git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-toolkit.git" "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
|
git_fetch_ref "${PI_TOOLKIT_REPO}" "${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_EXTENSIONS_REPO}" "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
|
||||||
git_fetch_ref "${PI_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \
|
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 && \
|
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) && \
|
(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; \
|
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
|
||||||
fi
|
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.
|
# WORKDIR / ENTRYPOINT / CMD inherited from base.
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ For Python REPLs and notebooks beyond the system interpreter, see the
|
|||||||
- A LAN-access helper that auto-configures ssh jump-via-host on
|
- A LAN-access helper that auto-configures ssh jump-via-host on
|
||||||
VM-backed hosts (OrbStack / Docker Desktop on macOS) so the container
|
VM-backed hosts (OrbStack / Docker Desktop on macOS) so the container
|
||||||
can reach the host's directly-attached LAN peers.
|
can reach the host's directly-attached LAN peers.
|
||||||
|
- Read-only `~/.ssh` is handled transparently: a per-host `ControlPath`
|
||||||
|
under it (common CGNAT configs like `~/.ssh/cm/...`) is redirected to a
|
||||||
|
writable socket dir for both `pi --ssh` and `dssh`/`dscp`.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
@@ -461,6 +464,23 @@ User-level overrides in `~/.ssh/config` win because Debian's
|
|||||||
`/etc/ssh/ssh_config` includes `/etc/ssh/ssh_config.d/*.conf` before
|
`/etc/ssh/ssh_config` includes `/etc/ssh/ssh_config.d/*.conf` before
|
||||||
the `Host *` block.
|
the `Host *` block.
|
||||||
|
|
||||||
|
### Per-host `ControlPath` on a read-only `~/.ssh`
|
||||||
|
|
||||||
|
`~/.ssh` is usually bind-mounted read-only, so a user `~/.ssh/config` that
|
||||||
|
points `ControlPath` back under it (e.g. the CGNAT idiom
|
||||||
|
`ControlPath ~/.ssh/cm/%r@%h:%p`) can't bind its master socket here — and a
|
||||||
|
system default can never override a user's per-host value. Two layers handle
|
||||||
|
this without editing the read-only config:
|
||||||
|
|
||||||
|
- **`pi --ssh <host>`** — the `ssh-controlmaster` extension detects an
|
||||||
|
unwritable system `ControlPath` and falls back to its own writable
|
||||||
|
`/tmp/pi-cm-<pid>.sock` master (its command-line `-o ControlPath` overrides
|
||||||
|
the user's path); the remote-`pwd` probe uses `-o ControlPath=none` so it
|
||||||
|
cannot fail on the read-only socket dir.
|
||||||
|
- **`ssh -F ~/.ssh-local/config` / `dssh` / `dscp`** — `setup-lan-access.sh`
|
||||||
|
redirects `ControlPath` into the writable `~/.ssh-local/cm` for every host
|
||||||
|
(the sidecar is rendered on all host OSes).
|
||||||
|
|
||||||
## tmux and 0-indexed sessions
|
## tmux and 0-indexed sessions
|
||||||
|
|
||||||
The image installs `/etc/tmux.conf` with:
|
The image installs `/etc/tmux.conf` with:
|
||||||
@@ -517,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
|
byte-identical across releases and the layer would silently reuse the
|
||||||
previous version's bytes).
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
### Image grew unexpectedly
|
### Image grew unexpectedly
|
||||||
|
|||||||
+10
-6
@@ -12,12 +12,16 @@ set -euo pipefail
|
|||||||
mkdir -p /tmp/sshcm
|
mkdir -p /tmp/sshcm
|
||||||
chmod 700 /tmp/sshcm
|
chmod 700 /tmp/sshcm
|
||||||
|
|
||||||
# ── LAN access: generic host-OS-agnostic reachability helper ────────
|
# ── LAN access + writable SSH sidecar: host-OS-agnostic helper ──────
|
||||||
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
|
# Generates the writable ~/.ssh-local/config on EVERY host OS: a `Host *`
|
||||||
# reach the host's directly-attached LAN peers by default; this generates a
|
# ControlPath redirect into ~/.ssh-local/cm (so `ssh -F` / dssh / dscp work
|
||||||
# writable ~/.ssh-local/config that uses the host as an SSH jump. On native
|
# even when ~/.ssh is bind-mounted read-only) plus `Include ~/.ssh/config`. On
|
||||||
# Linux (LAN reachable directly) it is a no-op. Controlled by DEVBOX_LAN_ACCESS
|
# VM-backed hosts (macOS OrbStack / Docker Desktop) it ALSO adds an
|
||||||
# (auto|jump|off) + HOST_SSH_USER. Always non-fatal. See the script header.
|
# SSH-jump-via-host block so the container can reach the host's
|
||||||
|
# directly-attached LAN peers; on native Linux (LAN reachable directly) the
|
||||||
|
# jump block is omitted but the sidecar is still rendered. Controlled by
|
||||||
|
# DEVBOX_LAN_ACCESS (auto|jump|off) + HOST_SSH_USER. Always non-fatal. See the
|
||||||
|
# script header.
|
||||||
if [ -r /usr/local/lib/pi-devbox/setup-lan-access.sh ]; then
|
if [ -r /usr/local/lib/pi-devbox/setup-lan-access.sh ]; then
|
||||||
bash /usr/local/lib/pi-devbox/setup-lan-access.sh || true
|
bash /usr/local/lib/pi-devbox/setup-lan-access.sh || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -14,7 +14,9 @@
|
|||||||
# The one thing reachable from a container on every OS is the host itself
|
# The one thing reachable from a container on every OS is the host itself
|
||||||
# (host.docker.internal). So on VM-backed hosts we generate a writable SSH
|
# (host.docker.internal). So on VM-backed hosts we generate a writable SSH
|
||||||
# config that reaches the host and lets the user ProxyJump onward to LAN
|
# config that reaches the host and lets the user ProxyJump onward to LAN
|
||||||
# peers the host can reach. On native Linux we do nothing.
|
# peers the host can reach. On native Linux we render the same writable
|
||||||
|
# config (for the ControlPath redirect + Include ~/.ssh/config) but emit no
|
||||||
|
# jump block, since LAN peers are reachable directly there.
|
||||||
#
|
#
|
||||||
# We ship the MECHANISM (a generic `host` jump alias + writable config),
|
# We ship the MECHANISM (a generic `host` jump alias + writable config),
|
||||||
# never the POLICY: the user's specific target hosts live in their own
|
# never the POLICY: the user's specific target hosts live in their own
|
||||||
@@ -30,7 +32,9 @@
|
|||||||
#
|
#
|
||||||
# CONTROLS (env)
|
# CONTROLS (env)
|
||||||
# DEVBOX_LAN_ACCESS = auto (default) | jump | off
|
# DEVBOX_LAN_ACCESS = auto (default) | jump | off
|
||||||
# auto → set up the jump config only on VM-backed hosts; no-op on Linux.
|
# auto → set up the host jump only on VM-backed hosts. The writable
|
||||||
|
# sidecar config (ControlPath redirect + Include) is always
|
||||||
|
# rendered, on every OS.
|
||||||
# jump → always set up (e.g. native Linux with extra_hosts host-gateway).
|
# jump → always set up (e.g. native Linux with extra_hosts host-gateway).
|
||||||
# off → do nothing.
|
# off → do nothing.
|
||||||
# HOST_SSH_USER — the username to SSH into the host as. REQUIRED for the
|
# HOST_SSH_USER — the username to SSH into the host as. REQUIRED for the
|
||||||
@@ -84,42 +88,72 @@ is_vm_backed() {
|
|||||||
getent hosts "$HOST_ALIAS_HOSTNAME" >/dev/null 2>&1
|
getent hosts "$HOST_ALIAS_HOSTNAME" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "$MODE" = "auto" ] && ! is_vm_backed; then
|
# ── Writable socket dir + sidecar (ALWAYS, every host OS) ─────────────
|
||||||
# Native Linux host: LAN peers are reachable directly. Nothing to do.
|
# The ControlPath redirect in the generated config needs a writable directory
|
||||||
exit 0
|
# regardless of host OS or jump mode. ~/.ssh is typically read-only, so the
|
||||||
fi
|
# master socket lives under the writable ~/.ssh-local. We create it and render
|
||||||
|
# the config UNCONDITIONALLY so the redirect (and `Include ~/.ssh/config`) works
|
||||||
# From here: MODE=jump, or MODE=auto on a VM-backed host.
|
# even on native Linux — where we set up no host jump but a read-only ~/.ssh
|
||||||
|
# would otherwise still break ControlMaster sockets.
|
||||||
command -v ssh-keygen >/dev/null 2>&1 || exit 0
|
|
||||||
|
|
||||||
mkdir -p "${SSH_LOCAL}/cm" 2>/dev/null || true
|
mkdir -p "${SSH_LOCAL}/cm" 2>/dev/null || true
|
||||||
chmod 700 "${SSH_LOCAL}" "${SSH_LOCAL}/cm" 2>/dev/null || true
|
chmod 700 "${SSH_LOCAL}" "${SSH_LOCAL}/cm" 2>/dev/null || true
|
||||||
|
|
||||||
# ── Jump key (generated once; preserved across restarts) ──────────────
|
# ── Decide whether to set up the host jump ────────────────────────────
|
||||||
|
# Jump = reach the container host (host.docker.internal) as an SSH ProxyJump
|
||||||
|
# onward to the host's LAN peers. Needed on VM-backed hosts (macOS / Docker
|
||||||
|
# Desktop) or when forced with DEVBOX_LAN_ACCESS=jump. On native Linux LAN
|
||||||
|
# peers are reachable directly, so NEED_JUMP=0 and we emit no jump block — but
|
||||||
|
# we still render the config for the ControlPath redirect + Include.
|
||||||
|
NEED_JUMP=0
|
||||||
|
if [ "$MODE" = "jump" ] || { [ "$MODE" = "auto" ] && is_vm_backed; }; then
|
||||||
|
NEED_JUMP=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Jump key (only when a jump is needed; generated once, preserved) ──
|
||||||
# Persisted via a named volume on ~/.ssh-local (see compose), so a fresh key
|
# Persisted via a named volume on ~/.ssh-local (see compose), so a fresh key
|
||||||
# is generated only on the very first start (or if the volume is wiped). When
|
# is generated only on the very first start (or if the volume is wiped). When
|
||||||
# we DO generate one it must be (re-)authorized on the host, so we flag it and
|
# we DO generate one it must be (re-)authorized on the host, so we flag it and
|
||||||
# print a copy-paste authorize line below.
|
# print a copy-paste authorize line below.
|
||||||
KEY_JUST_GENERATED=0
|
KEY_JUST_GENERATED=0
|
||||||
if [ ! -f "$KEY" ]; then
|
if [ "$NEED_JUMP" = "1" ] && command -v ssh-keygen >/dev/null 2>&1 && [ ! -f "$KEY" ]; then
|
||||||
ssh-keygen -t ed25519 -N '' -C "devbox-jump@${HOSTNAME:-container}" -f "$KEY" >/dev/null 2>&1 || exit 0
|
if ssh-keygen -t ed25519 -N '' -C "devbox-jump@${HOSTNAME:-container}" -f "$KEY" >/dev/null 2>&1; then
|
||||||
chmod 600 "$KEY" 2>/dev/null || true
|
chmod 600 "$KEY" 2>/dev/null || true
|
||||||
KEY_JUST_GENERATED=1
|
KEY_JUST_GENERATED=1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Render the writable config ────────────────────────────────────────
|
# ── Render the writable config ────────────────────────────────────────
|
||||||
USER_LINE=""
|
# Jump-specific blocks (the host alias, host-owned peer overrides, and the
|
||||||
if [ -n "${HOST_SSH_USER:-}" ]; then
|
# optional RFC1918 catch-all) only make sense when a jump is set up; on native
|
||||||
USER_LINE=" User ${HOST_SSH_USER}"
|
# Linux they are all empty and only the ControlPath redirect + Include remain.
|
||||||
fi
|
JUMP_BLOCK=""
|
||||||
|
|
||||||
# Optional host-owned named-peer jump overrides (portable: lives on the host,
|
|
||||||
# not in the image). Included BEFORE ~/.ssh/config so its ProxyJump wins.
|
|
||||||
SSH_LAN_CONF="${HOME}/.config/devbox-shell/ssh-lan.conf"
|
|
||||||
LAN_CONF_BLOCK=""
|
LAN_CONF_BLOCK=""
|
||||||
if [ -r "$SSH_LAN_CONF" ]; then
|
AUTOJUMP_BLOCK=""
|
||||||
LAN_CONF_BLOCK=$(cat <<'EOF'
|
if [ "$NEED_JUMP" = "1" ]; then
|
||||||
|
USER_LINE=""
|
||||||
|
if [ -n "${HOST_SSH_USER:-}" ]; then
|
||||||
|
USER_LINE=" User ${HOST_SSH_USER}"
|
||||||
|
fi
|
||||||
|
JUMP_BLOCK=$(cat <<EOF
|
||||||
|
|
||||||
|
# The container host (OrbStack / Docker Desktop). 'host' and 'mac' are aliases.
|
||||||
|
Host host mac
|
||||||
|
HostName ${HOST_ALIAS_HOSTNAME}
|
||||||
|
${USER_LINE}
|
||||||
|
IdentityFile ~/.ssh-local/devbox_jump_ed25519
|
||||||
|
IdentitiesOnly yes
|
||||||
|
ControlMaster auto
|
||||||
|
ControlPath ~/.ssh-local/cm/%r@%h:%p
|
||||||
|
ControlPersist 4h
|
||||||
|
ServerAliveInterval 30
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional host-owned named-peer jump overrides (portable: lives on the host,
|
||||||
|
# not in the image). Included BEFORE ~/.ssh/config so its ProxyJump wins.
|
||||||
|
SSH_LAN_CONF="${HOME}/.config/devbox-shell/ssh-lan.conf"
|
||||||
|
if [ -r "$SSH_LAN_CONF" ]; then
|
||||||
|
LAN_CONF_BLOCK=$(cat <<'EOF'
|
||||||
|
|
||||||
# Host-owned named-peer jump overrides (bind-mounted; edit on the host).
|
# Host-owned named-peer jump overrides (bind-mounted; edit on the host).
|
||||||
# Scope reset to match-all so the Include applies to every target host.
|
# Scope reset to match-all so the Include applies to every target host.
|
||||||
@@ -127,14 +161,13 @@ Host *
|
|||||||
Include ~/.config/devbox-shell/ssh-lan.conf
|
Include ~/.config/devbox-shell/ssh-lan.conf
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Optional opt-in RFC1918 catch-all: ProxyJump every private IP through the
|
# Optional opt-in RFC1918 catch-all: ProxyJump every private IP through the
|
||||||
# host. Matches the typed address, never the resolved HostName, so named hosts
|
# host. Matches the typed address, never the resolved HostName, so named hosts
|
||||||
# with their own ProxyJump are unaffected. Network-agnostic → roaming-safe.
|
# with their own ProxyJump are unaffected. Network-agnostic → roaming-safe.
|
||||||
AUTOJUMP_BLOCK=""
|
if [ "${DEVBOX_LAN_AUTOJUMP_PRIVATE:-0}" = "1" ]; then
|
||||||
if [ "${DEVBOX_LAN_AUTOJUMP_PRIVATE:-0}" = "1" ]; then
|
AUTOJUMP_BLOCK=$(cat <<'EOF'
|
||||||
AUTOJUMP_BLOCK=$(cat <<'EOF'
|
|
||||||
|
|
||||||
# RFC1918 auto-jump (DEVBOX_LAN_AUTOJUMP_PRIVATE=1): reach any private IP on
|
# RFC1918 auto-jump (DEVBOX_LAN_AUTOJUMP_PRIVATE=1): reach any private IP on
|
||||||
# the host's CURRENT LAN via bare `dssh user@<ip>`. Public IPs are unmatched
|
# the host's CURRENT LAN via bare `dssh user@<ip>`. Public IPs are unmatched
|
||||||
@@ -146,6 +179,7 @@ Host 10.* 192.168.* 172.16.* 172.17.* 172.18.* 172.19.* 172.20.* 172.21.* 172.22
|
|||||||
ProxyJump host
|
ProxyJump host
|
||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
INCLUDE_BLOCK=""
|
INCLUDE_BLOCK=""
|
||||||
@@ -176,17 +210,7 @@ Host *
|
|||||||
UserKnownHostsFile ~/.ssh-local/known_hosts
|
UserKnownHostsFile ~/.ssh-local/known_hosts
|
||||||
StrictHostKeyChecking accept-new
|
StrictHostKeyChecking accept-new
|
||||||
ControlPath ~/.ssh-local/cm/%r@%h:%p
|
ControlPath ~/.ssh-local/cm/%r@%h:%p
|
||||||
|
${JUMP_BLOCK}
|
||||||
# The container host (OrbStack / Docker Desktop). 'host' and 'mac' are aliases.
|
|
||||||
Host host mac
|
|
||||||
HostName ${HOST_ALIAS_HOSTNAME}
|
|
||||||
${USER_LINE}
|
|
||||||
IdentityFile ~/.ssh-local/devbox_jump_ed25519
|
|
||||||
IdentitiesOnly yes
|
|
||||||
ControlMaster auto
|
|
||||||
ControlPath ~/.ssh-local/cm/%r@%h:%p
|
|
||||||
ControlPersist 4h
|
|
||||||
ServerAliveInterval 30
|
|
||||||
${LAN_CONF_BLOCK}
|
${LAN_CONF_BLOCK}
|
||||||
${AUTOJUMP_BLOCK}
|
${AUTOJUMP_BLOCK}
|
||||||
${INCLUDE_BLOCK}
|
${INCLUDE_BLOCK}
|
||||||
@@ -199,6 +223,7 @@ chmod 600 "$CONFIG" 2>/dev/null || true
|
|||||||
# host won't recognize. With ~/.ssh-local persisted via a named volume, case
|
# host won't recognize. With ~/.ssh-local persisted via a named volume, case
|
||||||
# (b) fires only on first-ever start (or after the volume is reset) — so this
|
# (b) fires only on first-ever start (or after the volume is reset) — so this
|
||||||
# is normally a one-time, one-line step per machine, with no file to locate.
|
# is normally a one-time, one-line step per machine, with no file to locate.
|
||||||
|
if [ "$NEED_JUMP" = "1" ]; then
|
||||||
PUBKEY_TEXT="$(cat "${KEY}.pub" 2>/dev/null)"
|
PUBKEY_TEXT="$(cat "${KEY}.pub" 2>/dev/null)"
|
||||||
if [ -z "${HOST_SSH_USER:-}" ]; then
|
if [ -z "${HOST_SSH_USER:-}" ]; then
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
@@ -221,5 +246,6 @@ elif [ "$KEY_JUST_GENERATED" = "1" ]; then
|
|||||||
repeat this on container updates — only if that volume is reset.
|
repeat this on container updates — only if that volume is reset.
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
Executable
+43
@@ -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
|
||||||
@@ -113,6 +113,28 @@ else
|
|||||||
echo " ℹ️ pi-studio not present (non-studio variant) — skipping studio clone checks"
|
echo " ℹ️ pi-studio not present (non-studio variant) — skipping studio clone checks"
|
||||||
fi
|
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) ──────────────────────
|
# ── Runtime deployment (needs entrypoint to run) ──────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Runtime deployment ──"
|
echo "── Runtime deployment ──"
|
||||||
|
|||||||
Reference in New Issue
Block a user