ci: no image build on push — split cheap checks from builds
Lint / hadolint (push) Successful in 8s
Lint / docs-check (push) Successful in 14s
Lint / actionlint (push) Successful in 6m41s

A push to main used to trigger validate.yml, which builds an amd64 variant and
runs the smoke test. Pushing work-in-progress to main therefore cost a build.
Restructure so the three workflows divide by cost, matching pi-devbox:

  lint.yml                 cheap checks, every push/PR — the ONLY workflow a
                           push to main triggers
  validate.yml             amd64 build + smoke test — pull_request and
                           workflow_dispatch only, no push trigger at all
  docker-publish-split.yml the release path, tag-only (unchanged)

docs-check (DOCKER_HUB.md vs HUB_TEMPLATE) moves from validate.yml to lint.yml.
It needs no image, and anything that needs no image belongs in the workflow that
actually runs on push — otherwise the doc-drift guard would have been silently
lost when validate.yml stopped running on pushes. That guard earns its keep: it
has caught real drift.

validate-base/validate-omos additionally keep a `github.event_name != 'push'`
clause. It is redundant now that the trigger is gone, and deliberately so:
re-adding a push trigger later cannot silently re-enable builds on every push.

Renamed lint.yml `Lint workflows` -> `Lint`, since it now covers Dockerfiles and
docs as well as workflows. No references to the old name existed.

Safe because the release path already fails closed: docker-publish-split.yml
pushes variant tags only after smoke-base/smoke-omos pass and promotes
base-latest last, so an aborted release leaves at worst an unreferenced
base-<hash> blob on Hub — never a half-published version tag. Pre-tag
validation remains available three ways: open a PR, dispatch Validate, or
dispatch docker-publish-split.yml against a throwaway tag with
promote_latest=false — the only route that also exercises a CHANGED base, which
validate.yml structurally cannot (it builds variants on Hub's base-latest).

Coverage lost is narrower than it looks: validate-base/validate-omos were
already skipped whenever a commit touched Dockerfile.base, rootfs/, or
entrypoint*.sh, so they only ever ran for variant-only changes — most usefully a
bare OPENCODE_VERSION bump, for which an explicit dispatch is now the
equivalent.

Verified with the CI-pinned actionlint 1.7.7 and scripts/check-workflow-shell.sh.
AGENTS.md updated in the same commit (file roles for both workflows + the
trigger-model convention). The CHANGELOG entry lands with the v2.9.0 changeset
in the following commit, which is one unreleased block covering all of v2.9.0.
This commit is contained in:
pi
2026-08-04 16:23:03 +02:00
parent d2f2396c4a
commit 5fb07e0a39
3 changed files with 70 additions and 42 deletions
+33 -9
View File
@@ -1,13 +1,20 @@
name: Lint workflows
name: Lint
# 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.
# The repo's cheap-checks workflow: everything that can be verified WITHOUT
# building an image. Runs on every push and PR, in ~30s, independent of the
# build pipeline. Three jobs: workflow lint (actionlint + the Gitea shell
# guard), Dockerfile lint (hadolint), and the DOCKER_HUB.md docs-sync check.
#
# Deliberately the ONLY workflow that runs on a push to main. validate.yml
# (amd64 build + smoke) is PR/dispatch-only and docker-publish-split.yml is
# tag-only, so pushing work-in-progress to main never starts an image build.
#
# Its original purpose, still the most important one: 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.
on:
push:
pull_request:
@@ -90,3 +97,20 @@ jobs:
- name: Run hadolint
run: hadolint Dockerfile.base Dockerfile.variant
docs-check:
# Fails if DOCKER_HUB.md is out of sync with what generate-dockerhub-md.py
# would produce from HUB_TEMPLATE. Keeps the two docs from drifting.
#
# Lives here rather than in validate.yml because it needs no image: keeping
# it in the cheap workflow means it still runs on every push to main now
# that validate.yml is PR/dispatch-only. Reproduce locally with
# `python3 scripts/generate-dockerhub-md.py --check`.
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- uses: actions/checkout@v4
- name: Check DOCKER_HUB.md is in sync with HUB_TEMPLATE
run: python3 scripts/generate-dockerhub-md.py --check
+33 -30
View File
@@ -1,9 +1,24 @@
name: Validate
# Lightweight validation on pushes to main. Builds single-arch (amd64),
# runs the smoke test, and checks image size — without pushing anything
# to Docker Hub. Tag pushes are handled by docker-publish-split.yml which
# does the full multi-arch split-base build-and-push.
# Image build + smoke test. This workflow has NO push trigger at all — it fires
# only on pull_request and on explicit workflow_dispatch.
#
# Division of labour across this repo's three workflows:
# lint.yml — cheap checks, every push/PR: workflow lint,
# Dockerfile lint, DOCKER_HUB.md sync. No builds.
# validate.yml (this file) — amd64 build + smoke test. PR / manual only.
# docker-publish-split.yml — the release path, tag-only: multi-arch build,
# smoke gates, then push + promote.
#
# Net effect: no image build can start from a push to main. A tag — or an
# explicit dispatch — is required. That is safe because the release path fails
# closed: variant tags are pushed only after smoke-base/smoke-omos pass, and
# base-latest is promoted last, so an aborted release leaves at worst an
# unreferenced base-<hash> blob on Hub, never a half-published version tag.
#
# To validate before tagging: open a PR, dispatch this workflow, or dispatch
# docker-publish-split.yml against a throwaway tag with promote_latest=false
# (the only route that also exercises a CHANGED BASE — see the trade-off below).
#
# Trade-off: variant builds here use the published `base-latest` image
# from Docker Hub as their parent, NOT a locally-built base. This is
@@ -22,18 +37,10 @@ name: Validate
# blind-spot applies.
on:
push:
branches:
- main
paths-ignore:
- 'CHANGELOG.md'
- 'README.md'
- 'DOCKER_HUB.md'
- 'deploy/**'
- '.gitleaks.toml'
pull_request:
branches:
- main
workflow_dispatch:
# 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
@@ -44,20 +51,6 @@ defaults:
shell: bash
jobs:
docs-check:
# Fails if DOCKER_HUB.md is out of sync with what generate-dockerhub-md.py
# would produce from README.md. Keeps the two docs from drifting.
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check DOCKER_HUB.md is in sync with README.md
run: |
python3 scripts/generate-dockerhub-md.py --check
base-change-warning:
# Surfaces a warning when this commit changes base-image inputs
# (Dockerfile.base, rootfs/, entrypoint*.sh) AND exports `base_changed` so
@@ -97,12 +90,21 @@ jobs:
printf '%s\n' "$changed"
else
echo "base_changed=false" >> "$GITHUB_OUTPUT"
echo "No base-image inputs changed in this commit — validate.yml fully exercises the published base-latest."
echo "No base-image inputs changed in this commit — validate-base/validate-omos will build against the published base-latest."
fi
validate-base:
needs: [base-change-warning]
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' }}
# Two gates, both must hold for this job to run:
# 1. base_changed != true — the documented blind spot: this workflow builds
# variants on top of Hub's base-latest, so a commit changing base inputs
# would either not exercise the change or hard-fail against a stale
# base-latest (see base-change-warning).
# 2. event_name != push — belt-and-braces. Redundant today (this workflow
# has no push trigger), kept deliberately so that re-adding a push
# trigger later cannot silently re-enable image builds on every push to
# main. If you intend that, remove this clause explicitly.
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' && github.event_name != 'push' }}
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
@@ -165,7 +167,8 @@ jobs:
validate-omos:
needs: [base-change-warning]
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' }}
# Same two gates as validate-base — see the comment there.
if: ${{ needs.base-change-warning.outputs.base_changed != 'true' && github.event_name != 'push' }}
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest