Compare commits
3 Commits
6639ba5820
...
v2.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f7e23d236c | |||
| d9ad634d5a | |||
| acb2096406 |
@@ -34,6 +34,17 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
# Gitea Actions' default step shell is `sh -e {0}` (dash), which rejects
|
||||||
|
# bash-only syntax like `set -o pipefail`, `[[ ]]`, and arrays. Setting the
|
||||||
|
# default to bash workflow-wide eliminates the whole class of "forgot
|
||||||
|
# `shell: bash` on this step" bugs. (Ported from pi-devbox, where this class
|
||||||
|
# bit twice: ed49b8d resolve-versions, b7197e8/b33e9dc promote-base-latest,
|
||||||
|
# run 418.) All existing dash steps use only POSIX syntax, so bash (a
|
||||||
|
# superset) runs them unchanged. Enforced by lint.yml's shell guard.
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILDKIT_PROGRESS: plain
|
BUILDKIT_PROGRESS: plain
|
||||||
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox
|
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/opencode-devbox
|
||||||
@@ -519,11 +530,16 @@ jobs:
|
|||||||
- base-decide
|
- base-decide
|
||||||
- build-variant-base
|
- build-variant-base
|
||||||
- build-variant-omos
|
- build-variant-omos
|
||||||
# Skip on cache-hit base builds: when need_build=false, base-latest
|
# Run on every tag release (and promote_latest=true dispatch). The gate
|
||||||
# already points at the same digest as base-<hash>, so the retag is
|
# deliberately does NOT key off need_build anymore: the no-op optimization
|
||||||
# a tautology and any transient failure of it is purely cosmetic.
|
# for genuine cache-hit releases moved INTO the step as a crane digest
|
||||||
# Manual workflow_dispatch with promote_latest=true overrides this
|
# compare (see below). Keying the gate on need_build was wrong because a
|
||||||
# gate as an escape hatch (e.g., if base-latest got hand-deleted).
|
# prior dry-run dispatch (promote_latest=false) can pre-build+push
|
||||||
|
# base-<hash>, making need_build=false on the subsequent tag run even
|
||||||
|
# though base-latest is still stale — the old gate then skipped promotion
|
||||||
|
# and left base-latest pointing at the PREVIOUS base. (Ported from
|
||||||
|
# pi-devbox b7197e8, which hit exactly this on its v1.2.3 dry-run-first
|
||||||
|
# release, 2026-06-27.)
|
||||||
#
|
#
|
||||||
# `always()` wrapper + explicit base-variant success check protects
|
# `always()` wrapper + explicit base-variant success check protects
|
||||||
# against the gitea-Actions default of "skipped need => skip dependent":
|
# against the gitea-Actions default of "skipped need => skip dependent":
|
||||||
@@ -532,8 +548,7 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
always() &&
|
always() &&
|
||||||
needs.build-variant-base.result == 'success' &&
|
needs.build-variant-base.result == 'success' &&
|
||||||
(inputs.promote_latest == 'true' ||
|
(inputs.promote_latest == 'true' || github.ref_type == 'tag')
|
||||||
(github.ref_type == 'tag' && needs.base-decide.outputs.need_build == 'true'))
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -558,11 +573,31 @@ jobs:
|
|||||||
crane auth login docker.io \
|
crane auth login docker.io \
|
||||||
-u ${{ vars.DOCKERHUB_USERNAME }} \
|
-u ${{ vars.DOCKERHUB_USERNAME }} \
|
||||||
-p "${{ secrets.DOCKERHUB_TOKEN }}"
|
-p "${{ secrets.DOCKERHUB_TOKEN }}"
|
||||||
- name: Re-tag base-<hash> as base-latest
|
- name: Re-tag base-<hash> as base-latest (only if stale)
|
||||||
|
env:
|
||||||
|
BASE_HASH_REF: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||||
|
BASE_LATEST_REF: ${{ env.IMAGE }}:base-latest
|
||||||
run: |
|
run: |
|
||||||
crane copy \
|
set -euo pipefail
|
||||||
${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }} \
|
# Correctness invariant: after a release, base-latest must resolve to
|
||||||
${{ env.IMAGE }}:base-latest
|
# the SAME digest as the base-<hash> the just-built variants were
|
||||||
|
# FROM. Compare digests rather than trusting need_build — a prior
|
||||||
|
# dry-run dispatch can pre-build base-<hash>, so need_build=false on
|
||||||
|
# the tag run does NOT imply base-latest is already current. When the
|
||||||
|
# digests already match (genuine cache-hit release) this is a no-op,
|
||||||
|
# so we skip the crane copy entirely — preserving the original
|
||||||
|
# "don't do a tautological retag" intent and avoiding any cosmetic
|
||||||
|
# transient-failure exposure on releases that change nothing.
|
||||||
|
want=$(crane digest "${BASE_HASH_REF}")
|
||||||
|
have=$(crane digest "${BASE_LATEST_REF}" 2>/dev/null || echo "")
|
||||||
|
echo "base-<hash> digest: ${want}"
|
||||||
|
echo "base-latest digest: ${have:-<absent>}"
|
||||||
|
if [ "${want}" = "${have}" ]; then
|
||||||
|
echo "base-latest already current; nothing to promote."
|
||||||
|
else
|
||||||
|
echo "Promoting base-latest -> ${BASE_HASH_REF}"
|
||||||
|
crane copy "${BASE_HASH_REF}" "${BASE_LATEST_REF}"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Phase 6: update Hub description (only on real release runs) ────
|
# ── Phase 6: update Hub description (only on real release runs) ────
|
||||||
update-description:
|
update-description:
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Lint workflows
|
||||||
|
|
||||||
|
# Durable guard against CI-workflow bugs — most importantly the "bash-only
|
||||||
|
# syntax under the default `sh`/dash shell" footgun. Ported from pi-devbox,
|
||||||
|
# where this class broke resolve-versions (ed49b8d) and promote-base-latest
|
||||||
|
# (b7197e8 → run 418). actionlint runs shellcheck against each `run:` step
|
||||||
|
# using its *effective* shell, so `set -o pipefail` under dash is flagged as
|
||||||
|
# SC3040 before any expensive build runs. This is cheap (~10s) and independent
|
||||||
|
# of the build pipeline, so it fires on every push/PR — not just on release
|
||||||
|
# tags, which is where docker-publish-split.yml is otherwise only triggered.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: lint-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
actionlint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install shellcheck
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y --no-install-recommends shellcheck python3-yaml
|
||||||
|
|
||||||
|
- name: Gitea shell guard (catches the actionlint blind spot)
|
||||||
|
# actionlint models GitHub Actions, where the default run shell is
|
||||||
|
# bash, so it does NOT flag bash syntax in a step that merely OMITS
|
||||||
|
# `shell:` — which is exactly how ed49b8d and b7197e8 manifested on
|
||||||
|
# Gitea (default sh/dash). This guard enforces that every run: step
|
||||||
|
# resolves to bash under Gitea's real defaults. Run it BEFORE
|
||||||
|
# actionlint so the more precise diagnostic surfaces first.
|
||||||
|
run: bash scripts/check-workflow-shell.sh .gitea/workflows
|
||||||
|
|
||||||
|
- name: Install actionlint (pinned)
|
||||||
|
env:
|
||||||
|
ACTIONLINT_VERSION: 1.7.7
|
||||||
|
run: |
|
||||||
|
curl -fsSL \
|
||||||
|
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
|
||||||
|
| tar -xz -C /usr/local/bin actionlint
|
||||||
|
actionlint --version
|
||||||
|
|
||||||
|
- name: Run actionlint
|
||||||
|
# SHELLCHECK_OPTS excludes pure-style codes (quoting/style opinions)
|
||||||
|
# so the guard stays focused on correctness bugs — crucially the
|
||||||
|
# SC3xxx "not POSIX / wrong shell" family that catches the pipefail
|
||||||
|
# footgun. Do NOT exclude SC3040 (set -o pipefail under sh) or any
|
||||||
|
# other SC3xxx code.
|
||||||
|
env:
|
||||||
|
SHELLCHECK_OPTS: "-e SC2086 -e SC2016 -e SC2129 -e SC2001 -e SC2312"
|
||||||
|
# Pass explicit paths: actionlint's no-arg mode auto-detects a
|
||||||
|
# project by looking for `.github/workflows`, which doesn't exist in
|
||||||
|
# this `.gitea/workflows` repo and hard-fails with exit 3
|
||||||
|
# ("no project was found"). Globbing the workflow files is the
|
||||||
|
# supported way to lint a non-GitHub layout.
|
||||||
|
run: actionlint -color .gitea/workflows/*.yml
|
||||||
@@ -35,6 +35,14 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
# Gitea Actions' default step shell is `sh` (dash); force bash workflow-wide so
|
||||||
|
# no run: step silently falls through to dash. Enforced by lint.yml's
|
||||||
|
# scripts/check-workflow-shell.sh guard, which scans ALL .gitea/workflows/*.yml
|
||||||
|
# (so this file must resolve to bash too, not just docker-publish-split.yml).
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docs-check:
|
docs-check:
|
||||||
# Fails if DOCKER_HUB.md is out of sync with what generate-dockerhub-md.py
|
# Fails if DOCKER_HUB.md is out of sync with what generate-dockerhub-md.py
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
|||||||
- `.gitea/README.md` — **read this first** if you're touching CI. Architectural overview of the build pipeline (production vs split-base), wall-clock estimates, NPM_CONFIG_PREFIX gotcha, runner expectations, migration plan.
|
- `.gitea/README.md` — **read this first** if you're touching CI. Architectural overview of the build pipeline (production vs split-base), wall-clock estimates, NPM_CONFIG_PREFIX gotcha, runner expectations, migration plan.
|
||||||
- `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
|
- `.gitea/workflows/validate.yml` — lightweight amd64 build + smoke test on push to main and PRs. Also runs the DOCKER_HUB.md sync check.
|
||||||
- `.gitea/workflows/docker-publish-split.yml` — production CI pipeline on tag push (`v*`). Two-phase split-base: computes base hash, conditionally builds base, runs 2 parallel smoke tests, then 2 parallel multi-arch variant builds, promotes `base-latest` alias, updates Docker Hub description.
|
- `.gitea/workflows/docker-publish-split.yml` — production CI pipeline on tag push (`v*`). Two-phase split-base: computes base hash, conditionally builds base, runs 2 parallel smoke tests, then 2 parallel multi-arch variant builds, promotes `base-latest` alias, updates Docker Hub description.
|
||||||
|
- `.gitea/workflows/lint.yml` — cheap (~10s) workflow-lint on every push/PR/dispatch: a Gitea-accurate shell guard (`scripts/check-workflow-shell.sh`) plus pinned actionlint + shellcheck. The guard asserts every `run:` step resolves to `bash` under Gitea's `sh` default, closing the actionlint blind spot (actionlint models GitHub, whose default `run` shell is bash). Ported from pi-devbox.
|
||||||
|
|
||||||
## Versioning scheme
|
## Versioning scheme
|
||||||
|
|
||||||
|
|||||||
+67
-1
@@ -6,7 +6,22 @@ Tags follow **independent semver** (since `v2.0.0`) — they version *this image
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Unreleased
|
## v2.4.0 — 2026-07-01
|
||||||
|
|
||||||
|
Minor release. Adds two **non-modal editors** (`nano` + `micro`) alongside
|
||||||
|
nvim, ports **CI hardening** from pi-devbox (the sh-vs-bash workflow guard and
|
||||||
|
the `base-latest` digest-based promote fix), bakes a **global gitignore** into
|
||||||
|
the image, and bumps **opencode `1.17.10` → `1.17.13`**. Because
|
||||||
|
`Dockerfile.base` changed (nano + micro), this release rebuilds the base image.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **opencode `1.17.10` → `1.17.13`** (three upstream patches). Highlights:
|
||||||
|
session snapshots + revert controls (1.17.11); MCP OAuth reconnect/refresh
|
||||||
|
fixes, a TUI yolo auto-approve mode, and better default small models
|
||||||
|
(1.17.12); forced reasoning mode for OpenAI-compatible reasoning models plus
|
||||||
|
a GitHub Copilot stale-response-ID fix (1.17.13). Full notes:
|
||||||
|
<https://github.com/anomalyco/opencode/releases>.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
@@ -18,6 +33,57 @@ Tags follow **independent semver** (since `v2.0.0`) — they version *this image
|
|||||||
now ignored across all repos in the container without per-repo `.gitignore`
|
now ignored across all repos in the container without per-repo `.gitignore`
|
||||||
entries. The `core.excludesFile` wiring is skipped if the user already set one.
|
entries. The `core.excludesFile` wiring is skipped if the user already set one.
|
||||||
|
|
||||||
|
- **Non-modal editors `nano` + `micro` alongside `nvim`.** The image shipped
|
||||||
|
only nvim (`EDITOR=nvim`), a modal vi-style editor. Added both a classic and
|
||||||
|
a modern non-modal option for users who don't want vi keybindings:
|
||||||
|
- **nano** (apt): ~2.8 MB installed; its deps (libc6, libncursesw6,
|
||||||
|
libtinfo6) are already present via nvim/less/htop/tmux, so no extra
|
||||||
|
packages are 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 image). `EDITOR` stays `nvim`; both 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. Ported from
|
||||||
|
pi-devbox 3a59e15.
|
||||||
|
|
||||||
|
- **Workflow-lint guard (`.gitea/workflows/lint.yml` + `scripts/check-workflow-shell.sh`).**
|
||||||
|
New cheap (~10s) lint workflow that runs on every push/PR (not just release
|
||||||
|
tags): a Gitea-accurate shell guard plus pinned `actionlint` + `shellcheck`.
|
||||||
|
The custom guard asserts every `run:` step in every `.gitea/workflows/*.yml`
|
||||||
|
resolves to an effective shell of `bash`, closing the actionlint blind spot
|
||||||
|
(actionlint models GitHub, whose default `run` shell is bash, so it does not
|
||||||
|
flag bash syntax in a step that merely omits `shell:` — the exact way the
|
||||||
|
sh-vs-bash footgun manifests on Gitea, whose default is `sh`/dash). Ported
|
||||||
|
from pi-devbox.
|
||||||
|
|
||||||
|
### Changed (CI)
|
||||||
|
|
||||||
|
- **Workflow-wide `defaults: run: shell: bash`** added to
|
||||||
|
`docker-publish-split.yml` and `validate.yml`. Gitea Actions' default step
|
||||||
|
shell is `sh` (dash), so bash-only syntax (`set -o pipefail`, `[[ ]]`,
|
||||||
|
arrays) in a step that forgets `shell: bash` fails silently. Setting the
|
||||||
|
default workflow-wide eliminates the whole class. All pre-existing steps use
|
||||||
|
only POSIX syntax, so bash (a superset) runs them unchanged — no behavioural
|
||||||
|
change. Preventive port from pi-devbox, where this class bit twice.
|
||||||
|
|
||||||
|
### Fixed (CI)
|
||||||
|
|
||||||
|
- **`promote-base-latest` re-points `base-latest` by digest, not `need_build`.**
|
||||||
|
The job 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`, skips promotion, and leaves
|
||||||
|
`base-latest` one base behind. The gate now runs on every tag release /
|
||||||
|
promote dispatch, and the no-op optimization moved into the step as a `crane
|
||||||
|
digest` compare — it re-tags only when `base-latest` actually differs from the
|
||||||
|
released `base-<hash>` (genuine cache-hit releases stay a no-op). Workflow-only
|
||||||
|
change; base hash unaffected (no base rebuild). Ported from pi-devbox b7197e8
|
||||||
|
(which hit this on its v1.2.3 release, 2026-06-27).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v2.3.0 — 2026-06-25
|
## v2.3.0 — 2026-06-25
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|||||||
# apt-get upgrade picks up any security/CVE fixes published between
|
# apt-get upgrade picks up any security/CVE fixes published between
|
||||||
# debian:trixie-slim base-image rebuilds. Paired with the index update
|
# debian:trixie-slim base-image rebuilds. Paired with the index update
|
||||||
# and the install in the same layer so we don't bloat image history.
|
# and the install in the same layer so we don't bloat image history.
|
||||||
|
# `nano` is included as a small, non-modal terminal editor for users who
|
||||||
|
# don't want vi-style modal editing — a companion to nvim and the `micro`
|
||||||
|
# binary installed further down. ~2.8 MB; its deps (libc6, libncursesw6,
|
||||||
|
# libtinfo6) are already pulled in by nvim/less/htop/tmux, so it adds no
|
||||||
|
# extra packages. EDITOR stays nvim; opt in via `export EDITOR=nano`.
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get upgrade -y --no-install-recommends && \
|
apt-get upgrade -y --no-install-recommends && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
@@ -66,6 +71,7 @@ RUN apt-get update && \
|
|||||||
rsync \
|
rsync \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
|
nano \
|
||||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -204,6 +210,33 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;
|
|||||||
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
|
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
|
||||||
nvim --version | head -1
|
nvim --version | head -1
|
||||||
|
|
||||||
|
# micro — modern, non-modal terminal editor. Ships alongside nvim so users
|
||||||
|
# who aren't comfortable with vi-style modal editing have a friendly option:
|
||||||
|
# desktop-style keybindings (Ctrl+S save, Ctrl+Q quit, Ctrl+C/V/X, Ctrl+Z
|
||||||
|
# undo), mouse support, and syntax highlighting out of the box. A single
|
||||||
|
# static Go binary (~12 MB) installed from GitHub releases, exactly like
|
||||||
|
# bat/eza/zoxide below. EDITOR stays nvim (see below); users opt in with
|
||||||
|
# `export EDITOR=micro` or `git config --global core.editor micro`.
|
||||||
|
#
|
||||||
|
# NOTE: upstream moved zyedidia/micro -> micro-editor/micro. The old org URL
|
||||||
|
# still 302s, but its /releases/latest redirect lands on ANOTHER /latest URL
|
||||||
|
# (the org rename), so the tag-parsing idiom below would resolve "latest"
|
||||||
|
# instead of a version. Use the canonical micro-editor/micro URL.
|
||||||
|
# Arch asset naming differs from the others: amd64 -> linux64, arm64 ->
|
||||||
|
# linux-arm64. The tarball extracts to micro-<version>/micro.
|
||||||
|
ARG MICRO_VERSION=latest
|
||||||
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "linux64" ;; arm64) echo "linux-arm64" ;; *) echo "linux64" ;; esac) && \
|
||||||
|
V="${MICRO_VERSION}" && \
|
||||||
|
if [ "$V" = "latest" ]; then \
|
||||||
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||||
|
fi && \
|
||||||
|
V="${V#v}" && [ -n "$V" ] && \
|
||||||
|
echo "Installing micro ${V}" && \
|
||||||
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/download/v${V}/micro-${V}-${ARCH}.tar.gz" | tar -xz -C /tmp && \
|
||||||
|
install /tmp/micro-${V}/micro /usr/local/bin/micro && \
|
||||||
|
rm -rf /tmp/micro-${V} && \
|
||||||
|
micro --version
|
||||||
|
|
||||||
# bat — syntax-highlighted cat replacement
|
# bat — syntax-highlighted cat replacement
|
||||||
ARG BAT_VERSION=latest
|
ARG BAT_VERSION=latest
|
||||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||||
|
|||||||
+1
-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.10
|
ARG OPENCODE_VERSION=1.17.13
|
||||||
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 ; \
|
||||||
|
|||||||
@@ -263,6 +263,11 @@ volumes:
|
|||||||
- ~/.config/nvim:/home/developer/.config/nvim:ro
|
- ~/.config/nvim:/home/developer/.config/nvim:ro
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Not a vi person?** The image also ships two non-modal editors alongside nvim:
|
||||||
|
> **nano** (classic, minimal) and **micro** (modern — desktop-style `Ctrl+S`/`Ctrl+Q`
|
||||||
|
> keys, mouse, syntax highlighting). `EDITOR` stays `nvim`; opt in per-shell with
|
||||||
|
> `export EDITOR=nano` (or `micro`), or for git with `git config --global core.editor micro`.
|
||||||
|
|
||||||
### Python development with uv
|
### Python development with uv
|
||||||
|
|
||||||
The image includes Python 3.13 (from Debian Trixie) and [uv](https://docs.astral.sh/uv/), a fast Python package manager that replaces pip, venv, and pyenv:
|
The image includes Python 3.13 (from Debian Trixie) and [uv](https://docs.astral.sh/uv/), a fast Python package manager that replaces pip, venv, and pyenv:
|
||||||
@@ -807,7 +812,7 @@ Container (Debian trixie)
|
|||||||
├── opencode binary
|
├── opencode binary
|
||||||
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
|
├── oh-my-opencode-slim (optional — multi-agent orchestration plugin, includes Bun)
|
||||||
├── AWS CLI v2 (SSO + Bedrock auth)
|
├── AWS CLI v2 (SSO + Bedrock auth)
|
||||||
├── neovim 0.12, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
|
├── neovim 0.12, nano, micro, tmux, htop, bat, eza, zoxide, uv, rustup, make, gcc, g++, rsync
|
||||||
├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, curl, tree
|
├── git, git-crypt, age, gitleaks, ssh, ripgrep, fd, fzf, jq, curl, tree
|
||||||
├── Node.js (for MCP servers)
|
├── Node.js (for MCP servers)
|
||||||
├── Bun (optional — included with oh-my-opencode-slim)
|
├── Bun (optional — included with oh-my-opencode-slim)
|
||||||
|
|||||||
Executable
+67
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Gitea-accurate guard against the "bash syntax under the default sh/dash
|
||||||
|
# shell" footgun. Ported from pi-devbox, where this class bit twice
|
||||||
|
# (ed49b8d resolve-versions; b7197e8/b33e9dc promote-base-latest, run 418).
|
||||||
|
# opencode-devbox has not been bitten yet — this is a PREVENTIVE guard so a
|
||||||
|
# future author can't reintroduce the class.
|
||||||
|
#
|
||||||
|
# WHY A CUSTOM CHECK AND NOT JUST actionlint:
|
||||||
|
# actionlint models *GitHub* Actions, whose default `run` shell is bash. It
|
||||||
|
# therefore assumes a step that omits `shell:` runs under bash, and does NOT
|
||||||
|
# flag `set -o pipefail` there. Gitea Actions' default is `sh` (dash), so the
|
||||||
|
# exact bug (omit `shell:`, use bash syntax) is invisible to actionlint.
|
||||||
|
# actionlint only fires when a step *explicitly* declares `shell: sh`.
|
||||||
|
#
|
||||||
|
# THE INVARIANT THIS ENFORCES:
|
||||||
|
# Every `run:` step in every .gitea/workflows/*.yml must resolve to an
|
||||||
|
# effective shell of `bash` — via the step's own `shell:`, a job-level
|
||||||
|
# `defaults.run.shell`, or a workflow-level `defaults.run.shell`. Any step
|
||||||
|
# that would fall through to Gitea's `sh` default is a FAILURE, because a
|
||||||
|
# future author adding bash syntax to it fails silently in CI.
|
||||||
|
#
|
||||||
|
# Pair this with actionlint (which catches explicit `shell: sh` + bash syntax,
|
||||||
|
# expression errors, and much else). Together they cover the class on Gitea.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WF_DIR="${1:-.gitea/workflows}"
|
||||||
|
|
||||||
|
python3 - "$WF_DIR" <<'PY'
|
||||||
|
import sys, glob, os
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
sys.stderr.write("ERROR: python3 yaml module missing (apt install python3-yaml)\n")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
wf_dir = sys.argv[1]
|
||||||
|
files = sorted(glob.glob(os.path.join(wf_dir, "*.yml")) + glob.glob(os.path.join(wf_dir, "*.yaml")))
|
||||||
|
if not files:
|
||||||
|
sys.stderr.write(f"ERROR: no workflow files under {wf_dir}\n")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
problems = []
|
||||||
|
for f in files:
|
||||||
|
with open(f) as fh:
|
||||||
|
doc = yaml.safe_load(fh) or {}
|
||||||
|
wf_shell = (((doc.get("defaults") or {}).get("run") or {}).get("shell"))
|
||||||
|
jobs = doc.get("jobs") or {}
|
||||||
|
for jname, job in jobs.items():
|
||||||
|
job = job or {}
|
||||||
|
job_shell = (((job.get("defaults") or {}).get("run") or {}).get("shell"))
|
||||||
|
steps = job.get("steps") or []
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
step = step or {}
|
||||||
|
if "run" not in step:
|
||||||
|
continue # `uses:` steps have no shell
|
||||||
|
eff = step.get("shell") or job_shell or wf_shell or "sh" # Gitea default = sh
|
||||||
|
if eff != "bash":
|
||||||
|
name = step.get("name") or f"step[{i}]"
|
||||||
|
problems.append(f"{f}: job '{jname}' / '{name}': effective shell = '{eff}' (Gitea default is sh; declare shell: bash or a bash default)")
|
||||||
|
|
||||||
|
if problems:
|
||||||
|
sys.stderr.write("Workflow shell guard FAILED — bash default not guaranteed:\n")
|
||||||
|
for p in problems:
|
||||||
|
sys.stderr.write(f" - {p}\n")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f"Workflow shell guard OK — all run: steps in {len(files)} workflow file(s) resolve to bash.")
|
||||||
|
PY
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
# Verifies:
|
# Verifies:
|
||||||
# - Core binaries are on PATH and runnable
|
# - Core binaries are on PATH and runnable
|
||||||
|
# - non-modal editors nano + micro are present (alongside nvim)
|
||||||
# - opencode itself starts and prints a version
|
# - opencode itself starts and prints a version
|
||||||
# - Entrypoint runs cleanly as non-root after UID adjustment
|
# - Entrypoint runs cleanly as non-root after UID adjustment
|
||||||
# - Generated opencode.json has the expected shape
|
# - Generated opencode.json has the expected shape
|
||||||
@@ -118,6 +119,8 @@ run "node" "node --version"
|
|||||||
run "npm" "npm --version"
|
run "npm" "npm --version"
|
||||||
run "git" "git --version"
|
run "git" "git --version"
|
||||||
run "nvim" "nvim --version | head -1"
|
run "nvim" "nvim --version | head -1"
|
||||||
|
run "nano" "nano --version | head -1"
|
||||||
|
run "micro" "micro --version"
|
||||||
run "bat" "bat --version"
|
run "bat" "bat --version"
|
||||||
run "eza" "eza --version | head -1"
|
run "eza" "eza --version | head -1"
|
||||||
run "zoxide" "zoxide --version"
|
run "zoxide" "zoxide --version"
|
||||||
|
|||||||
Reference in New Issue
Block a user