Infrastructure pass: CI smoke tests, floating versions, chown sentinel, generate-config script
Main changes: - Extract opencode.json generation from entrypoint-user.sh into a standalone Python script (rootfs/usr/local/lib/opencode-devbox/ generate-config.py). Preserves the never-overwrite-existing-config guarantee. Cuts entrypoint-user.sh from 176 to 97 lines. - Install MemPalace via 'uv tool install' into an isolated venv at /opt/uv-tools/mempalace/ with a /usr/local/bin/mempalace-mcp-server wrapper, replacing the 'pip install --break-system-packages' escape hatch. The wrapper is what generate-config.py references in the auto-generated opencode.json. Also fix 'mempalace init' in entrypoint-user.sh to use --yes so first-start initialization isn't interactive (this used to hang or print prompts into the user's terminal). Gated by INSTALL_MEMPALACE build arg (default true) so users who don't need AI memory can shave ~300 MB. - Sentinel-file pattern in entrypoint.sh volume-ownership loop: write .devbox-owner after a successful chown -R, skip the recursive walk on subsequent starts when the sentinel matches FINAL_UID:FINAL_GID. Cuts multi-second startup costs to milliseconds on large volumes (nvim plugins, palace data). UID changes still trigger a full chown. - Float all GitHub/Gitea-hosted binary versions: gosu, fzf, git-lfs, neovim, bat, eza, zoxide, uv, gitea-mcp now default to 'latest' and resolve the newest upstream release at build time via the /releases/ latest redirect. Go (go.dev JSON feed) and oh-my-opencode-slim (npm @latest) likewise. Intentional pins still in place: OPENCODE_VERSION, NODE_VERSION=22, DEBIAN_VERSION=trixie-slim. Each *_VERSION ARG accepts an explicit value to lock a specific version when needed. - New scripts/smoke-test.sh verifies binary presence, opencode startup, entrypoint user drop, generate-config idempotency, bun's presence- per-variant, and image size against thresholds (2500 MB base, 3000 MB OMOS). Prints resolved component versions as its first step so CI logs always record what got baked into a given image. - New .gitea/workflows/validate.yml runs on push to main and PRs: single-arch amd64 build, smoke test, DOCKER_HUB.md sync check. Tag- triggered docker-publish.yml now smoke-tests each variant on amd64 before the full multi-arch push. - scripts/generate-dockerhub-md.py auto-generates DOCKER_HUB.md from README.md using explicit SECTION_RULES. --check mode fails CI when the committed file is out of sync. Enforces the 25 kB Docker Hub limit. Adding a new README section forces an explicit keep/drop/ replace decision. - Remove dead INSTALL_PYTHON build arg (was a no-op since mempalace added python3 unconditionally).
This commit is contained in:
Executable
+290
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate DOCKER_HUB.md from README.md.
|
||||
|
||||
Rationale
|
||||
---------
|
||||
README.md is the authoritative source. DOCKER_HUB.md is a subset
|
||||
intended for users pulling the pre-built image from Docker Hub — so
|
||||
build-from-source instructions, developer setup (git hooks, gitleaks),
|
||||
and CI/contribution content are dropped.
|
||||
|
||||
Docker Hub enforces a 25 kB limit on the full description field.
|
||||
|
||||
Usage
|
||||
-----
|
||||
Regenerate in place:
|
||||
python3 scripts/generate-dockerhub-md.py
|
||||
|
||||
Fail if DOCKER_HUB.md is out of sync with what this script would emit
|
||||
(run this in CI):
|
||||
python3 scripts/generate-dockerhub-md.py --check
|
||||
|
||||
Design
|
||||
------
|
||||
Sections are selected and in some cases rewritten via `SECTION_RULES`
|
||||
below. This keeps the transformation explicit and easy to audit — if
|
||||
a new section is added to README.md that should also appear on Docker
|
||||
Hub, extend SECTION_RULES rather than inventing implicit heuristics.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
README = REPO_ROOT / "README.md"
|
||||
DOCKER_HUB = REPO_ROOT / "DOCKER_HUB.md"
|
||||
|
||||
# Max size for Docker Hub full_description (bytes, UTF-8).
|
||||
MAX_SIZE_BYTES = 25_000
|
||||
|
||||
# Per-section transformation.
|
||||
#
|
||||
# Each key is a top-level section title as it appears in README.md
|
||||
# (without the leading "## ").
|
||||
#
|
||||
# The value is one of:
|
||||
# "keep" — include verbatim.
|
||||
# "drop" — exclude entirely.
|
||||
# "replace" — substitute a custom body (see REPLACEMENTS).
|
||||
# "trim" — keep but drop selected level-3 sub-sections listed
|
||||
# in TRIM_SUBSECTIONS[title].
|
||||
#
|
||||
# Unknown sections default to "drop" with a warning — forcing an
|
||||
# explicit decision whenever README gains a new section.
|
||||
SECTION_RULES: dict[str, str] = {
|
||||
"Why?": "drop", # build-motivation, not user-facing
|
||||
"Quick Start": "replace", # swap docker compose clone flow for docker run
|
||||
"Features": "keep",
|
||||
"Usage": "keep",
|
||||
"Configuration": "trim", # drop dev-build sub-sections
|
||||
"oh-my-opencode-slim (Multi-Agent Orchestration)": "keep",
|
||||
"AWS Bedrock Authentication": "keep",
|
||||
"MemPalace — persistent AI memory": "keep",
|
||||
"Gitea MCP server": "keep",
|
||||
"Shell defaults": "keep",
|
||||
"Secret Scanning": "drop", # dev-only — gitleaks is for committers
|
||||
"Architecture": "keep",
|
||||
"License": "replace", # point at source repo instead
|
||||
}
|
||||
|
||||
# Level-3 sub-section titles (without the leading "### ") to drop from
|
||||
# sections flagged as "trim". These are dev/build-oriented — Docker Hub
|
||||
# users already have the image and don't need rebuild or multi-user
|
||||
# compose instructions.
|
||||
TRIM_SUBSECTIONS: dict[str, set[str]] = {
|
||||
"Configuration": {
|
||||
"Multi-user setup",
|
||||
"Rebuilding the Image",
|
||||
"Build Args",
|
||||
},
|
||||
}
|
||||
|
||||
# Replacement bodies. Keys match SECTION_RULES entries marked "replace".
|
||||
# Each value is the full section including the "## Title" heading.
|
||||
REPLACEMENTS: dict[str, str] = {
|
||||
"Quick Start": """## Quick Start
|
||||
|
||||
```bash
|
||||
docker run -it --rm \\
|
||||
-e ANTHROPIC_API_KEY=your-key \\
|
||||
-e OPENCODE_PROVIDER=anthropic \\
|
||||
-e GIT_USER_NAME="Your Name" \\
|
||||
-e GIT_USER_EMAIL="you@example.com" \\
|
||||
-v ~/projects:/workspace \\
|
||||
-v ~/.ssh:/home/developer/.ssh:ro \\
|
||||
joakimp/opencode-devbox:latest
|
||||
```
|
||||
|
||||
This drops you straight into opencode with your project mounted at `/workspace`.
|
||||
|
||||
For an interactive shell first (useful for AWS SSO login):
|
||||
|
||||
```bash
|
||||
docker run -it --rm \\
|
||||
-e ANTHROPIC_API_KEY=your-key \\
|
||||
-e OPENCODE_PROVIDER=anthropic \\
|
||||
-v ~/projects:/workspace \\
|
||||
-v ~/.ssh:/home/developer/.ssh:ro \\
|
||||
joakimp/opencode-devbox:latest bash
|
||||
```
|
||||
|
||||
Then run `opencode` when ready.
|
||||
|
||||
For docker-compose users, see the source repo for `docker-compose.yml` and `.env.example` templates.
|
||||
""",
|
||||
"License": """## Source
|
||||
|
||||
MIT licensed. Source, issues, and `docker-compose.yml` templates: <https://gitea.jordbo.se/joakimp/opencode-devbox>
|
||||
""",
|
||||
}
|
||||
|
||||
|
||||
# Prepended to the generated file.
|
||||
HEADER = """# opencode-devbox — Docker Hub
|
||||
|
||||
Portable AI developer environment for [opencode](https://opencode.ai). Debian-based, with git, SSH, Node.js, AWS CLI v2, and common dev tools pre-installed.
|
||||
|
||||
## Image Variants
|
||||
|
||||
Two image variants are published for each release:
|
||||
|
||||
| Tag | Description |
|
||||
|---|---|
|
||||
| `latest` / `vX.Y.Z` | Base image — opencode, Node.js, AWS CLI, dev tools |
|
||||
| `latest-omos` / `vX.Y.Z-omos` | Base + [oh-my-opencode-slim](https://github.com/alvinunreal/oh-my-opencode-slim) multi-agent orchestration and Bun |
|
||||
|
||||
Both variants support `linux/amd64` and `linux/arm64`.
|
||||
|
||||
> **NOTE:** This file is auto-generated from `README.md` by `scripts/generate-dockerhub-md.py`. Edit README.md and regenerate rather than editing this file directly.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def split_sections(md: str) -> list[tuple[str, str]]:
|
||||
"""Split markdown on level-2 headings, returning (title, body) pairs.
|
||||
|
||||
The body includes the heading line and everything up to (but not
|
||||
including) the next level-2 heading or EOF. Content before the first
|
||||
``## `` is returned with an empty title (the document preamble).
|
||||
"""
|
||||
pattern = re.compile(r"^## ", re.MULTILINE)
|
||||
parts = pattern.split(md)
|
||||
preamble, *rest = parts
|
||||
|
||||
sections: list[tuple[str, str]] = []
|
||||
if preamble.strip():
|
||||
sections.append(("", preamble))
|
||||
for part in rest:
|
||||
line, _, body = part.partition("\n")
|
||||
sections.append((line.strip(), f"## {line}\n{body}"))
|
||||
return sections
|
||||
|
||||
|
||||
def trim_subsections(body: str, drop: set[str]) -> str:
|
||||
"""Remove level-3 sub-sections whose title is in `drop`.
|
||||
|
||||
A sub-section starts at a line beginning with "### " and ends at
|
||||
the next "### " or "## " (or EOF).
|
||||
"""
|
||||
if not drop:
|
||||
return body
|
||||
|
||||
# Split on level-3 headings while preserving the level-2 header
|
||||
# block. First piece is everything up to the first "### ".
|
||||
parts = re.split(r"(^### .+\n)", body, flags=re.MULTILINE)
|
||||
# parts alternates: [before_first_h3, "### Title\n", body, "### Title\n", body, ...]
|
||||
kept: list[str] = [parts[0]] if parts else []
|
||||
i = 1
|
||||
while i < len(parts):
|
||||
heading = parts[i]
|
||||
content = parts[i + 1] if i + 1 < len(parts) else ""
|
||||
title = heading[4:].strip()
|
||||
if title not in drop:
|
||||
kept.append(heading)
|
||||
kept.append(content)
|
||||
i += 2
|
||||
return "".join(kept)
|
||||
|
||||
|
||||
def generate() -> str:
|
||||
"""Produce the DOCKER_HUB.md content string."""
|
||||
readme = README.read_text(encoding="utf-8")
|
||||
sections = split_sections(readme)
|
||||
|
||||
out: list[str] = [HEADER]
|
||||
unknown: list[str] = []
|
||||
|
||||
for title, body in sections:
|
||||
if title == "":
|
||||
# README preamble is replaced by our HEADER; skip.
|
||||
continue
|
||||
|
||||
rule = SECTION_RULES.get(title)
|
||||
if rule is None:
|
||||
unknown.append(title)
|
||||
continue
|
||||
if rule == "drop":
|
||||
continue
|
||||
if rule == "keep":
|
||||
out.append(body.rstrip() + "\n\n")
|
||||
elif rule == "trim":
|
||||
trimmed = trim_subsections(body, TRIM_SUBSECTIONS.get(title, set()))
|
||||
out.append(trimmed.rstrip() + "\n\n")
|
||||
elif rule == "replace":
|
||||
out.append(REPLACEMENTS[title].rstrip() + "\n\n")
|
||||
else: # pragma: no cover — programmer error
|
||||
raise AssertionError(f"unknown rule {rule!r} for section {title!r}")
|
||||
|
||||
if unknown:
|
||||
print(
|
||||
"ERROR: README.md contains sections not classified in "
|
||||
"SECTION_RULES:\n - "
|
||||
+ "\n - ".join(unknown)
|
||||
+ "\n\nAdd each to SECTION_RULES in "
|
||||
"scripts/generate-dockerhub-md.py (choose keep/drop/replace).",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(2)
|
||||
|
||||
return "".join(out).rstrip() + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
"--check",
|
||||
action="store_true",
|
||||
help="Fail if DOCKER_HUB.md differs from generated content.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
content = generate()
|
||||
size = len(content.encode("utf-8"))
|
||||
if size > MAX_SIZE_BYTES:
|
||||
print(
|
||||
f"ERROR: generated DOCKER_HUB.md is {size} bytes, exceeding the "
|
||||
f"Docker Hub limit of {MAX_SIZE_BYTES} bytes.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
if args.check:
|
||||
existing = DOCKER_HUB.read_text(encoding="utf-8") if DOCKER_HUB.exists() else ""
|
||||
if existing != content:
|
||||
print(
|
||||
"ERROR: DOCKER_HUB.md is out of sync with README.md.\n"
|
||||
"Run: python3 scripts/generate-dockerhub-md.py",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# Show a small diff hint.
|
||||
import difflib
|
||||
|
||||
diff = difflib.unified_diff(
|
||||
existing.splitlines(keepends=True),
|
||||
content.splitlines(keepends=True),
|
||||
fromfile="DOCKER_HUB.md (committed)",
|
||||
tofile="DOCKER_HUB.md (generated)",
|
||||
n=2,
|
||||
)
|
||||
sys.stderr.writelines(list(diff)[:80])
|
||||
return 1
|
||||
print(
|
||||
f"OK: DOCKER_HUB.md is in sync with README.md "
|
||||
f"({size} bytes, {MAX_SIZE_BYTES} limit).",
|
||||
)
|
||||
return 0
|
||||
|
||||
DOCKER_HUB.write_text(content, encoding="utf-8")
|
||||
print(
|
||||
f"Wrote {DOCKER_HUB} ({size} bytes, {MAX_SIZE_BYTES} limit).",
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# Smoke-test a freshly-built opencode-devbox image.
|
||||
#
|
||||
# Verifies:
|
||||
# - Core binaries are on PATH and runnable
|
||||
# - opencode itself starts and prints a version
|
||||
# - Entrypoint runs cleanly as non-root after UID adjustment
|
||||
# - Generated opencode.json has the expected shape
|
||||
# - MCP wrapper works (when mempalace is installed)
|
||||
#
|
||||
# Usage: ./scripts/smoke-test.sh <image> [--variant base|omos]
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 all checks passed
|
||||
# 1 one or more checks failed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="${1:-}"
|
||||
VARIANT="base"
|
||||
if [ "${2:-}" = "--variant" ]; then
|
||||
VARIANT="${3:-base}"
|
||||
fi
|
||||
|
||||
if [ -z "$IMAGE" ]; then
|
||||
echo "usage: $0 <image> [--variant base|omos]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
FAILED=0
|
||||
pass() { echo " ✓ $1"; }
|
||||
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
|
||||
|
||||
run() {
|
||||
# Run a command inside the image and capture its output.
|
||||
# First arg is a label, rest is the shell command.
|
||||
local label="$1"; shift
|
||||
local out
|
||||
if out=$(docker run --rm --entrypoint="" "$IMAGE" sh -c "$*" 2>&1); then
|
||||
pass "$label ($(echo "$out" | head -1))"
|
||||
else
|
||||
fail "$label: $out"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "=== Smoke test: $IMAGE (variant: $VARIANT) ==="
|
||||
echo
|
||||
echo "-- Resolved component versions --"
|
||||
# Prints the actual version of every floating component so CI logs
|
||||
# always record what got baked into this image, even when Dockerfile
|
||||
# ARGs default to "latest".
|
||||
docker run --rm --entrypoint="" "$IMAGE" sh -c '
|
||||
printf " %-15s %s\n" "opencode" "$(opencode --version 2>&1 | head -1)"
|
||||
printf " %-15s %s\n" "node" "$(node --version)"
|
||||
printf " %-15s %s\n" "npm" "$(npm --version)"
|
||||
printf " %-15s %s\n" "nvim" "$(nvim --version | head -1)"
|
||||
printf " %-15s %s\n" "bat" "$(bat --version)"
|
||||
printf " %-15s %s\n" "eza" "$(eza --version | head -2 | tail -1)"
|
||||
printf " %-15s %s\n" "zoxide" "$(zoxide --version)"
|
||||
printf " %-15s %s\n" "uv" "$(uv --version)"
|
||||
printf " %-15s %s\n" "fzf" "$(fzf --version)"
|
||||
printf " %-15s %s\n" "fd" "$(fd --version)"
|
||||
printf " %-15s %s\n" "rg" "$(rg --version | head -1)"
|
||||
printf " %-15s %s\n" "gosu" "$(gosu --version)"
|
||||
printf " %-15s %s\n" "git-lfs" "$(git-lfs --version)"
|
||||
printf " %-15s %s\n" "gitea-mcp" "$(gitea-mcp --version 2>&1 | head -1)"
|
||||
printf " %-15s %s\n" "aws" "$(aws --version 2>&1)"
|
||||
if command -v bun >/dev/null 2>&1; then
|
||||
printf " %-15s %s\n" "bun" "$(bun --version)"
|
||||
fi
|
||||
if command -v mempalace >/dev/null 2>&1; then
|
||||
printf " %-15s %s\n" "mempalace" "$(mempalace --version 2>&1 | head -1 || echo installed)"
|
||||
fi
|
||||
'
|
||||
echo
|
||||
echo "-- Core binaries --"
|
||||
run "opencode" "opencode --version"
|
||||
run "node" "node --version"
|
||||
run "npm" "npm --version"
|
||||
run "git" "git --version"
|
||||
run "nvim" "nvim --version | head -1"
|
||||
run "bat" "bat --version"
|
||||
run "eza" "eza --version | head -1"
|
||||
run "zoxide" "zoxide --version"
|
||||
run "uv" "uv --version"
|
||||
run "uvx" "uvx --version"
|
||||
run "rustup-init" "rustup-init --version"
|
||||
run "fzf" "fzf --version"
|
||||
run "fd" "fd --version"
|
||||
run "rg" "rg --version | head -1"
|
||||
run "jq" "jq --version"
|
||||
run "aws" "aws --version"
|
||||
run "gitea-mcp" "gitea-mcp --version"
|
||||
run "gosu" "gosu --version"
|
||||
run "tmux" "tmux -V"
|
||||
|
||||
echo
|
||||
echo "-- Optional / variant-gated --"
|
||||
# mempalace: present unless built with INSTALL_MEMPALACE=false
|
||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v mempalace" >/dev/null 2>&1; then
|
||||
run "mempalace" "mempalace --help | head -1"
|
||||
run "mempalace-mcp-server" "test -x /usr/local/bin/mempalace-mcp-server && echo wrapper-present"
|
||||
else
|
||||
echo " - mempalace not installed (INSTALL_MEMPALACE=false)"
|
||||
fi
|
||||
|
||||
# bun: only in the omos variant
|
||||
if [ "$VARIANT" = "omos" ]; then
|
||||
run "bun (omos)" "bun --version"
|
||||
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
||||
# verify it shows up in the global module list.
|
||||
run "oh-my-opencode-slim" "npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
|
||||
else
|
||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v bun" >/dev/null 2>&1; then
|
||||
fail "bun should NOT be in base image but was found"
|
||||
else
|
||||
pass "bun correctly absent from base image"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- Entrypoint behaviour --"
|
||||
|
||||
# Generate-config script exists and has valid syntax.
|
||||
run "generate-config.py exists" \
|
||||
"test -x /usr/local/lib/opencode-devbox/generate-config.py && python3 -m py_compile /usr/local/lib/opencode-devbox/generate-config.py && echo ok"
|
||||
|
||||
# Entrypoint drops to developer user and runs a trivial command.
|
||||
# Writes the result to a file inside the container so we don't have to
|
||||
# disentangle entrypoint log output from command stdout on the host.
|
||||
label="entrypoint drops to developer"
|
||||
tmpout=$(mktemp)
|
||||
if docker run --rm -e OPENCODE_PROVIDER= "$IMAGE" \
|
||||
sh -c 'whoami > /tmp/who && cat /tmp/who' > "$tmpout" 2>/dev/null; then
|
||||
# The last line of stdout is the whoami output. Entrypoint log lines
|
||||
# (MemPalace init, "Adjusted developer UID", etc.) go to stderr or
|
||||
# get printed before our sh command runs.
|
||||
actual=$(tail -1 "$tmpout" | tr -d '[:space:]')
|
||||
if [ "$actual" = "developer" ]; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label: expected 'developer', got '$actual' (full output: $(cat "$tmpout"))"
|
||||
fi
|
||||
else
|
||||
fail "$label: container failed"
|
||||
fi
|
||||
rm -f "$tmpout"
|
||||
|
||||
# Config generation with anthropic provider writes valid JSON with the
|
||||
# expected shape. The script's log message goes to stderr (line 1 of
|
||||
# generate-config.py uses file=sys.stderr) so capturing only stdout
|
||||
# gives us clean JSON.
|
||||
label="generate-config produces valid opencode.json"
|
||||
tmp=$(mktemp -d)
|
||||
if docker run --rm \
|
||||
-e OPENCODE_PROVIDER=anthropic \
|
||||
-e HOME=/tmp/home \
|
||||
--entrypoint="" \
|
||||
"$IMAGE" sh -c '
|
||||
mkdir -p /tmp/home
|
||||
python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null
|
||||
cat /tmp/home/.config/opencode/opencode.json
|
||||
' > "$tmp/out.json" 2>/dev/null; then
|
||||
if python3 -c "
|
||||
import json, sys
|
||||
c = json.load(open('$tmp/out.json'))
|
||||
assert c['model'].startswith('anthropic/'), c
|
||||
assert c['autoupdate'] is False
|
||||
assert c['share'] == 'disabled'
|
||||
" 2>&1; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label: output doesn't match expected shape: $(cat "$tmp/out.json")"
|
||||
fi
|
||||
else
|
||||
fail "$label: container failed: $(cat "$tmp/out.json")"
|
||||
fi
|
||||
|
||||
# Config generation is idempotent — running twice must not overwrite.
|
||||
label="generate-config never overwrites existing config"
|
||||
if docker run --rm \
|
||||
-e OPENCODE_PROVIDER=anthropic \
|
||||
-e HOME=/tmp/home \
|
||||
--entrypoint="" \
|
||||
"$IMAGE" sh -c '
|
||||
mkdir -p /tmp/home/.config/opencode
|
||||
echo "{\"sentinel\": \"user-config\"}" > /tmp/home/.config/opencode/opencode.json
|
||||
python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null
|
||||
cat /tmp/home/.config/opencode/opencode.json
|
||||
' 2>/dev/null | grep -q '"sentinel": "user-config"'; then
|
||||
pass "$label"
|
||||
else
|
||||
fail "$label: existing config was modified!"
|
||||
fi
|
||||
rm -rf "$tmp"
|
||||
|
||||
echo
|
||||
echo "-- Image size --"
|
||||
SIZE_BYTES=$(docker image inspect --format='{{.Size}}' "$IMAGE")
|
||||
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
|
||||
echo " Uncompressed size: ${SIZE_MB} MB"
|
||||
|
||||
# Thresholds (uncompressed): base 2500 MB, omos 3000 MB. Adjust as image content evolves.
|
||||
THRESHOLD=2500
|
||||
[ "$VARIANT" = "omos" ] && THRESHOLD=3000
|
||||
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
||||
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
||||
else
|
||||
pass "image size ${SIZE_MB} MB within threshold ${THRESHOLD} MB"
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$FAILED" -gt 0 ]; then
|
||||
echo "=== FAILED: $FAILED check(s) ===" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "=== PASSED ==="
|
||||
Reference in New Issue
Block a user