port pi-devbox v1.1.4–v1.1.6 hardening; bump opencode 1.17.7→1.17.8
Functional (not verbatim) port of the build-provenance, CI-hardening, SSH and shell fixes from the sibling pi-devbox repo, adapted to opencode-devbox's companions and two-variant (base/omos) shape. Defaults unchanged → canonical CI build stays byte-identical apart from the opencode bump and the (cache-free) provenance layer. Fixed: - SSH read-only ~/.ssh ControlPath: setup-lan-access.sh now renders the writable ~/.ssh-local/config sidecar (ControlPath redirect + Include) on EVERY host OS instead of exit 0-ing on native Linux; jump-specific blocks gated behind new NEED_JUMP flag. dssh/dscp + ControlMaster now survive a read-only ~/.ssh on native-Linux hosts. (pi-devbox v1.1.5) - bash history loss in nested/tmux shells: DEVBOX_HIST_SET no longer exported so each shell re-installs its own history -a flush. (pi-devbox v1.1.4) Added: - build provenance: OCI labels + /etc/opencode-devbox/build-manifest.json written from ground truth (opencode --version, installed omos version, /opt/mempalace-toolkit HEAD); wired into build-variant-* and smoke-* jobs; smoke-test.sh asserts manifest + label. (pi-devbox v1.1.6) - scripts/check-base-hash.sh CI guard: fails if a Dockerfile.base ARG *_REF is not folded into the base_tag hash. (pi-devbox v1.1.6) - overridable MEMPALACE_TOOLKIT_REPO build-arg in Dockerfile.base. (v1.1.6) Changed: - resolve-versions: fail-loud validation (SHA / semver) that aborts the release instead of silently falling back to floating main; adds shell: bash (set -o pipefail is illegal under the runner default dash). (pi-devbox v1.1.6) Bumped: - opencode-ai 1.17.7 → 1.17.8 (current npm latest stable). Deferred (needs a decision): opencode.json merge-on-recreate — see CHANGELOG.
This commit is contained in:
@@ -59,6 +59,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: |
|
||||||
@@ -130,14 +133,32 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Resolve omos version from npm registry
|
- name: Resolve omos version from npm registry
|
||||||
id: resolve
|
id: resolve
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -euo pipefail
|
||||||
|
# Fail loud rather than silently shipping a floating ref or a bad
|
||||||
|
# version. 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 the gitea lookup fell
|
||||||
|
# back to `main` via `|| echo`, and the npm lookup had no guard.)
|
||||||
|
# NOTE: shell: bash is REQUIRED — `set -o pipefail` is illegal in
|
||||||
|
# the runner's default dash/sh and aborts the step immediately.
|
||||||
|
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
|
||||||
|
}
|
||||||
# Query the npm registry directly via curl+jq rather than `npm view`.
|
# Query the npm registry directly via curl+jq rather than `npm view`.
|
||||||
# catthehacker/ubuntu:act-latest ships Node/npm under /opt/acttoolcache/
|
# catthehacker/ubuntu:act-latest ships Node/npm under /opt/acttoolcache/
|
||||||
# and adds it to PATH only via /etc/environment — which act_runner never
|
# and adds it to PATH only via /etc/environment — which act_runner never
|
||||||
# sources (it reads the Docker image's ENV instructions, not /etc/environment).
|
# sources (it reads the Docker image's ENV instructions, not /etc/environment).
|
||||||
# curl and jq are both guaranteed present in every job in this workflow.
|
# curl and jq are both guaranteed present in every job in this workflow.
|
||||||
OMOS_VERSION=$(curl -sf "https://registry.npmjs.org/oh-my-opencode-slim/latest" | jq -r '.version')
|
OMOS_VERSION=$(curl -sf "https://registry.npmjs.org/oh-my-opencode-slim/latest" | jq -r '.version' 2>/dev/null || true)
|
||||||
|
if ! printf '%s' "${OMOS_VERSION:-}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||||
|
echo "::error::Could not resolve oh-my-opencode-slim version from npm (got '${OMOS_VERSION:-<empty>}'). Refusing to build with an unresolved version."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "omos_version=${OMOS_VERSION}" >> "$GITHUB_OUTPUT"
|
echo "omos_version=${OMOS_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Resolved OMOS_VERSION=${OMOS_VERSION}"
|
echo "Resolved OMOS_VERSION=${OMOS_VERSION}"
|
||||||
# Resolve mempalace-toolkit main HEAD to a commit SHA. Unlike omos
|
# Resolve mempalace-toolkit main HEAD to a commit SHA. Unlike omos
|
||||||
@@ -150,8 +171,8 @@ jobs:
|
|||||||
# env vars are unset (degrades to anon, still HTTP 200).
|
# env vars are unset (degrades to anon, still HTTP 200).
|
||||||
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
||||||
"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"
|
||||||
echo "Resolved MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}"
|
echo "Resolved MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}"
|
||||||
|
|
||||||
@@ -288,6 +309,8 @@ jobs:
|
|||||||
BASE_IMAGE=${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
BASE_IMAGE=${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
INSTALL_OPENCODE=true
|
INSTALL_OPENCODE=true
|
||||||
INSTALL_OMOS=false
|
INSTALL_OMOS=false
|
||||||
|
RELEASE_TAG=smoke
|
||||||
|
SOURCE_REVISION=${{ github.sha }}
|
||||||
- name: Smoke test (amd64)
|
- name: Smoke test (amd64)
|
||||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-base --variant base
|
||||||
|
|
||||||
@@ -331,6 +354,8 @@ jobs:
|
|||||||
INSTALL_OPENCODE=true
|
INSTALL_OPENCODE=true
|
||||||
INSTALL_OMOS=true
|
INSTALL_OMOS=true
|
||||||
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
|
OMOS_VERSION=${{ needs.resolve-versions.outputs.omos_version }}
|
||||||
|
RELEASE_TAG=smoke
|
||||||
|
SOURCE_REVISION=${{ github.sha }}
|
||||||
- env:
|
- env:
|
||||||
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
EXPECTED_OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
||||||
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
|
run: bash scripts/smoke-test.sh opencode-devbox:smoke-omos --variant omos
|
||||||
@@ -338,7 +363,7 @@ jobs:
|
|||||||
# ── Phase 4: multi-arch publish per variant ────────────────────────
|
# ── Phase 4: multi-arch publish per variant ────────────────────────
|
||||||
|
|
||||||
build-variant-base:
|
build-variant-base:
|
||||||
needs: [base-decide, smoke-base]
|
needs: [base-decide, smoke-base, resolve-versions]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -377,8 +402,10 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TAGS: ${{ steps.tags.outputs.tags }}
|
TAGS: ${{ steps.tags.outputs.tags }}
|
||||||
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
|
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
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}"
|
||||||
# 3-attempt retry around `docker buildx build --push` (see build-base
|
# 3-attempt retry around `docker buildx build --push` (see build-base
|
||||||
@@ -392,6 +419,10 @@ jobs:
|
|||||||
--build-arg "BASE_IMAGE=${BASE_IMAGE_FULL}" \
|
--build-arg "BASE_IMAGE=${BASE_IMAGE_FULL}" \
|
||||||
--build-arg "INSTALL_OPENCODE=true" \
|
--build-arg "INSTALL_OPENCODE=true" \
|
||||||
--build-arg "INSTALL_OMOS=false" \
|
--build-arg "INSTALL_OMOS=false" \
|
||||||
|
--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"
|
||||||
@@ -447,8 +478,10 @@ jobs:
|
|||||||
TAGS: ${{ steps.tags.outputs.tags }}
|
TAGS: ${{ steps.tags.outputs.tags }}
|
||||||
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
OMOS_VERSION: ${{ needs.resolve-versions.outputs.omos_version }}
|
||||||
|
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
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}"
|
||||||
# 3-attempt retry (see build-base step for rationale). Variant: omos.
|
# 3-attempt retry (see build-base step for rationale). Variant: omos.
|
||||||
@@ -462,6 +495,10 @@ jobs:
|
|||||||
--build-arg "INSTALL_OPENCODE=true" \
|
--build-arg "INSTALL_OPENCODE=true" \
|
||||||
--build-arg "INSTALL_OMOS=true" \
|
--build-arg "INSTALL_OMOS=true" \
|
||||||
--build-arg "OMOS_VERSION=${OMOS_VERSION}" \
|
--build-arg "OMOS_VERSION=${OMOS_VERSION}" \
|
||||||
|
--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"
|
||||||
|
|||||||
@@ -6,6 +6,102 @@ Tags follow **independent semver** (since `v2.0.0`) — they version *this image
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
Ports the build-provenance, CI-hardening, SSH and shell fixes that landed in
|
||||||
|
the sibling **pi-devbox** repo (v1.1.4–v1.1.6) into opencode-devbox, adapted to
|
||||||
|
this image's companions and two-variant (`base`/`omos`) shape. Also bumps
|
||||||
|
opencode. Defaults are unchanged, so the canonical CI build stays byte-identical
|
||||||
|
apart from the opencode bump and the (cache-free) provenance layer.
|
||||||
|
|
||||||
|
### Fixed: read-only `~/.ssh` ControlPath / LAN sidecar on native Linux
|
||||||
|
|
||||||
|
`rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh` previously
|
||||||
|
`exit 0`-ed early on native-Linux hosts (`auto` mode, not VM-backed) **before**
|
||||||
|
rendering the writable `~/.ssh-local/config` sidecar. On such hosts with a
|
||||||
|
read-only `~/.ssh` bind-mount, `dssh`/`dscp` got no config and the `Host *`
|
||||||
|
ControlPath redirect into `~/.ssh-local/cm` never happened, so a user
|
||||||
|
`~/.ssh/config` carrying the CGNAT idiom `ControlPath ~/.ssh/cm/%r@%h:%p`
|
||||||
|
broke ControlMaster. The sidecar (ControlPath redirect + `Include
|
||||||
|
~/.ssh/config`) is now rendered on **every** host OS; only the jump-specific
|
||||||
|
blocks (host alias, key generation, peer overrides, RFC1918 catch-all) stay
|
||||||
|
gated behind a new `NEED_JUMP` flag. `Dockerfile.base` and `entrypoint-user.sh`
|
||||||
|
comments updated to document the always-render behavior and the
|
||||||
|
plain-`ssh <host>` caveat. (Mirrors pi-devbox v1.1.5; the pi-only
|
||||||
|
`ssh-controlmaster` extension layer has no opencode equivalent and is N/A.)
|
||||||
|
|
||||||
|
### Fixed: bash history loss in nested / tmux shells
|
||||||
|
|
||||||
|
`rootfs/home/developer/.bash_aliases` exported the `DEVBOX_HIST_SET` flush
|
||||||
|
guard, so it leaked into child processes — every nested shell (crucially each
|
||||||
|
tmux pane, which inherits the tmux server's env) saw the guard already set and
|
||||||
|
skipped installing `history -a` in `PROMPT_COMMAND`. Those shells only
|
||||||
|
persisted history on a clean exit, silently losing in-memory history on abrupt
|
||||||
|
termination (`docker stop`, `tmux kill-server`, SIGKILL). The guard is now
|
||||||
|
shell-local (dropped `export`). (Mirrors pi-devbox v1.1.4.)
|
||||||
|
|
||||||
|
### Added: build provenance — OCI labels + on-disk manifest
|
||||||
|
|
||||||
|
The variant build now bakes OCI labels
|
||||||
|
(`org.opencontainers.image.{version,revision,created}` +
|
||||||
|
`se.jordbo.opencode-devbox.{opencode-version,install-omos,omos-version,mempalace-toolkit-ref}`)
|
||||||
|
and writes `/etc/opencode-devbox/build-manifest.json` from **ground truth** —
|
||||||
|
the live `opencode --version`, the installed `oh-my-opencode-slim` version
|
||||||
|
(JSON `null` in the `base` variant), and the actual checked-out HEAD of
|
||||||
|
`/opt/mempalace-toolkit` — so a published tag is self-describing and
|
||||||
|
reconstructable after CI logs rotate. Provenance ARGs (`RELEASE_TAG`,
|
||||||
|
`BUILD_DATE`, `SOURCE_REVISION`, re-declared `MEMPALACE_TOOLKIT_REF`) are
|
||||||
|
declared last in `Dockerfile.variant` so they never bust the expensive
|
||||||
|
npm-install layers. Wired into both `build-variant-*` and `smoke-*` jobs;
|
||||||
|
`scripts/smoke-test.sh` now asserts the manifest exists, is complete, has no
|
||||||
|
`unknown` components, and that the `opencode-version` OCI label is present.
|
||||||
|
(Mirrors pi-devbox v1.1.6.)
|
||||||
|
|
||||||
|
### Added: base-rebuild hash guard (`scripts/check-base-hash.sh`)
|
||||||
|
|
||||||
|
New CI guard (run first in the `base-decide` job) that fails the build if any
|
||||||
|
floating `ARG *_REF` consumed by `Dockerfile.base` is not folded into the
|
||||||
|
`base_tag` hash — preventing the v1.1.2-class staleness footgun where a
|
||||||
|
ref-only dependency change silently fails to rebuild the base. Passes today
|
||||||
|
(`MEMPALACE_TOOLKIT_REF` is already folded in); this is forward protection.
|
||||||
|
(Mirrors pi-devbox v1.1.6.)
|
||||||
|
|
||||||
|
### Changed: fail-loud version/ref resolution
|
||||||
|
|
||||||
|
The `resolve-versions` step now validates each resolved value — the
|
||||||
|
mempalace-toolkit ref must be a 40-hex commit SHA, the omos version must be
|
||||||
|
semver — and **aborts the release** on failure instead of silently falling
|
||||||
|
back to a floating `main` ref (which defeats both cache-busting and
|
||||||
|
reproducibility). The step also gains `shell: bash`, because `set -o pipefail`
|
||||||
|
is illegal under the runner's default dash/sh and would otherwise abort the
|
||||||
|
step (this exact latent bug bit pi-devbox's first v1.1.6 run). (Mirrors
|
||||||
|
pi-devbox v1.1.6.)
|
||||||
|
|
||||||
|
### Added: overridable `MEMPALACE_TOOLKIT_REPO` build-arg
|
||||||
|
|
||||||
|
`Dockerfile.base` no longer hardcodes the mempalace-toolkit clone URL inline;
|
||||||
|
it is now an `ARG MEMPALACE_TOOLKIT_REPO` defaulting to the canonical gitea
|
||||||
|
origin, so a relocated/forked build can repoint it via `--build-arg` without
|
||||||
|
editing the Dockerfile. Default unchanged. (Mirrors pi-devbox v1.1.6.)
|
||||||
|
|
||||||
|
### Bumped: opencode-ai 1.17.7 → 1.17.8
|
||||||
|
|
||||||
|
`OPENCODE_VERSION` ARG in `Dockerfile.variant`. `1.17.8` is the current npm
|
||||||
|
`latest` stable. Only the variant layer rebuilds; the base is unaffected.
|
||||||
|
|
||||||
|
### Deferred (needs a decision): opencode.json merge-on-recreate
|
||||||
|
|
||||||
|
pi-devbox v1.1.4 added a non-destructive deep-merge of new template keys into a
|
||||||
|
preserved-volume `settings.json`. The direct analogue does **not** port cleanly
|
||||||
|
here: opencode's config is *generated from env vars* and written as **JSONC
|
||||||
|
with comments** (not a static image-owned template), and `generate-config.py`
|
||||||
|
deliberately never touches an existing config (host bind-mount or persisted
|
||||||
|
volume). A `jq`-style merge would strip the JSONC comments and risks clobbering
|
||||||
|
or re-adding entries a user removed. Left for a separate, deliberate change —
|
||||||
|
see discussion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v2.1.2 — 2026-06-16
|
## v2.1.2 — 2026-06-16
|
||||||
|
|
||||||
Image-semver **patch**: bumps opencode to `1.17.7`. No devbox-side changes
|
Image-semver **patch**: bumps opencode to `1.17.7`. No devbox-side changes
|
||||||
|
|||||||
+15
-1
@@ -94,6 +94,15 @@ RUN apt-get update && \
|
|||||||
# 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
|
||||||
# (mode 700) on each container start.
|
# (mode 700) on each container start.
|
||||||
|
#
|
||||||
|
# CAVEAT (and why dssh/dscp are 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 for a plain `ssh <host>` — a system drop-in here
|
||||||
|
# can never override a user's per-host value. For `ssh -F ~/.ssh-local/config`
|
||||||
|
# (the dssh/dscp aliases), setup-lan-access.sh redirects ControlPath into the
|
||||||
|
# writable ~/.ssh-local sidecar, so those paths are unaffected. See CHANGELOG
|
||||||
|
# "Unreleased".
|
||||||
RUN mkdir -p /etc/ssh/ssh_config.d && \
|
RUN mkdir -p /etc/ssh/ssh_config.d && \
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'# Devbox-baked default. See Dockerfile.base "SSH client defaults".' \
|
'# Devbox-baked default. See Dockerfile.base "SSH client defaults".' \
|
||||||
@@ -312,6 +321,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 is overridable so a relocated/forked build can repoint
|
||||||
|
# the clone without editing this Dockerfile (matches the *_REPO pattern used by
|
||||||
|
# other companions). Defaults to the canonical gitea origin; the default CI
|
||||||
|
# build is byte-identical.
|
||||||
|
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
|
||||||
@@ -320,7 +334,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; \
|
||||||
|
|||||||
+55
-1
@@ -39,7 +39,7 @@ ARG USER_NAME=developer
|
|||||||
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
|
# edit, so the cache-hit class of bug that bit pi-devbox v0.74.0..
|
||||||
# v0.75.5 cannot apply here.
|
# v0.75.5 cannot apply here.
|
||||||
ARG INSTALL_OPENCODE=true
|
ARG INSTALL_OPENCODE=true
|
||||||
ARG OPENCODE_VERSION=1.17.7
|
ARG OPENCODE_VERSION=1.17.8
|
||||||
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
||||||
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
NPM_CONFIG_PREFIX=/usr npm install -g opencode-ai@${OPENCODE_VERSION} && \
|
||||||
opencode --version ; \
|
opencode --version ; \
|
||||||
@@ -91,4 +91,58 @@ RUN if [ "${INSTALL_OMOS}" = "true" ]; then \
|
|||||||
NPM_CONFIG_PREFIX=/usr npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
|
NPM_CONFIG_PREFIX=/usr npm install -g oh-my-opencode-slim@${OMOS_VERSION}; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Build provenance: OCI labels + on-disk manifest ──────────────────
|
||||||
|
# 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 npm-install layers above. OPENCODE_VERSION,
|
||||||
|
# OMOS_VERSION and INSTALL_OMOS are already in scope from earlier in this
|
||||||
|
# stage and need no re-declaration; MEMPALACE_TOOLKIT_REF is consumed in
|
||||||
|
# Dockerfile.base, so it is re-declared here only to land in the labels.
|
||||||
|
ARG RELEASE_TAG=dev
|
||||||
|
ARG BUILD_DATE=
|
||||||
|
ARG SOURCE_REVISION=
|
||||||
|
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.opencode-devbox.opencode-version="${OPENCODE_VERSION}" \
|
||||||
|
se.jordbo.opencode-devbox.install-omos="${INSTALL_OMOS}" \
|
||||||
|
se.jordbo.opencode-devbox.omos-version="${OMOS_VERSION}" \
|
||||||
|
se.jordbo.opencode-devbox.mempalace-toolkit-ref="${MEMPALACE_TOOLKIT_REF}"
|
||||||
|
|
||||||
|
# The manifest is written from GROUND TRUTH — the live `opencode --version`,
|
||||||
|
# the omos package's installed version (when present), and the actual
|
||||||
|
# checked-out HEAD of /opt/mempalace-toolkit (cloned in the base) — not
|
||||||
|
# merely the intended build-args. That way it also exposes a dependency
|
||||||
|
# that silently resolved to something other than the requested value.
|
||||||
|
# oh-my-opencode-slim is present only in the omos variant (JSON null
|
||||||
|
# otherwise). NOTE: omos is installed under prefix /usr at build time, so
|
||||||
|
# we resolve its dir via `npm root -g` with that prefix rather than the
|
||||||
|
# runtime NPM_CONFIG_PREFIX the base sets for the developer volume.
|
||||||
|
RUN set -e; \
|
||||||
|
mkdir -p /etc/opencode-devbox; \
|
||||||
|
rev() { git -C "$1" rev-parse HEAD 2>/dev/null || echo "unknown"; }; \
|
||||||
|
OPENCODE_V="$(opencode --version 2>/dev/null | head -n1 | tr -d '\r\n')"; \
|
||||||
|
OMOS_REV='null'; \
|
||||||
|
if [ "${INSTALL_OMOS}" = "true" ]; then \
|
||||||
|
OMOS_DIR="$(NPM_CONFIG_PREFIX=/usr npm root -g 2>/dev/null)/oh-my-opencode-slim"; \
|
||||||
|
OMOS_V="$(node -e "process.stdout.write(require('${OMOS_DIR}/package.json').version)" 2>/dev/null || echo unknown)"; \
|
||||||
|
OMOS_REV="\"${OMOS_V}\""; \
|
||||||
|
fi; \
|
||||||
|
{ \
|
||||||
|
echo '{'; \
|
||||||
|
echo " \"release_tag\": \"${RELEASE_TAG}\","; \
|
||||||
|
echo " \"build_date\": \"${BUILD_DATE}\","; \
|
||||||
|
echo " \"source_revision\": \"${SOURCE_REVISION}\","; \
|
||||||
|
echo " \"opencode_version\": \"${OPENCODE_V}\","; \
|
||||||
|
echo " \"components\": {"; \
|
||||||
|
echo " \"opencode\": \"${OPENCODE_V}\","; \
|
||||||
|
echo " \"oh-my-opencode-slim\": ${OMOS_REV},"; \
|
||||||
|
echo " \"mempalace-toolkit\": \"$(rev /opt/mempalace-toolkit)\""; \
|
||||||
|
echo " }"; \
|
||||||
|
echo '}'; \
|
||||||
|
} > /etc/opencode-devbox/build-manifest.json; \
|
||||||
|
echo "── build manifest ──"; cat /etc/opencode-devbox/build-manifest.json
|
||||||
|
|
||||||
# WORKDIR / ENTRYPOINT / CMD inherited from base.
|
# WORKDIR / ENTRYPOINT / CMD inherited from base.
|
||||||
|
|||||||
+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/opencode-devbox/setup-lan-access.sh ]; then
|
if [ -r /usr/local/lib/opencode-devbox/setup-lan-access.sh ]; then
|
||||||
bash /usr/local/lib/opencode-devbox/setup-lan-access.sh || true
|
bash /usr/local/lib/opencode-devbox/setup-lan-access.sh || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -89,9 +89,16 @@ fi
|
|||||||
# we append with a newline separator to avoid the ';;' parse error
|
# we append with a newline separator to avoid the ';;' parse error
|
||||||
# described at the top of this file. Guarded so repeated sourcing
|
# described at the top of this file. Guarded so repeated sourcing
|
||||||
# (e.g. `exec bash`) doesn't stack duplicates.
|
# (e.g. `exec bash`) doesn't stack duplicates.
|
||||||
|
#
|
||||||
|
# The guard MUST stay shell-local (NOT exported): if it leaks into child
|
||||||
|
# processes, every nested shell -- crucially each tmux pane, which inherits
|
||||||
|
# the tmux server's env -- skips installing `history -a` and only persists
|
||||||
|
# history on a clean exit. Abrupt termination (docker stop, tmux kill-server,
|
||||||
|
# SIGKILL) then loses that shell's in-memory history. Keeping it unexported
|
||||||
|
# means each new interactive shell re-installs its own per-prompt flush.
|
||||||
if [ -z "${DEVBOX_HIST_SET:-}" ]; then
|
if [ -z "${DEVBOX_HIST_SET:-}" ]; then
|
||||||
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND$'\n'}history -a"
|
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND$'\n'}history -a"
|
||||||
export DEVBOX_HIST_SET=1
|
DEVBOX_HIST_SET=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Prompt: show [opencode-devbox] tag so it's obvious you're in the container
|
# ── Prompt: show [opencode-devbox] tag so it's obvious you're in the container
|
||||||
|
|||||||
@@ -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,41 +88,71 @@ 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=""
|
||||||
|
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'
|
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).
|
||||||
@@ -127,13 +161,12 @@ 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
|
||||||
@@ -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-split.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
|
||||||
@@ -197,6 +197,34 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Build provenance (manifest + OCI labels) --"
|
||||||
|
run "/etc/opencode-devbox/build-manifest.json present" \
|
||||||
|
"test -f /etc/opencode-devbox/build-manifest.json"
|
||||||
|
run_expect "manifest records opencode component" \
|
||||||
|
"cat /etc/opencode-devbox/build-manifest.json" '"opencode"'
|
||||||
|
run_expect "manifest records opencode_version" \
|
||||||
|
"cat /etc/opencode-devbox/build-manifest.json" '"opencode_version"'
|
||||||
|
run_expect "manifest records mempalace-toolkit component" \
|
||||||
|
"cat /etc/opencode-devbox/build-manifest.json" '"mempalace-toolkit"'
|
||||||
|
# Every resolved component must be a real value, never the 'unknown'
|
||||||
|
# sentinel that rev()/version lookups emit on failure. (oh-my-opencode-slim
|
||||||
|
# is JSON null in the base variant — that is expected, not 'unknown'.)
|
||||||
|
run "manifest has no unresolved ('unknown') components" \
|
||||||
|
"! grep -q '\"unknown\"' /etc/opencode-devbox/build-manifest.json"
|
||||||
|
if [ "$VARIANT" = "omos" ]; then
|
||||||
|
run "manifest omos component is resolved (not null) in omos variant" \
|
||||||
|
"! grep -q '\"oh-my-opencode-slim\": null' /etc/opencode-devbox/build-manifest.json"
|
||||||
|
fi
|
||||||
|
# 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.opencode-devbox.opencode-version" }}' "$IMAGE" 2>/dev/null || true)
|
||||||
|
if [ -n "$LBL" ] && [ "$LBL" != "<no value>" ]; then
|
||||||
|
pass "OCI label se.jordbo.opencode-devbox.opencode-version=$LBL"
|
||||||
|
else
|
||||||
|
fail "OCI label se.jordbo.opencode-devbox.opencode-version missing or empty"
|
||||||
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "-- Entrypoint behaviour --"
|
echo "-- Entrypoint behaviour --"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user