Upstream omaclaren/pi-studio stopped publishing GitHub Releases at v0.5.55
but keeps tagging every version (v0.9.36 now) and pushing to main. Tracking
main HEAD risked baking half-finished commits that land after a tag.
resolve-versions now lists all tags via a single git ls-remote (the REST
tags API paginates at 100 and the repo has >140 tags, so a single page can
miss the newest), picks the highest X.Y.Z with sort -V (pre-releases
excluded), and peels it to a commit SHA for PI_STUDIO_REF. SHA (not moving
tag) keeps cache-busting + reproducibility; require_sha still enforced.
Studio-variant only, not base-affecting. No change to the resolved commit
today (v0.9.36 == current main HEAD). Validated: yq parse OK, bash -n OK,
live git ls-remote -> v0.9.36 -> 2ef38ef. No tag.
Vanilla Neovim fell back to a 256-colour palette over ssh/kitty and rendered
strings/comments in a muddy, low-contrast colour. Fix, all base-affecting:
- rootfs/etc/xdg/nvim/sysinit.vim: system vimrc enabling termguicolors. Loads
for every user before any personal ~/.config/nvim; overridable per-user.
- Dockerfile.base: install kitty-terminfo (TERM=xterm-kitty understood; lets
Neovim auto-detect true colour) and set ENV COLORTERM=truecolor.
- smoke-test.sh: assert xterm-kitty terminfo present and nvim tgc default on.
- CHANGELOG (Unreleased/Added) + README editor note.
No tag — lands on the next base-<hash> rebuild.
Claude Code's per-machine local settings can carry credentials and should never
be committed. Add the pattern to the seeded ~/.gitignore_global so fresh
containers match a host global that already ignores it. Existing containers are
unaffected (seed copied only when absent). Base-affecting; rebuilds base-<hash>.
pandoc's bundled typst template defaults the document font to an empty
tuple (font: ()), so a naked `pandoc --pdf-engine=typst` fails with
"font fallback list must not be empty". Patch the template default to
Libertinus Serif (typst's own bundled default) at build time so PDF
export works out of the box. Document usage in README and note the fix
in CHANGELOG (Unreleased). Base-affecting.
Finalize the Unreleased batch as v1.4.0 (minor — significant base
additions). Base rebuilds (Dockerfile.base for typst/xz-utils;
.bash_aliases for the SSH check). pi auto-resolves latest (0.80.3 ->
0.80.6); mempalace stays 3.5.0 (current).
pandoc has been in the base since v1.0.0 but only as a front-end; PDF
export (studio_export_pdf / pandoc -o out.pdf) failed with 'xelatex not
found' because no back-end engine was installed. Ship typst (~30 MB
static Rust binary, no LaTeX) as the default engine via
`pandoc --pdf-engine=typst`, chosen over a ~600 MB TeX Live install.
texlive-xetex remains the higher-fidelity install-on-demand fallback.
- Dockerfile.base: install typst (latest GitHub-release idiom, pin via
TYPST_VERSION); add xz-utils (typst ships .tar.xz); bump
BASE_REBUILD_DATE. Lands in base-<hash>.
- smoke-test.sh: verify typst + a real pandoc --pdf-engine=typst render.
- README/AGENTS/CHANGELOG: typst now shipped (supersedes the planned
:latest-studio-tex variant).
No tag pushed — CI build intentionally deferred.
Adds a _devbox_check_host_ssh() check to ~/.bash_aliases (baked into
the image). On first bash session of each container it tries a quick
SSH probe to the Mac host; if it fails it prints a clear one-time
warning with the exact two steps needed to fix it:
1. Enable Remote Login in macOS System Settings
2. echo '<public key>' >> ~/.ssh/authorized_keys
The check is guarded:
- only runs inside a container (/.dockerenv)
- only when the jump key exists (~/.ssh-local/devbox_jump_ed25519.pub)
- only once per container lifetime (/tmp flag, cleared on recreate)
After --force-recreate the key changes, the flag is gone, and the
check runs again on the first bash window. Subsequent windows are
silent.
PDF export from Studio/pandoc still isn't shipped. Record the engine decision
in the living docs (README + AGENTS): pandoc is in the image but has no PDF
back-end, so export fails with 'xelatex not found'. Prefer a lightweight engine
— typst (~30 MB static binary, 'pandoc --pdf-engine=typst') which is small
enough it could ship in base rather than needing a separate ':latest-studio-tex'
variant; texlive-xetex (~600 MB) kept as the higher-fidelity fallback. Also drop
stale 'v1.3.0' pins (v1.3.0 already shipped without PDF) in favour of 'a future
release'. CHANGELOG history left untouched.
Wire the shared-palace option (implemented in mempalace-toolkit's mempalace.ts)
into the container:
- .env.example: document MEMPALACE_REMOTE_URL / MEMPALACE_REMOTE_TOKEN (env_file-only,
per this repo's convention).
- docker-compose.mempalace.yml: optional shared server (mempalace-mcp --transport http),
loopback-bound by default.
- docker-compose.yml: local-vs-external note on the palace-volume comment.
- README + CHANGELOG (Unreleased).
The image shipped only nvim (EDITOR=nvim), a modal vi-style editor. Not
everyone is comfortable with vi keybindings, so add both a classic and a
modern non-modal option:
- nano (apt): ~2.8 MB installed; deps (libc6, libncursesw6, libtinfo6)
already present via nvim/less/htop/tmux, so no extra packages pulled in.
- micro: ~12 MB single static Go binary from GitHub releases (same pattern
as bat/eza/zoxide). Desktop-style keys (Ctrl+S/Ctrl+Q), mouse, syntax
highlighting. ARG MICRO_VERSION pins; defaults to latest.
Combined ~15 MB (<0.5% of the ~3.2 GB image). EDITOR stays nvim; both new
editors are opt-in (export EDITOR=micro | nano). Uses the canonical
micro-editor/micro URL because the old zyedidia/micro org rename makes
/releases/latest redirect to another /latest, defeating the tag-parsing
latest-resolution idiom.
Base-image change, so it lands on the next base-<hash> rebuild. Updates
README tool table + EDITOR note, CHANGELOG (Unreleased/Added), and
smoke-test.sh (nano + micro presence checks).
actionlint's no-arg project auto-detection looks for .github/workflows
and hard-fails (exit 3, 'no project was found') on this .gitea/workflows
layout — observed on run 420. Glob the workflow files explicitly. The
Gitea shell guard step already passed in that run; only the actionlint
invocation needed the path fix.
Root cause of the recurring 'Illegal option -o pipefail' failures
(ed49b8d resolve-versions; b7197e8 promote-base-latest, run 418):
docker-publish.yml had no workflow-level default shell, so Gitea's
sh/dash default applied and every bash-syntax step had to individually
remember 'shell: bash'.
- docker-publish.yml: add 'defaults: run: shell: bash' — fixes the whole
class; all pre-existing dash steps are POSIX so bash runs them unchanged.
- lint.yml: new workflow, runs on every push/PR (not just release tags):
* scripts/check-workflow-shell.sh — Gitea-accurate guard: fails if any
run: step doesn't resolve to bash. Catches the omit-shell+bash-syntax
case that actionlint MISSES (actionlint models GitHub, where the
default shell is bash, so a shell-less step is assumed bash).
* actionlint + shellcheck — catches explicit 'shell: sh' + bash syntax
(SC3040) and general workflow errors.
Verified locally: guard + actionlint pass current workflows; guard fails
a synthetic omit-shell+pipefail workflow; shellcheck clean.
b7197e8 moved the digest-compare into the re-tag step with 'set -euo
pipefail' but no 'shell: bash'; Gitea's default sh (dash) aborts on
-o pipefail, leaving base-latest un-promoted on the v1.2.4 release
(run 418). Same footgun as ed49b8d. Consumer tags unaffected (they
FROM base-<hash>, not base-latest).
Seed ~/.gitignore_global from /etc/skel-devbox (seed-if-absent, like
.bash_aliases/.inputrc, so user edits survive recreate) and wire it via
git config --global core.excludesFile, guarded so a user-set excludesFile
is never overridden. Ignores *.bak, *.bak.*, *~, *.orig, *.swp, *.tmp
across all repos without per-repo .gitignore entries.
Removes GITEA_ACCESS_TOKEN / GITEA_HOST / GITHUB_PERSONAL_ACCESS_TOKEN from
the compose environment: block. An environment: entry both overrides
env_file AND is interpolated from the host shell, so a stale shell export
(e.g. one auto-loaded by an opencode/dotenv hook) silently shadowed the
users .env — an updated token never reached the container. Secrets now flow
solely via env_file: .env; .env.example already documents every variable.
- docker-compose.yml: drop the 3 passthrough lines + explanatory comment
- README.md: sync the "basic shape" snippet
- CHANGELOG.md: note under Unreleased (no tag bump / unpublished)
The gate keyed off need_build=='true', assuming need_build==false meant
base-latest was already current. A dry-run dispatch (promote_latest=false)
that pre-builds base-<hash> falsifies that: the later tag run sees
need_build==false and skipped promotion, leaving base-latest one base
behind (observed 2026-06-27, v1.2.3 dry-run-first release).
Gate now runs on every tag release / promote dispatch; the no-op
optimization moved into the step as a crane digest compare so it re-tags
only when base-latest actually differs from the released base-<hash>.
Workflow-only change; base hash unaffected (no base rebuild).
Patch release. Headline: mempalace-mcp self-heals instead of latching
available=false permanently after a slow virtiofs cold-open. Base image
rebuilds via the mempalace-toolkit ref advancing to e12b624 (folded into
the base-decide hash). No pi/mempalace version change — pi npm latest is
still 0.80.2 (= v1.2.2). Also releases the queued yq (mikefarah Go yq) and
mempalace-skill temporal-grounding changes.
mempalace.ts now self-heals (respawn with capped backoff) instead of
latching unavailable, and the init-timeout default is 300000. Update the
explanatory comment + tunable list (MEMPALACE_MCP_MAX_RESPAWNS,
MEMPALACE_MCP_RESPAWN_BACKOFF_MS). Comment-only; no build/ENV change.
Baked mempalace SKILL.md now instructs agents to establish current date/time
and compute the delta against the actual diary/drawer timestamp before using
relative terms (yesterday/last week), and explicitly that a container recreate
or fresh session is NOT a day boundary (pi-devbox restarts several times a day).
Phase 1 wake-up section + anti-pattern bullet. CHANGELOG Unreleased.
Match the repo's latest-following convention (tealdeer/uv/etc.) and keep the
container in sync with the Mac's brew yq. smoke-test now asserts mikefarah AND
major v4, so a surprise yq v5 fails CI instead of silently breaking
provision.sh. Pin still available via --build-arg YQ_VERSION=vX.Y.Z.
Debian/Ubuntu `apt install yq` is kislyuk/yq (Python, v3.x), incompatible
with the mikefarah v4 syntax the cloud-init repo's provision.sh/deploy.sh
require. Replace the apt package with a pinned mikefarah Go binary, mirroring
the existing tealdeer ARG (latest-or-pin) pattern, multi-arch amd64/arm64.
smoke-test.sh now asserts `yq --version` reports mikefarah so CI catches a
regression. CHANGELOG: Unreleased entry.
- Bump mempalace pin 3.4.0 -> 3.5.0: 3.5.0 carries the upstream fix for the
top-level-anyOf diary_write schema (issue #1728 / PR #1717, merged
2026-06-14). Verified against the published 3.5.0 wheel that mcp_server.py
now advertises 'required: [agent_name]' with no root-level anyOf.
- Remove the Dockerfile.base perl workaround that stripped the anyOf from the
installed mcp_server.py — obsolete now the fix is upstream.
- pi auto-resolves to npm latest (0.79.10 -> 0.80.2) at build.
- CHANGELOG v1.2.2.
Bake pi-extensions + mempalace skills into the image (available without a
mounted skillset) and add the mempalace session-start proactive-load directive
so frequently-recreated containers actually pick the skill up. Closes the
fork/recall + mempalace under-utilisation gap.
CHANGELOG: [Unreleased] -> v1.2.1.
Baking the mempalace fallback skill fixed *availability*, but mempalace had
no proactive-load directive anywhere (pi-toolkit's global AGENTS.md only
points to pi-extensions), so a new container would still surface it only via
description-matching — the same under-utilisation the pi-extensions directive
was created to fix.
Add a session-start pointer to the pi-devbox managed AGENTS.md block
(pi-global-AGENTS.append.md): gated to pi-devbox containers and conditional on
the MemPalace MCP tools being present. Memory continuity matters most in a
frequently-recreated container — the palace is its only cross-recreate memory.
- pi-global-AGENTS.append.md: '## Session start: load the mempalace skill'.
- smoke-test: assert the pointer merges into the global AGENTS.md at build.
- docs: VENDORED.md, README, CHANGELOG [Unreleased].
Now both skills are complete in pi-devbox: directive + skill file.
pi-extensions = directive (pi-toolkit) + baked skill; mempalace = directive
(this block) + baked skill.
The pi-toolkit global AGENTS.md tells every pi session to read
~/.agents/skills/pi-extensions/SKILL.md at start (the fork/recall
under-utilisation fix), but that skill lived only in the private skillset
repo — so the pointer dangled in any container started without skillset
mounted. Bake fallbacks so the pointer always resolves.
- pi-extensions (Option 1 + Option 2, layered):
* Canonical skill promoted to the public pi-extensions package repo under
skill/ (separate commit there); co-located with the code it documents.
* rootfs/ carries a committed snapshot (the floor).
* Dockerfile.variant copies /opt/pi-extensions/skill/ over the snapshot
after the pinned clone, so a normal build ships the fresh package copy
(recorded via PI_EXTENSIONS_REF) and an old-ref/mirror build still ships
the snapshot. Helper evaluate-extension-usage.py travels with it.
- mempalace (Option 2 only): snapshot in rootfs/. Its consumer skill has no
public package home (mempalace-toolkit ships a different skill,
opencode-mempalace-bridge), so no build-time refresh.
- entrypoint links both (only-when-absent; mounted skillset still wins).
- smoke-test: build-time presence + package-match check + runtime symlink
assertions; readiness gate now waits on the last-linked skill.
- docs: skills/VENDORED.md (provenance + refresh), README, AGENTS.md,
CHANGELOG [Unreleased].
Note: shipped in the NEXT release; v1.2.0 (run 409) predates this.
The runtime 'pi-devbox-environment skill linked' smoke assertion failed in
CI run 408 (gating build-variant). Root cause: the skill-linking block ran
AFTER the pi-toolkit/extensions deploy, but the smoke readiness gate only
waits on pi-deploy markers (keybindings.json, mempalace.ts) — which land
before the skill symlink — so the assertion sampled too early.
- entrypoint-user.sh: move the image-baked-skills symlink loop to run early
(before the pi deploy block), so it completes before any readiness marker.
Still before the skillset deploy, so foreign-link semantics are unchanged.
- smoke-test.sh: add the skill symlink to the readiness gate as well.
Build-time checks (baked skill, append snippet, merged AGENTS marker) all
passed in 408; only the timing of the runtime check was wrong.
Ship skills inside the image (independent of any mounted skillset repo):
- rootfs/usr/local/share/pi-devbox/skills/<name>/ symlinked into
~/.agents/skills/ by entrypoint-user.sh (foreign-link, survives volume
recreate, never clobbers a skillset/user skill of the same name).
- New pi-devbox-environment skill: persistence model, host/LAN SSH
reachability, split-DNS mechanisms, interactive-vs-tool-shell alias
gotcha, tmux 0-index, uv-first Python, pi-studio reachability. Agnostic
to host OS / hostnames / domains / nameservers (discovered at runtime).
- Dockerfile.variant appends pi-global-AGENTS.append.md onto pi-toolkit's
pi-global-AGENTS.md (single global slot) so the skill is loaded
proactively; gated on /usr/local/lib/pi-devbox/. Idempotent.
- smoke-test: baked-skill + append-snippet + merged-marker presence and a
runtime symlink assertion.
- docs: README 'Agent skills' section, AGENTS.md layout, DOCKER_HUB.md;
moved studio-tex roadmap to v1.3.0.
pi 0.79.7 -> 0.79.10 (auto-resolved from npm latest at build).
The host-owned, bind-mounted ~/.config/devbox-shell/ssh-lan.conf is the
intended place to add `ProxyJump host` overrides for named LAN peers (so
`pi --ssh <peer>` / `dssh <peer>` route through the host), but it was only
documented in .env.example and the setup-lan-access.sh header — never in the
README, where someone hitting "can't reach LAN peers" actually looks.
- README: add a "Naming LAN peers" subsection under the macOS LAN-peers
troubleshooting block, with a ProxyJump example and the read-only ~/.ssh
caveat; add a pointer to it from the SSH and ControlMaster section.
- setup-lan-access.sh: correct the INCLUDE_BLOCK comment that suggested adding
ProxyJump to the read-only ~/.ssh/config; point at ssh-lan.conf instead.
- CHANGELOG: note under Unreleased.
Docs/comment only — no behavior change.
The default run shell is 'sh -e {0}' (dash on the act runner), which
rejects 'set -o pipefail' ('Illegal option -o pipefail') — failing the
resolve-versions job on line 2 and cascading every dependent job to
skipped (v1.1.6 run 401). The heavy build steps already declare
'shell: bash'; the resolve step did not. Added it.
Adds OCI labels + /etc/pi-devbox/build-manifest.json so a published tag is
self-describing and reconstructable after CI logs rotate (manifest is
written from the actual checked-out HEAD of each /opt clone + live
pi --version, not just the intended build-args).
Hardens the build plumbing:
- scripts/check-base-hash.sh guards the base-rebuild invariant: every
floating ARG *_REF in Dockerfile.base must be folded into the base_tag
hash, else a ref-only change silently fails to rebuild the base
(v1.1.2-class staleness footgun). Runs in base-decide and locally.
- resolve-versions now fails loud instead of falling back to a floating
main/master on a transient API failure — validates each ref is a 40-hex
SHA (and pi a real semver) and aborts the release otherwise.
- The three gitea companions (pi-toolkit, pi-extensions, mempalace-toolkit)
gained overridable *_REPO build-args (defaulting to the canonical gitea
origin) so a relocated/forked build can repoint them without editing the
Dockerfiles — matching the existing PI_FORK_REPO/PI_OBSMEM_REPO pattern.
README documents the forked/relocated build-arg trick and how to read the
labels + manifest. smoke-test asserts the manifest + labels. pi bumps
0.79.7 → 0.79.8 (auto-resolved at build).
Coordinated with the pi-extensions ssh-controlmaster fix (picked up at build via
PI_EXTENSIONS_REF=main), this makes `pi --ssh <host>` and `dssh`/`dscp` robust
to a user ~/.ssh/config whose per-host ControlPath points under the read-only
~/.ssh bind-mount (e.g. `ControlPath ~/.ssh/cm/%r@%h:%p`). A system default can
never override a user's per-host value, so the fix lives in two layers.
- setup-lan-access.sh: always render the writable ~/.ssh-local/config sidecar
(Host * ControlPath redirect into ~/.ssh-local/cm + Include ~/.ssh/config) on
EVERY host OS. Previously the script exited early (no-op) on native Linux,
leaving dssh/dscp broken when ~/.ssh was read-only there too. The host-jump
block, its key generation, and the authorize hints stay gated on VM-backed
detection / DEVBOX_LAN_ACCESS=jump (new NEED_JUMP flag).
- Dockerfile.base: document that the /etc/ssh drop-in default cannot override a
user per-host ControlPath; cross-ref the two handling layers.
- entrypoint-user.sh: correct the now-stale "no-op on native Linux" comment.
- README.md / DOCKER_HUB.md: document read-only-~/.ssh ControlPath handling.
CHANGELOG: v1.1.5 (Fixed + Changed + pi 0.79.6 -> 0.79.7 auto-resolved bump).
The settings.json bootstrap only fires when the file is ABSENT, so a
settings.json on a preserved named volume never picks up config added in a
later image (e.g. the observational-memory / pi-fork blocks, a newly-enabled
model). Users had to hand-merge after every upgrade.
On start, when settings.json already exists, deep-merge the template into it
with 'jq -s ".[0] * .[1]"' (template first, live second) so the user's values
always win and only MISSING keys are filled from the template. Arrays are
leaves (a model the user removed is not re-added). Rewrites only when the
merge changes something, backs up the original first, and skips safely (no
clobber) if either file is invalid JSON. Opt out with PI_SETTINGS_MERGE=0.
Add a recreate-sanity-check assertion that settings.json carries the
observational-memory + pi-fork blocks after recreate.
The history-flush guard was exported, so it leaked into child processes.
Any nested shell -- crucially each tmux pane (which inherits the tmux
server's env) -- then saw the guard already set and skipped installing
'history -a' in PROMPT_COMMAND. Those shells only persisted history on a
clean exit, so abrupt termination (docker stop, tmux kill-server, SIGKILL)
silently lost their in-memory history. zoxide was less affected (its hook
is installed unguarded and writes immediately).
Make the guard shell-local (drop 'export') so every new interactive shell
re-installs its own per-prompt flush. Add a recreate-sanity-check assertion
that a nested login shell still wires up 'history -a'.
Storage was never the issue: ~/.cache/bash (devbox-shell-history) and
~/.local/share/zoxide (devbox-zoxide) are both persistent named volumes.
pi-toolkit now symlinks pi-global-AGENTS.md -> ~/.pi/agent/AGENTS.md (pi's
global-instructions file, loaded at every start; directs the agent to read
the pi-extensions skill at session start). Add a recreate-sanity-check
assertion alongside the keybindings symlink check so a future image build
that bakes the new pi-toolkit verifies the wiring landed.
GITEA_ACCESS_TOKEN + GITEA_HOST (passed from host .env via compose,
primarily for gitea-mcp) are also usable for any direct Gitea API work —
run inspection, tag checks — not just ci-release-watcher. Prefer over a
PAT file when present; host-managed lifecycle, nothing to revoke. Release
checklist step 7 now notes the env-token alternative.
Patch release per AGENTS.md versioning scheme (pi version bump + smaller
fixes). Cut Unreleased → v1.1.2 (2026-06-15): pi 0.79.3→0.79.4
(CI-auto-resolved), the mempalace-toolkit SHA-resolution build fix,
recreate-sanity-check.sh maintainer tooling, and the AGENTS/README docs.
Bumped the README sanity-check version example to 0.79.4.
Step 3 now runs scripts/recreate-sanity-check.sh inside the running
container after a local recreate — the runtime peer of the build-time
smoke-test.sh gate. Verifies persisted volumes survived and pi wiring
re-deployed, not just that the container booted.
scripts/recreate-sanity-check.sh verifies what is actually live in a
recreated container — persisted volumes, pi runtime wiring (keybindings,
extensions, mempalace.ts bridge, settings.json, fork/obsmem/studio
registrations), /tmp/sshcm, skel defaults, /opt toolkits. smoke-test.sh
runs at build time with --entrypoint="" and cannot see any of this.
Variant (studio/plain) auto-detected via /opt/pi-studio. pi version is
asserted only with --expected-version (built from 'latest', no Dockerfile
pin to self-derive). Maintainer tooling, not baked into the image.
Documented in README and CHANGELOG.
Parity with opencode-devbox: PR #1735 (the diary_write root-anyOf fix) was
closed UNMERGED on 2026-06-11, so the old "remove once PR #1735 ships" TODO
pointed at a dead PR. Issue #1728 is still open; PR #1717 is the current live
candidate; mempalace PyPI latest is still 3.4.0 (== our pin), so the
workaround stays.
- Dockerfile.base: rewrite the upstream-tracking comment + TODO (#1735 dead,
watch #1717, removal trigger = a PyPI release > 3.4.0 stripping root anyOf).
- CHANGELOG: Unreleased Docs entry.
Docs-only; no behavior change.
mempalace-toolkit is the only companion cloned in Dockerfile.base (all
others live in Dockerfile.variant), so it bypassed the resolve-versions ->
build-arg plumbing and its ref stayed a literal `main`. Because the base
only rebuilds on a content hash of Dockerfile.base + rootfs/* + entrypoints,
a toolkit-only fix would silently fail to land unless Dockerfile.base itself
changed (as it incidentally did in v1.1.1).
Changes:
- resolve-versions: new mempalace_toolkit_ref output (gitea commits API,
mirrors pi-toolkit resolution; jq '.[0].sha // "main"' fallback).
- base-decide: needs resolve-versions; fold the resolved SHA into the
base-tag hash so a moved toolkit forces a base rebuild automatically.
- build-base: needs resolve-versions; pass --build-arg MEMPALACE_TOOLKIT_REF.
- Dockerfile.base: switch clone from `git clone --branch` to a SHA-capable
`git fetch <ref> + checkout FETCH_HEAD` (the --branch <SHA> footgun
already fixed in Dockerfile.variant, run 374).
base_tag now reflects a live gitea lookup; on API blip it falls back to
`main`, triggering one extra rebuild, never a missed one.
No new tag — lands on the next v* release or workflow_dispatch.