Compare commits

..

4 Commits

Author SHA1 Message Date
Joakim Persson 56e3742a2d fix(ci): skip the BuildKit check whose warning embedded this Dockerfile in CI output
Lint / hadolint (push) Successful in 10s
Lint / skill-floor (push) Failing after 13s
Lint / doc-drift (push) Successful in 18s
Lint / actionlint (push) Successful in 33s
Publish Docker Image / lint-gate (push) Successful in 32s
Publish Docker Image / resolve-versions (push) Successful in 17s
Publish Docker Image / base-decide (push) Successful in 13s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke-studio (push) Successful in 5m38s
Publish Docker Image / smoke (push) Successful in 8m20s
Publish Docker Image / build-variant-studio (push) Successful in 20m34s
Publish Docker Image / build-variant (push) Successful in 21m5s
Publish Docker Image / update-description (push) Successful in 8s
Publish Docker Image / promote-base-latest (push) Successful in 9s
v1.10.1 (run 707) failed with ZERO failing steps: every step Success, `smoke`
printing "101 passed, 0 failed", `smoke-studio` "104 passed, 0 failed", both
jobs red. The clipboard fix from f3b3748 worked exactly as predicted; this is a
second, independent defect that run 704 had masked.

CHAIN, measured end to end rather than reasoned:

`ARG BASE_IMAGE` has no default on purpose (the two-phase build always supplies
it), which trips BuildKit's InvalidDefaultArgInFrom check. buildx attaches that
warning's source context to the build metadata as
buildx.build.warnings[].sourceInfo.data — the ENTIRE Dockerfile, base64, on ONE
line. docker/build-push-action writes that metadata to $GITHUB_OUTPUT as a
`name<<ghadelimiter_<uuid>` heredoc. Gitea's act_runner truncates any single
line at exactly 65536 chars, so the closing delimiter was cut off:

  invalid format delimiter 'ghadelimiter_...' not found before end of file

and the runner failed the job while attributing it to no step at all.

  v1.9.4  run 695 SUCCESS: longest metadata line 56355, 0 delimiter errors
  v1.10.0 run 704 failed : longest metadata line 65536, 1 delimiter error
  v1.10.1 run 707 failed : longest metadata line 65536, 1 delimiter error

65536 = 2^16: cut AT the cap, not merely long. Independent route via file size
rather than log parsing: 42251 B at v1.9.4 (base64 56335, matching the log) vs
50034 B now (base64 66712, truncated). The cap corresponds to a 49152 B
Dockerfile, so this cycle's comment growth crossed it by 882 B.

Located by asking which top-level metadata key CONTAINS the base64, instead of
assuming: it is buildx.build.warnings -> sourceInfo -> data in BOTH runs.

THIS IS THE SECOND FIX FOR THIS BUG. The first, `provenance: false` on the two
smoke build steps, was committed as 784fad7 and is REVERTED here: a real buildx
on another host showed provenance metadata present in both modes with a longest
string of 71 chars, i.e. the base64 was never in provenance. Shipping it would
have left the release broken a third time while looking like a fix.

Also rejected, each measured: floating action tags moving (act action-bundle
hashes byte-identical between runs 695 and 707, all four) and the build-check
annotation text changing (byte-identical).

FIX: `# check=skip=InvalidDefaultArgInFrom` as the FIRST line of
Dockerfile.variant — BuildKit parses `# check=` only before any other line, so
placement is load-bearing. No honest default exists for BASE_IMAGE: `scratch`
would satisfy the linter while being a lie, and would convert today's instant
"invalid reference format" into a failure deep in the build. Verified against
the actual edited file with `docker buildx build --check`: "Check complete, no
warnings found." Zero warnings means no sourceInfo, so the longest metadata line
drops 65536 -> ~1936 and the file's SIZE stops gating CI.

GUARD: scripts/check-dockerfile-directives.sh, wired into BOTH lint.yml and
docker-publish.yml's lint-gate, because a check that gates only `push` lets a
tag regress — the adoption slip that let doc-drift land 27 h after the v1.9.3
tag. It also fails if ARG BASE_IMAGE gains a default, so the directive cannot
rot into guarding a check that can no longer fire. Truth table, exit codes:
present 0, removed 1, demoted to line 2 → 1, ARG defaulted 1, file missing 2
(a gate that cannot run must not pass).

Release renamed v1.10.1 -> v1.10.2 with both failed tags left standing as
tombstones. Docs swept again (README "since" marker, Dockerfile decision
comments) because CI reads them from the TAG.

Gates: doc-drift 23 OK / 0 DRIFT; base-hash, workflow-shell, skill-floor,
lint-shell (17 files now), dockerfile-directives all rc=0.
2026-10-02 10:58:03 +02:00
Joakim Persson 784fad78f3 fix(ci): provenance: false on the smoke builds — base64(Dockerfile) crossed 64 KiB
Lint / hadolint (push) Successful in 9s
Lint / skill-floor (push) Successful in 12s
Lint / doc-drift (push) Successful in 18s
Lint / actionlint (push) Successful in 32s
v1.10.1 (run 707) failed with ZERO failing steps: every step reported Success,
`smoke` printed "Results: 101 passed, 0 failed", `smoke-studio` printed "104
passed, 0 failed", and both jobs went red anyway. The clipboard fix worked
exactly as predicted; this is a second, independent defect that run 704 hid.

MECHANISM, measured end to end:

buildx writes a provenance attestation into the metadata it returns, and that
metadata embeds the ENTIRE Dockerfile as a single base64 "data" field on ONE
line. docker/build-push-action writes that metadata to $GITHUB_OUTPUT as a
`name<<ghadelimiter_<uuid>` heredoc. Gitea's act_runner truncates any single
line at exactly 65536 chars, so once base64(Dockerfile.variant) crosses 64 KiB
the closing delimiter is cut off and the runner reports

  invalid format delimiter 'ghadelimiter_...' not found before end of file

then fails the job while attributing it to no step at all.

  v1.9.4  run 695 (SUCCESS): longest metadata line 56355 chars, 0 delimiter errors
  v1.10.0 run 704 (failed) : longest metadata line 65536 chars, 1 delimiter error
  v1.10.1 run 707 (failed) : longest metadata line 65536 chars, 1 delimiter error

65536 is exactly 2^16 — the line was TRUNCATED at the cap, not merely long.
Independent route via file size, not log parsing: Dockerfile.variant was 42251 B
at v1.9.4 (base64 56335, matching the log) and is 50034 B at HEAD (base64 66712,
cut to 65536). The cap corresponds to a 49152 B Dockerfile, so this cycle's
comment growth (+7783 B, mostly comments I wrote) crossed it by 882 B.

Hypotheses measured and REJECTED before landing this:
  - floating action tags moved: the act action-bundle hashes are IDENTICAL
    between run 695 and run 707 (all four), so no action changed version.
  - build-check annotation changed: the ::warning text is byte-identical.
  - stale clipboard assertion again: no, the suites print 0 failed.

WHY provenance: false and not shorter comments — trimming would restore the
margin and silently re-arm the trap for the next comment, with a failure mode of
"job red, no failing step, suite green", which cost most of this session to
diagnose once. Dropping the attestation removes the Dockerfile-size coupling
entirely.

BLAST RADIUS IS NIL for what ships: these two steps build with `load: true` and
the images are discarded after the suite runs, so the attestation has no
consumer. The PUBLISHED images are built by raw `docker buildx build --push` in
run: blocks (three call sites), which never write $GITHUB_OUTPUT metadata and
are therefore both unaffected by the bug and unchanged by this fix.

Verified: YAML parses; check-workflow-shell rc=0; doc-drift 23 OK / 0 DRIFT.
Next step is a `smoke_only=true` dispatch against main — the escape hatch this
workflow already documents — so the fix is proven before another tag is cut.
2026-10-02 10:50:16 +02:00
Joakim Persson c2c42c34a2 docs(v1.10.1): cut v1.10.1; v1.10.0 stays tagged-but-never-published
Lint / skill-floor (push) Successful in 11s
Lint / doc-drift (push) Successful in 12s
Lint / hadolint (push) Successful in 14s
Lint / actionlint (push) Successful in 19s
Publish Docker Image / resolve-versions (push) Successful in 12s
Publish Docker Image / base-decide (push) Successful in 13s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / lint-gate (push) Successful in 25s
Publish Docker Image / smoke-studio (push) Failing after 5m43s
Publish Docker Image / build-variant-studio (push) Has been skipped
Publish Docker Image / smoke (push) Failing after 8m21s
Publish Docker Image / build-variant (push) Has been skipped
Publish Docker Image / promote-base-latest (push) Has been skipped
Publish Docker Image / update-description (push) Has been skipped
v1.10.0's build failed one assertion of 104 and published nothing. Rather than
re-point a tag CI had already consumed, the number moves: a version that failed
its build should stay failed and readable, not be quietly overwritten with
different bytes.

CHANGELOG gains a v1.10.1 section carrying the smoke fix (moved out of
v1.10.0's Fixed list), and states plainly that everything under v1.10.0 ships
here so the big section stays the content record. v1.10.0's heading is marked
"tagged, never published — superseded by v1.10.1" with a blockquote naming the
commit (12f99c4), the run (704), the four jobs that skipped, and the fact that
no image carries the tag.

Pre-tag doc sweep, because CI reads docs from the TAG and POSTs DOCKER_HUB.md
to the Hub as full_description:

  - README "Terminal UI mode: fullscreen is the default (since v1.10.0)"
    -> v1.10.1. A "since" marker pointing at an image nobody can pull is a
    worse lie than no marker.
  - Dockerfile.variant decision comments for the pi and pi-atelier bumps
    -> v1.10.1 (these describe which release carries the change).
  - scripts/smoke-test.sh deliberately KEEPS "the v1.10.0 tag build" — that
    one is a historical event and v1.10.0 is its correct name.
  - DOCKER_HUB.md needed no change: its fullscreen note is version-free.

No code changes. RELEASE_TAG is derived from github.ref_name, so nothing in the
build hardcodes the version.

Base layer does NOT rebuild: no base input (Dockerfile.base, rootfs/**,
entrypoint.sh, entrypoint-user.sh) has changed since 12f99c4, so base-decide
will cache-hit base-dad0f365ff24, which run 704's build-base already pushed at
07:52:38Z. This retry skips the expensive half.

Gates: doc-drift 23 OK / 0 DRIFT / 0 SKIP; base-hash, workflow-shell,
skill-floor, lint-shell all rc=0.
2026-10-02 10:36:21 +02:00
Joakim Persson f3b3748223 fix(smoke): derive the clipboard assertion from what pi declares
Lint / skill-floor (push) Successful in 7s
Lint / actionlint (push) Successful in 17s
Lint / hadolint (push) Successful in 14s
Lint / doc-drift (push) Successful in 14s
v1.10.0's first tag build (run 704) failed on ONE assertion out of 104, and
published nothing. The assertion was wrong, not the image.

scripts/smoke-test.sh required @mariozechner/clipboard to be installed and
require()-able at every install site, failing hard when the family was absent.
pi 0.86.0 (#9163) "Replaced the external native clipboard dependency with
bundled asynchronous macOS, Windows, and X11 helpers while preserving platform
command and OSC 52 fallbacks". Measured in the published tarballs rather than
inferred from the changelog:

  pi 0.85.1 -> dependencies include @mariozechner/clipboard@0.3.9
  pi 0.87.1 -> no @mariozechner/* at all
  pi 1.0.0  -> no @mariozechner/* at all

So on a correct v1.10.0 image the package is legitimately gone, `if [ -z
"$sites" ]; then exit 1` fired, and both smoke jobs went red on the same line:
smoke 100 passed/1 failed, smoke-studio 103 passed/1 failed with every
studio-specific assertion green. build-variant, build-variant-studio,
promote-base-latest and update-description all skipped -> nothing published.
The gate worked; it was enforcing pi 0.85.1's dependency graph.

The guard is NOT deleted, because its "fail when absent" shape is the thing
that stops prune_foreign_natives() from deleting the binding instead of the
surplus platform copies. It now derives the expectation from what the image's
pi actually DECLARES:

  declares + install loads     -> pass
  declares + no install        -> FAIL (prune removed function)
  does not declare + no install-> pass (pi bundles it since 0.86.0)
  does not declare + install   -> FAIL (orphan nothing depends on)

That re-arms automatically if a future pi re-adds the dependency, which a
skip-if-absent would not.

Verified as that four-quadrant truth table on EXIT CODES, not messages, since
run() keys on status: rc=0/1/0/1 as listed. The final check extracted the
command string verbatim from the committed file and ran it through `sh -c` the
way run() does, so the escaping was tested as shipped rather than as drafted.

Gates: doc-drift 23 OK / 0 DRIFT; base-hash, workflow-shell, skill-floor,
lint-shell all rc=0. smoke-test.sh is not a base-hash input, so the base layer
does not rebuild.
2026-10-02 10:27:25 +02:00
7 changed files with 252 additions and 11 deletions
+7
View File
@@ -222,6 +222,13 @@ jobs:
- name: Components the next build would bake differently are named in the CHANGELOG
run: bash scripts/check-doc-drift.sh
# Runs HERE as well as in lint.yml deliberately. A guard that gates only
# `push` lets a tag regress — the adoption slip that let doc-drift land
# 27 h after the v1.9.3 tag. This one protects the tag build from the
# zero-failing-step failure mode that killed v1.10.0 and v1.10.1.
- name: Dockerfile.variant still opens with its BuildKit check directive
run: bash scripts/check-dockerfile-directives.sh Dockerfile.variant
resolve-versions:
# Gated: a defective tree must not reach a 46-minute base build.
needs: [lint-gate]
+6
View File
@@ -90,6 +90,12 @@ jobs:
# actionlint so the more precise diagnostic surfaces first.
run: bash scripts/check-workflow-shell.sh .gitea/workflows
- name: Dockerfile.variant still opens with its BuildKit check directive
# See scripts/check-dockerfile-directives.sh: without that first line the
# smoke jobs fail with `invalid format delimiter 'ghadelimiter_...'` and
# NO failing step. Also wired into docker-publish.yml's lint-gate.
run: bash scripts/check-dockerfile-directives.sh Dockerfile.variant
- name: Install actionlint (pinned)
env:
ACTIONLINT_VERSION: 1.7.12
+110 -1
View File
@@ -11,7 +11,116 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## v1.10.0 — 2026-10-02
## v1.10.2 — 2026-10-02
**v1.10.0 and v1.10.1 were both tagged and never published.** Neither failure
was in the image — both were in the test and CI harness, and each hid the other:
| tag | run | symptom | cause |
|---|---|---|---|
| v1.10.0 | 704 | `smoke` 100 passed / **1 failed**; `smoke-studio` 103 / **1** | a smoke assertion required a pi dependency upstream had deleted |
| v1.10.1 | 707 | `smoke` **101 passed / 0 failed**, `smoke-studio` **104 / 0**, and both jobs red anyway with **no failing step** | base64(`Dockerfile.variant`) in buildx's warning metadata crossed act_runner's 64 KiB line cap |
In both runs every publish job skipped, so no image, no `latest`, and no Hub
description was ever written — the gates did their job twice. **Everything
described under v1.10.0 below ships here**; that section remains the content
record and nothing in it changed.
The tag number moves each time rather than being re-pointed, because CI had
already consumed the previous one. A version that failed its build should stay
failed and readable, not be quietly overwritten with different bytes.
### Fixed
- **A job can fail with every step green, a smoke suite reporting `0 failed`,
and no failing step anywhere — and it cost two tags** — `Dockerfile.variant`
now opens with `# check=skip=InvalidDefaultArgInFrom`, and
`scripts/check-dockerfile-directives.sh` keeps it there.
The chain, measured rather than reasoned: `ARG BASE_IMAGE` deliberately has no
default (the two-phase build always supplies it), which trips BuildKit's
`InvalidDefaultArgInFrom` check. buildx attaches that warning's source context
to the build metadata as `buildx.build.warnings[].sourceInfo.data` — **the
entire Dockerfile, base64, on one line**. `docker/build-push-action` writes
that metadata to `$GITHUB_OUTPUT` as a `name<<ghadelimiter_<uuid>` heredoc,
and Gitea's act_runner truncates any single line at exactly 65536 chars. Once
base64 of the file passed 64 KiB the closing delimiter was cut off and the
runner reported `invalid format delimiter 'ghadelimiter_...' not found before
end of file`, then failed the job while attributing it to nothing.
Numbers: the file was 42251 B at v1.9.4 → base64 56355 chars, fine; it is
50034 B now → base64 66712, truncated to exactly 65536 (2^16, i.e. cut *at*
the cap, not merely long). The cap corresponds to a 49152 B Dockerfile, so
this release's comment growth — much of it comments added while documenting
the v1.10.0 round — crossed it by **882 bytes**. Confirmed two ways: by
parsing the metadata out of both job logs, and independently by arithmetic on
the file size at each tag. Verified fixed by running `docker buildx build
--check` against the actual edited file: *"Check complete, no warnings
found."* With zero warnings there is no `sourceInfo`, so the longest metadata
line drops from 65536 to ~1936 chars and the Dockerfile's **size stops being
coupled to whether CI passes**.
Hypotheses measured and rejected on the way, each of which would have produced
a wrong fix: floating action tags moving (the act action-bundle hashes are
byte-identical between the green v1.9.4 run and the red one — all four); the
build-check annotation text changing (byte-identical); and `provenance: false`,
which was written, tested against a real buildx, and **reverted** when the
metadata turned out to carry the base64 under `buildx.build.warnings`, not
under provenance. The published images are unaffected either way: they are
built by raw `docker buildx build --push`, which never writes
`$GITHUB_OUTPUT` metadata.
The new guard runs in **both** `lint.yml` and the publish workflow's
`lint-gate`, because a check that gates only `push` lets a tag regress — the
same adoption slip that let doc-drift land 27 h after the v1.9.3 tag. It
also fails if `ARG BASE_IMAGE` ever gains a default, so the skip directive
cannot rot into pointing at a check that can no longer fire. Truth table:
directive present → rc=0; removed → rc=1; demoted to line 2 → rc=1; ARG given
a default → rc=1; file missing → rc=2 (cannot-run must not pass).
- **The smoke suite asserted a pi dependency that upstream deleted, and it cost
this release its first tag build** — `scripts/smoke-test.sh` required
`@mariozechner/clipboard` to be installed and `require()`-able at every install
site. pi **0.86.0** (`#9163`) *"Replaced the external native clipboard
dependency with bundled asynchronous macOS, Windows, and X11 helpers while
preserving platform command and OSC 52 fallbacks"*. Measured in the published
tarballs: pi `0.85.1` declares `@mariozechner/clipboard@0.3.9`; `0.87.1` and
`1.0.0` declare no `@mariozechner/*` at all. So on a correct v1.10.0 image the
package is legitimately absent, the assertion's `if [ -z "$sites" ]; then exit
1` fired, and run 704 ended **100 passed / 1 failed** on `smoke` and **103
passed / 1 failed** on `smoke-studio` — same single assertion, every
studio-specific check green. `build-variant`, `build-variant-studio`,
`promote-base-latest` and `update-description` were all skipped, so **nothing
was published**: the gate behaved exactly as designed, against a stale
expectation rather than a real defect.
The fix does not delete the guard, because the guard was right: "fail when the
family is absent" is what stops `prune_foreign_natives()` from silently
deleting the binding instead of the surplus platform copies. It now **derives
its expectation from what the image's pi actually declares** — if pi declares
the dependency an install must exist and load; if pi does not, no install may
linger. That re-arms by itself should a future pi re-add it, and it still
catches an orphaned install. Verified as a four-quadrant truth table with the
command string extracted verbatim from the file and run through `sh -c` the way
`run()` invokes it: declared+present → rc=0, declared+absent → rc=1,
undeclared+absent → rc=0, undeclared+present → rc=1.
Worth stating plainly, since it is the whole value of the round: pi 1.0.0,
pi-atelier v0.13.0 and pi-studio v0.9.61 passed **103 of 104** assertions on a
pi MAJOR bump. The one red was the suite describing pi 0.85.1's dependency
graph, not the image.
---
## v1.10.0 — 2026-10-02 (tagged, never published — content shipped as v1.10.2)
> Tagged 2026-10-02 at commit `12f99c4`. Its build (run 704) went red on the
> stale clipboard assertion described under v1.10.1, so `build-variant`,
> `build-variant-studio`, `promote-base-latest` and `update-description` all
> skipped and nothing reached the Hub. No image carries this tag. The content
> below is accurate and shipped as **v1.10.2**. v1.10.1 (commit `c2c42c3`,
> run 707) was the first attempt at republishing it and died on the separate
> act_runner line-cap bug described under v1.10.2.
Three small fixes found by the v1.9.4 first-boot acceptance and the CI base
hash prediction, plus one boot-time wiring fix that stops a recurring `git push`
+37 -3
View File
@@ -1,3 +1,37 @@
# check=skip=InvalidDefaultArgInFrom
#
# ^ MUST stay the FIRST line of this file, and it is load-bearing for CI, not
# style. BuildKit parses `# check=` only before any other line, so moving it
# below the title comment silently disables it.
#
# Why it exists: `ARG BASE_IMAGE` (below) deliberately has NO default — this
# file is only ever built by the two-phase CI with --build-arg BASE_IMAGE=
# <image>:base-<hash>. BuildKit's InvalidDefaultArgInFrom check flags that as
# "default value for global ARG results in an empty or invalid base image
# name". There is no honest default to give it: `scratch` would satisfy the
# linter while being a lie (nothing here can build FROM scratch), and it would
# convert today's instant "invalid reference format" into a failure deep in the
# build. So the check is skipped by name, with the reason written down.
#
# What the warning actually COST, which is why this is not cosmetic: buildx
# attaches the warning's source context to the build metadata as
# buildx.build.warnings[].sourceInfo.data — the ENTIRE Dockerfile, base64, on
# ONE line. docker/build-push-action writes that metadata to $GITHUB_OUTPUT as
# a `name<<ghadelimiter_<uuid>` heredoc, and Gitea's act_runner truncates any
# single line at exactly 65536 chars. Once base64(this file) crossed 64 KiB the
# closing delimiter was cut off, and the runner failed the job with
# invalid format delimiter 'ghadelimiter_...' not found before end of file
# and NO failing step: every step green, smoke suite "0 failed", job red.
# Measured: 42251 B at v1.9.4 -> base64 56355 (fine); 50034 B at v1.10.1 ->
# base64 66712, truncated to exactly 65536. The cap corresponds to a 49152 B
# Dockerfile, so this release's comment growth crossed it by 882 B. It killed
# v1.10.0 (run 704, where a stale clipboard assertion hid it) and v1.10.1
# (run 707). With zero warnings there is no sourceInfo, so the longest metadata
# line drops from 65536 to ~1936 chars and the Dockerfile's SIZE stops being
# coupled to CI passing at all.
#
# If this ever recurs — job red, zero failing steps, suite reporting 0 failed —
# grep the job log for `ghadelimiter` first.
# pi-devbox — variant image
#
# FROMs a base-<hash> image produced by Dockerfile.base and adds only
@@ -154,11 +188,11 @@ ARG USER_NAME=developer
# (e7d77dc -> 1529e14 -> 731c3d4 in the nine days to 2026-10-01), so waiting for
# a tag means holding pi indefinitely. A full SHA keeps the one property
# `master` does not have: rebuilding this tag later produces the SAME image.
# v1.9.5 SKIPPED 0.99.x and 1.0.0 for lack of evidence. v1.10.0 went and got the
# v1.9.5 SKIPPED 0.99.x and 1.0.0 for lack of evidence. v1.10.2 went and got the
# evidence instead of waiting for obsmem to mention a version, because "no issue
# names 1.0" is an absence of a statement, not a measurement.
#
# v1.10.0: 0.87.1 -> 1.0.0. MEASURED 2026-10-02 against the published npm
# v1.10.2: 0.87.1 -> 1.0.0. MEASURED 2026-10-02 against the published npm
# tarballs for 0.87.1 and 1.0.0, unpacked side by side:
# 1. NO `### Breaking Changes` SECTION IN 1.0.0 AT ALL. The changelog's
# breaking sections belong to 0.87.0, 0.86.0, 0.84.3, 0.84.0, 0.83.0,
@@ -273,7 +307,7 @@ ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
# pi >=0.84.0, so it still spans the pinned 0.85.1. 13 commits v0.10.1..v0.10.3,
# all under src/ tests/ docs/ scripts/ plus metadata; no entry-point move.
#
# v1.10.0: v0.10.3 -> v0.13.0, closing the two-minor gap v1.9.5 flagged as the
# v1.10.2: v0.10.3 -> v0.13.0, closing the two-minor gap v1.9.5 flagged as the
# residual risk of the pi bump. peerDependencies are UNCHANGED at pi >=0.84.0
# across v0.10.3, v0.12.1 and v0.13.0 — still a FLOOR, so still not evidence of
# anything; the floor is satisfied by 1.0.0 either way. What was actually
+1 -1
View File
@@ -358,7 +358,7 @@ DOT syntax errors instead of crashing. Then in Studio: open the PNG (or a
`.md` that embeds it) and hit **refresh-from-disk** after each edit.
Note: SVG is **not** in Studio's local-image-link allowlist — use PNG.
## Terminal UI mode: fullscreen is the default (since v1.10.0)
## Terminal UI mode: fullscreen is the default (since v1.10.2)
pi **1.0.0** changed the default terminal UI mode to **fullscreen**, and this
image adopts upstream's default rather than overriding it. Fullscreen draws into
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Guard: Dockerfile.variant must OPEN with its BuildKit `# check=` directive.
#
# Why this is a gate and not a comment. BuildKit parses `# check=` ONLY before
# any other line in the file, so moving it below the title comment — or dropping
# it during an unrelated header edit — silently re-enables the
# InvalidDefaultArgInFrom warning on `ARG BASE_IMAGE` / `FROM ${BASE_IMAGE}`.
#
# A re-enabled warning is not cosmetic. buildx attaches the warning's source
# context to the build metadata as buildx.build.warnings[].sourceInfo.data: the
# entire Dockerfile, base64, on ONE line. docker/build-push-action writes that
# metadata to $GITHUB_OUTPUT as a `name<<ghadelimiter_<uuid>` heredoc, and
# Gitea's act_runner truncates any single line at exactly 65536 chars. Since
# base64(Dockerfile.variant) passed 64 KiB (50034 B source -> 66712 chars, cut
# to 65536), the closing delimiter is lost and act_runner fails the job with
# invalid format delimiter 'ghadelimiter_...' not found before end of file
# while attributing it to NO step: every step reports Success, the smoke suite
# prints "0 failed", and the job is red regardless. That cost two tags —
# v1.10.0 (run 704, where a stale clipboard assertion masked it) and v1.10.1
# (run 707) — and most of a session to localise.
#
# So: one deterministic grep, run both on push AND in the publish workflow's
# lint-gate. A check that gates only `push` lets a tag regress — the same
# adoption slip that let doc-drift land 27 h after the v1.9.3 tag.
#
# Exit codes: 0 OK, 1 violation, 2 cannot-run (missing file) — matching
# lint-shell.sh / check-skill-floor.sh / check-doc-drift.sh, because a gate that
# cannot run must not pass.
set -euo pipefail
DF="${1:-Dockerfile.variant}"
EXPECTED='# check=skip=InvalidDefaultArgInFrom'
if [ ! -f "$DF" ]; then
echo "::error::check-dockerfile-directives: '$DF' not found (cannot-run)" >&2
exit 2
fi
first="$(head -n 1 "$DF")"
if [ "$first" != "$EXPECTED" ]; then
{
echo "::error::$DF line 1 must be exactly: $EXPECTED"
echo "::error::found instead: ${first:-<empty>}"
echo "::error::"
echo "::error::BuildKit only honours '# check=' before any other line. Without it the"
echo "::error::InvalidDefaultArgInFrom warning returns, buildx embeds this whole file as"
echo "::error::base64 in buildx.build.warnings[].sourceInfo.data, that single line exceeds"
echo "::error::act_runner's 65536-char cap, and the smoke jobs fail with"
echo "::error:: invalid format delimiter 'ghadelimiter_...' not found before end of file"
echo "::error::and NO failing step. See the header of $DF for the full measurement."
} >&2
exit 1
fi
# Second, independent assertion: the condition the directive exists FOR. If a
# later edit gives ARG BASE_IMAGE a default, the directive becomes dead weight
# and should be removed deliberately rather than left to rot — and if the ARG is
# renamed, this guard would otherwise keep passing while guarding nothing.
if ! grep -qE '^ARG BASE_IMAGE$' "$DF"; then
{
echo "::error::$DF no longer contains a bare 'ARG BASE_IMAGE' (no default)."
echo "::error::That is the only thing '$EXPECTED' suppresses. Either restore the bare ARG"
echo "::error::or drop the directive and this guard together — do not leave a skip"
echo "::error::directive pointing at a check that can no longer fire."
} >&2
exit 1
fi
echo "Dockerfile directive guard OK — $DF opens with the check-skip directive and still declares a bare ARG BASE_IMAGE."
+22 -6
View File
@@ -30,7 +30,7 @@
# client bundle present + registered via `pi install`
# - no foreign npm-11 platform packages (@esbuild, clipboard) beyond the host
# - no build-time npm cache (/root/.npm) shipped in the image
# - esbuild compiles + clipboard native loads at every install site
# - esbuild compiles everywhere; clipboard present iff pi declares it
# - image size within threshold
set -euo pipefail
@@ -986,11 +986,27 @@ run "no build-time npm cache shipped (/root/.npm)" \
run "esbuild works at every install site (prune removed weight, not function)" \
'sites=$(find /usr/lib/node_modules /opt -type d -path "*/node_modules/esbuild" -prune 2>/dev/null); if [ -z "$sites" ]; then echo "no esbuild install found at all" >&2; exit 1; fi; for d in $sites; do node -e "require(\"$d\").transformSync(\"const x:number=1\",{loader:\"ts\"})" || { echo "esbuild broken at $d" >&2; exit 1; }; done; echo ok'
# Clipboard is the family pruned second, and its napi-rs loader picks its native
# binding at require() time — so a successful load IS the proof that the kept
# platform package is the one this image needs.
run "clipboard native loads at every install site" \
'sites=$(find /usr/lib/node_modules /opt -type d -path "*/node_modules/@mariozechner/clipboard" -prune 2>/dev/null); if [ -z "$sites" ]; then echo "no @mariozechner/clipboard install found at all" >&2; exit 1; fi; for d in $sites; do node -e "var c=require(\"$d\"); if (typeof c.setText !== \"function\") { throw new Error(\"native binding missing\"); }" || { echo "clipboard native broken at $d" >&2; exit 1; }; done; echo ok'
# Clipboard is the family pruned second. Upstream pi 0.86.0 (#9163) REPLACED the
# external `@mariozechner/clipboard` dependency with bundled macOS/Windows/X11
# helpers plus the OSC 52 fallback, so from 0.86.0 on there is nothing external to
# prune and nothing to require(). pi 0.85.1 declared it; 0.87.1 and 1.0.0 do not.
# This assertion therefore derives its expectation from what the image's pi
# actually DECLARES rather than hardcoding either state: it re-arms by itself if a
# future pi re-adds the dependency, and it still fails if an install lingers that
# nothing depends on. Hardcoding "must exist" is what failed the v1.10.0 tag build
# (100 passed, 1 failed, nothing published) against a perfectly correct image.
run "clipboard native: present iff pi declares it (prune removed weight, not function)" \
'PKG=/usr/lib/node_modules/@earendil-works/pi-coding-agent/package.json;
if grep -q "\"@mariozechner/clipboard\"" "$PKG" 2>/dev/null; then declared=yes; else declared=no; fi;
sites=$(find /usr/lib/node_modules /opt -type d -path "*/node_modules/@mariozechner/clipboard" -prune 2>/dev/null);
if [ "$declared" = yes ]; then
[ -n "$sites" ] || { echo "pi declares @mariozechner/clipboard but NO install found - prune removed function" >&2; exit 1; };
for d in $sites; do node -e "var c=require(\"$d\"); if (typeof c.setText !== \"function\") { throw new Error(\"native binding missing\"); }" || { echo "clipboard native broken at $d" >&2; exit 1; }; done;
echo "ok: pi declares it and the native binding loads at every site";
else
[ -z "$sites" ] || { echo "pi no longer declares @mariozechner/clipboard yet installs remain: $sites" >&2; exit 1; };
echo "ok: pi bundles clipboard since upstream 0.86.0 (#9163); no external install expected";
fi'
# ── Image size ────────────────────────────────────────────────────────
echo ""