ci: port pi-devbox CI hardening — bash-default footgun guard + base-latest digest promote

Two CI-only changes ported from pi-devbox (no runtime/image impact),
adapted to opencode-devbox's split-base 2-variant pipeline. Rides the
next release.

C — eliminate the sh-vs-bash footgun class:
- Add `defaults: run: shell: bash` workflow-wide to docker-publish-split.yml
  and validate.yml. Gitea's default step shell is sh/dash, so bash-only
  syntax in a step that omits `shell: bash` fails silently. All pre-existing
  steps are POSIX, so bash runs them unchanged (no behavioural change).
- New .gitea/workflows/lint.yml (push/PR/dispatch): a Gitea-accurate shell
  guard (scripts/check-workflow-shell.sh) + pinned actionlint + shellcheck.
  The guard closes the actionlint blind spot: actionlint models GitHub
  (default shell bash) so it does NOT flag bash syntax in a shell-less step.
  Guard scans ALL .gitea/workflows/*.yml (hence the validate.yml default too).
  Ported from pi-devbox 26384fe/d1db595.

B — promote-base-latest re-points base-latest by digest, not need_build:
  The gate keyed off need_build=='true', assuming need_build==false meant
  base-latest was current. A dry-run dispatch that pre-builds base-<hash>
  falsifies that, leaving base-latest one base behind. Gate now runs on every
  tag release / promote dispatch; the no-op optimization moved into the step
  as a crane digest compare (re-tags only when base-latest != released
  base-<hash>). Ported from pi-devbox b7197e8.

Validated locally: all 3 workflows YAML-parse; shell guard passes real
workflows and correctly fails a synthetic omit-shell+pipefail workflow;
actionlint (pinned 1.7.7) passes with explicit .gitea/workflows/*.yml glob.
This commit is contained in:
pi
2026-07-01 23:00:20 +02:00
parent 6639ba5820
commit acb2096406
5 changed files with 223 additions and 11 deletions
+34
View File
@@ -18,6 +18,40 @@ Tags follow **independent semver** (since `v2.0.0`) — they version *this image
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.
- **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