Compare commits

..

4 Commits

Author SHA1 Message Date
pi f20b2a7926 ci(lint): don't re-lint on tag pushes
Lint / hadolint (push) Successful in 9s
Lint / actionlint (push) Has been cancelled
`on: push:` with no filter also fires on refs/tags/v*, which is duplicate work:
the tagged tree was already linted when that same commit was pushed to main
(v1.6.4 sha e86e5df linted as id=529 on main, then again as id=531 on the tag).

Two costs beyond the wasted run. It consumed one of the two self-hosted runners
while the release pipeline wanted both for its parallel multi-arch variant
builds; and it made release-run discovery ambiguous, since the newest-first runs
listing puts the tag-ref lint run above the publish run.

`branches: ['**']` keeps the documented intent exactly — lint fires early on
every branch push and PR, rather than only at tag time — while excluding tag
refs. docker-publish.yml is untouched and still tag-scoped.
2026-08-04 18:06:34 +02:00
pi 66a19aa394 docs(agents): how to find the release run (tag push fires two workflows)
The release-day checklist said "Watch CI" without saying which run, and the
Gitea API example used limit=5. Both are traps, because a tag push produces
TWO runs here: lint.yml has a bare `push:` trigger so it fires on the tag ref
as well, and docker-publish.yml fires on v*. The runs listing is newest-first
and the lint run sorts ABOVE the publish run, so "first run matching
refs/tags/<tag>" picks lint reliably. Verified against the real API for v1.6.4:

  id=531  #104  lint.yml@refs/tags/v1.6.4           <- picked by the naive rule
  id=530  #103  docker-publish.yml@refs/tags/v1.6.4 <- the actual release build
  id=529  #102  lint.yml@refs/heads/main            <- same sha, already linted

Lint goes green in minutes while the image is still building, so watching it
makes a release look finished before anything is published. limit=5 compounds
it: the publish run is already at position 4 of 5 in the current listing.

Documents: head_sha-filtered discovery with limit=20; the jobs endpoint takes
the internal id, never the run_number (silently returns another run's jobs);
and the correct ci-release-watcher config for this repo — EXPECT_WORKFLOW,
the studio tag pair, base-latest as existence-only, and CRITICAL_JOBS with
build-variant-studio spelled out (job names are matched exactly, and the
skill's default omits it) while excluding promote-base-latest, which
legitimately skips on a base cache hit.

Smoke-gate detail in step 5 is retained.
2026-08-04 18:06:34 +02:00
pi 572430237f Bump mempalace pin 3.5.0 → 3.6.0 (lockstep with opencode-devbox v2.9.0)
Lint / actionlint (push) Successful in 38s
Lint / hadolint (push) Successful in 50s
3.6.0 (2026-07-17, PyPI latest) is additive/reliability only: secure
`mempalace serve` remote mode, optional Milvus backend, atomic KG
supersede(), conversation chronology, mining exclusions, plus recovery and
locking fixes.

Reviewed for MCP tool-schema changes before bumping — that being the exact
regression class this pin exists to catch, after an unpinned install once
swept in the broken 3.3.x/3.4.0 diary_write schema. There are none, and
nothing touches diary_write, so the perl workaround removed in v1.2.2 stays
removed.

Two fixes matter for how this image uses mempalace: read-only mode now covers
checkpoint + delete_by_source in _MUTATING_TOOLS (#1930), and agent
attribution is preserved in mempalace_checkpoint (#2023/#2034) — the latter
because the diary protocol relies on per-agent attribution.

Also adds a CHANGELOG Unreleased block that backfills the per-variant image
description labels (1fd524e), pushed after the v1.6.4 tag without an entry.

Not tagged: more changes are queued for the next release. Pushing to main
triggers only lint.yml (actionlint + hadolint) — the image build/publish
workflow is tag-only. Verified clean against the CI-pinned hadolint 2.14.0.
2026-08-04 16:01:39 +02:00
joakimp 1fd524e7fb Give each variant its own image description label
Lint / actionlint (push) Successful in 14s
Lint / hadolint (push) Successful in 12s
Both published variants inherited Dockerfile.base's
description="pi-devbox — base image (variant-independent)", so v1.6.4 and
v1.6.4-studio both advertised themselves on Docker Hub as the base image —
misleading, and useless for telling the two apart.

A LABEL cannot branch on INSTALL_STUDIO, so the text arrives as a build-arg:
CI passes a variant-specific string (interpolating RELEASE_TAG, PI_VERSION and,
for studio, STUDIO_TAG), and the Dockerfile default keeps a bare local
`docker build -f Dockerfile.variant` honest instead of misleading.

Also sets org.opencontainers.image.title/description alongside the legacy bare
`description` key, so Hub and OCI-aware tooling both see it. ARGs stay in the
last-declared block, so the label layer is still the only thing invalidated.

Verified: hadolint 2.14.0 (the CI-pinned version) clean on both Dockerfiles;
workflow YAML parses; check-workflow-shell.sh passes. Lands on the next release.
2026-07-30 07:59:41 +02:00
6 changed files with 139 additions and 8 deletions
+4
View File
@@ -490,6 +490,8 @@ jobs:
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
--build-arg "IMAGE_TITLE=pi-devbox" \
--build-arg "IMAGE_DESCRIPTION=pi-devbox ${RELEASE_TAG} — core variant: pi coding agent CLI ${PI_VERSION}, pi-toolkit, extensions (fork + observational-memory), MemPalace. No browser UI — see the -studio tags for that." \
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
--build-arg "BUILD_DATE=${BUILD_DATE}" \
--build-arg "SOURCE_REVISION=${GITHUB_SHA:-}" \
@@ -579,6 +581,8 @@ jobs:
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
--build-arg "INSTALL_STUDIO=true" \
--build-arg "IMAGE_TITLE=pi-devbox (studio)" \
--build-arg "IMAGE_DESCRIPTION=pi-devbox ${RELEASE_TAG} — studio variant: everything in the core variant (pi ${PI_VERSION}, pi-toolkit, fork + observational-memory, MemPalace) plus the pi-studio browser UI ${STUDIO_TAG}." \
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
--build-arg "PI_STUDIO_VERSION=${STUDIO_TAG}" \
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
+16 -2
View File
@@ -6,11 +6,25 @@ name: Lint
# actionlint runs shellcheck against each `run:` step using its *effective*
# shell, so `set -o pipefail` under dash is flagged as SC3040 before any
# expensive build runs. This is cheap (~10s) and independent of the build
# pipeline, so it fires on every push/PR — not just on release tags, which
# is where the build workflow (docker-publish.yml) is otherwise only
# pipeline, so it fires on every branch push/PR — not just on release tags,
# which is where the build workflow (docker-publish.yml) is otherwise only
# triggered.
#
# `branches: ['**']` (rather than a bare `push:`) deliberately EXCLUDES tag
# pushes. A bare `push:` also fires on `refs/tags/v*`, which was pure duplicate
# work — the tagged tree was already linted when the same commit was pushed to
# main (v1.6.4: lint id=529 on refs/heads/main, then id=531 again on
# refs/tags/v1.6.4, same sha e86e5df). Worse, that duplicate consumed one of the
# two self-hosted runners while the release pipeline wanted both for its
# parallel multi-arch variant builds, and it made release-run discovery
# ambiguous: the runs listing is newest-first, so the tag-ref lint run sorts
# ABOVE the publish run and "first run matching refs/tags/<tag>" picks lint —
# which goes green in minutes while the image is still building. See AGENTS.md
# "Gitea API access" for the head_sha-filtered discovery pattern.
on:
push:
branches:
- '**'
pull_request:
workflow_dispatch:
+53 -2
View File
@@ -76,7 +76,10 @@ re-brand of opencode-devbox's `pi-only` variant.
4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`.
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
pi version + new-base-tooling presence. Variant build is multi-arch
(amd64 + arm64) only after smoke passes.
(amd64 + arm64) only after smoke passes. **A tag push produces two runs, not
one** — `lint.yml` fires on every push (including tag refs) and
`docker-publish.yml` fires on `v*` tags. Watch the **publish** run; see
*Gitea API access* below for how to find it without picking lint by mistake.
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
base-latest if the base was rebuilt this run).
7. **Revoke any short-lived Gitea PAT** used during the release at
@@ -92,13 +95,61 @@ host `.env` via `docker-compose.yml` (`${GITEA_ACCESS_TOKEN:-}` /
**not** baked into the image. When configured, they are also available for
**any** direct Gitea API interaction from inside the container — inspecting
CI runs, checking published tags, listing commits — e.g.
`curl -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=5"`.
`curl -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20"`.
Prefer this over a short-lived PAT file when the env token is present (the
`ci-release-watcher` skill auto-detects it). Public-repo GET listings work
unauthenticated too, so the token matters mainly for private repos or
rate-limit headroom; its lifecycle is host-managed, so there is nothing to
revoke after use. Never echo the token value (including into logs).
**Gotcha — a tag push fires EVERY workflow whose triggers match the tag ref.**
`lint.yml` uses a bare `push:` trigger, so a release tag yields *both* a lint run
and the publish run. The listing is newest-first and lint sorts **above** the
publish run, so "take the first run whose `path` contains `refs/tags/<tag>`"
picks the wrong one **reliably, not occasionally**. Real listing for v1.6.4:
```
id=531 #104 lint.yml@refs/tags/v1.6.4 <- wrong; sorts first
id=530 #103 docker-publish.yml@refs/tags/v1.6.4 <- the release build
id=529 #102 lint.yml@refs/heads/main <- same commit, linted on push
```
Lint goes green in minutes while the image is still building, so watching it
makes a release look finished when nothing has been published yet.
**Gotcha — the jobs endpoint takes the internal `id`, NOT the `run_number` the
UI shows as `#104`.** The two diverge widely, and `GET
.../actions/runs/<run_number>/jobs` does **not** error — it silently returns a
*different* run's jobs. Always read `id` from the run listing:
```bash
# Which runs did this tag/commit trigger? Filter on head_sha; never trust
# ordering or run numbering. limit=20, not 5 — with two runs per push the
# publish run falls off a 5-item window fast.
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
"$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20" \
| jq --arg sha "$(git rev-list -n1 vX.Y.Z)" \
'.workflow_runs[] | select(.head_sha==$sha) | {id, run_number, path, status, conclusion}'
# pick the id whose .path starts with docker-publish.yml, then:
curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \
"$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs/<id>/jobs" \
| jq '.jobs[] | {name, status, conclusion}'
```
**Watcher config for this repo** (`ci-release-watcher` skill, hub-only shape —
pi-devbox has no downstream host to deploy to):
- `EXPECT_WORKFLOW=docker-publish.yml` — the skill's `preflight_run()` aborts at
startup if the run id belongs to lint instead.
- `EXPECTED_FRESH_TAGS='vX.Y.Z latest vX.Y.Z-studio latest-studio'`
- `EXPECTED_EXISTS_TAGS='base-latest'` — existence only: it is content-addressed
and legitimately keeps its old timestamp when the base is a cache hit.
- `CRITICAL_JOBS='build-variant build-variant-studio'` — job names are matched
**exactly** (`critical.issubset(succeeded)`), so the studio variant must be
listed explicitly; the skill's default omits it. Leave `promote-base-latest`
out: it legitimately skips on a base cache hit, which would misclassify a good
run. `update-description` is the cosmetic post-publish job.
## Cache-hit footgun (must-know)
`PI_VERSION` defaults to `latest` in `Dockerfile.variant` but **CI must
+40
View File
@@ -11,6 +11,46 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## Unreleased
Staged, not yet tagged — more changes are queued for the next release before CI
is kicked off.
### Changed
- **mempalace pin `3.5.0``3.6.0`** (`Dockerfile.base` `MEMPALACE_VERSION`),
in lockstep with opencode-devbox v2.9.0 as the pin's own comment requires.
3.6.0 (2026-07-17) is PyPI latest and is additive/reliability only — secure
`mempalace serve` remote mode, optional Milvus backend, atomic KG
`supersede()`, conversation chronology, mining exclusions, plus recovery and
locking fixes. **Reviewed for MCP tool-schema changes before bumping** — that
being the exact regression class this pin exists to catch, after an unpinned
install once swept in the broken 3.3.x/3.4.0 `diary_write` schema: there are
**none**, and nothing touches `diary_write`, so the perl workaround removed in
v1.2.2 stays removed. Two fixes are directly relevant to how this image uses
mempalace: read-only mode now covers `checkpoint` + `delete_by_source` in
`_MUTATING_TOOLS` (#1930), and agent attribution is preserved in
`mempalace_checkpoint` (#2023/#2034) — the latter matters because the diary
protocol relies on per-agent attribution. Rebuilds the base image.
### Documentation
- Documents **per-variant image description labels** (committed and pushed after
the v1.6.4 tag without a changelog entry). Both published variants used to
inherit `Dockerfile.base`'s `description="pi-devbox — base image
(variant-independent)"`, so `v1.6.4` and `v1.6.4-studio` both advertised
themselves on Docker Hub as *the base image* — misleading, and useless for
telling the two apart. Since a `LABEL` cannot branch on `INSTALL_STUDIO`, the
text now arrives as a build arg: CI passes a variant-specific string
(interpolating `RELEASE_TAG`, `PI_VERSION`, and `STUDIO_TAG` for studio), while
the `Dockerfile.variant` default keeps a bare local `docker build` honest
rather than misleading. Sets `org.opencontainers.image.title`/`.description`
alongside the legacy bare `description` key so both Hub and OCI-aware tooling
see it. The ARGs stay in the last-declared block, so the label layer remains
the only thing invalidated.
---
## v1.6.4 — 2026-07-30
Patch release. Headline: **the `fork` tool has never once loaded since v1.0.0**
+15 -4
View File
@@ -367,13 +367,24 @@ ARG INSTALL_MEMPALACE=true
# diary_write schema. Pinning makes mempalace upgrades a reviewable diff
# rather than a surprise.
#
# 3.5.0 (2026-06) ships the upstream fix for the top-level-anyOf diary_write
# 3.5.0 (2026-06) shipped the upstream fix for the top-level-anyOf diary_write
# schema (issue #1728 / PR #1717, merged 2026-06-14): the advertised schema
# is now `"required": ["agent_name"]` with entry/content enforced at dispatch,
# which Anthropic's tools API accepts — so the old mcp_server.py perl
# workaround that used to live below is gone. Keep in lockstep with
# opencode-devbox when bumping.
ARG MEMPALACE_VERSION=3.5.0
# workaround that used to live below is gone.
#
# 3.6.0 (2026-07-17, PyPI latest) is additive/reliability only — secure
# `mempalace serve` remote mode, optional Milvus backend, atomic KG
# supersede(), conversation chronology, mining exclusions, plus recovery and
# locking fixes. Reviewed for MCP tool-schema changes before bumping (that
# being the exact regression class this pin exists to catch): there are NONE,
# and nothing touches diary_write. Two fixes matter for how this image uses
# mempalace: read-only mode now covers checkpoint + delete_by_source in
# _MUTATING_TOOLS (#1930), and agent attribution is preserved in
# mempalace_checkpoint (#2023/#2034).
#
# Keep in lockstep with opencode-devbox when bumping.
ARG MEMPALACE_VERSION=3.6.0
ENV UV_TOOL_DIR=/opt/uv-tools
ENV UV_TOOL_BIN_DIR=/usr/local/bin
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
+11
View File
@@ -219,10 +219,21 @@ 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
# Dockerfile.base sets description="pi-devbox — base image (variant-independent)"
# and every variant INHERITS it, so both published images used to advertise
# themselves on Docker Hub as the base image. A LABEL cannot branch on
# INSTALL_STUDIO, so the description arrives as a build-arg: CI passes the
# variant-specific string (see docker-publish.yml), and the default below keeps
# a plain `docker build -f Dockerfile.variant` honest rather than misleading.
ARG IMAGE_TITLE="pi-devbox"
ARG IMAGE_DESCRIPTION="pi-devbox — development container for the pi coding agent"
LABEL org.opencontainers.image.version="${RELEASE_TAG}" \
org.opencontainers.image.revision="${SOURCE_REVISION}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.title="${IMAGE_TITLE}" \
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
description="${IMAGE_DESCRIPTION}" \
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}" \