Files
ci-workflows/.gitea/workflows/reusable/oci-build-push-verify.yml
T
zcode-mulm e2b81e7ee6 fix(cleanup): per-run 缓存清理改容器 root 执行(PLAT-014,坑 #18 同族)
宿主 08-30 治理后为非 root gitea-runner,rm 容器 root 属主缓存文件必 EACCES
(Seabed run 917 两 attempt 测试全绿、清理步必死)。复用 job 已拉的 node-image
find -mindepth 1 -delete + 宿主 rmdir 自建目录。v3 不动;合并后打 v4,
per-run 调用方(Seabed)升 @v4,fixed 调用方(Reef)可留 @v3。
2026-08-31 20:52:23 +08:00

320 lines
13 KiB
YAML

# Reusable build+push+verify for est CI (host docker daemon).
#
# P1-7 verification status (2026-08-27, Gitea 1.27.2):
# - reusable workflow calls verified working (selftest caller:
# .gitea/workflows/selftest.yml)
# - checkout via mu-ref/actions-checkout@v4 + HEAD==SHA postcondition
# (tn host node v22; v3 restoration history in reusable/node-quality.yml
# header; the action repo must stay PUBLIC — act clones it anonymously)
# - registry identity is the triggering actor (github.actor), credentials
# flow exclusively through the REGISTRY_PASSWORD secret (no hardcoded
# usernames — coordination review pitfall 21)
# - proven conventions from the business-repo pipelines:
# verify() mounts + </dev/null stdin discipline, registry-neutral
# verifier contract (Gitea 1.27 sends Bearer AND Basic WWW-Authenticate
# headers joined by fetch — callers' verifiers must accept the boundary,
# see the business repos' verifier implementations), bare-repo@digest
# references (docker 29 rejects repo:tag@digest)
# - buildx registry cache optional (cache-registry-ref empty disables);
# flat two-level naming rule applies (est/ci-cache-<repo>)
name: reusable-oci-build-push-verify
on:
workflow_call:
inputs:
image-root:
required: false
type: string
default: git.moneywood.site/est
node-image:
required: true
type: string
description: "Digest-pinned helper image with node + docker CLI + verifier tooling"
image-matrix:
required: false
type: string
default: ""
description: "Multiline matrix: service|dockerfile|target per line (required for the built-in loop; script mode uses it only for the digest summary)"
platform:
required: false
type: string
default: linux/amd64
build-args:
required: false
type: string
default: ""
description: "Multiline KEY=VALUE build args (static values only — caller-side expressions are NOT evaluated in with:)"
revision-arg-name:
required: false
type: string
default: ""
description: "ARG name that receives the triggering commit SHA (a SOURCE_REVISION-style ARG); empty disables"
verify-command:
required: false
type: string
default: ""
description: "Repo verify script run inside the helper per image (receives --image/--index/--target/--architecture/--revision)"
cache-registry-ref:
required: false
type: string
default: ""
description: "e.g. est/ci-cache-<repo>; empty disables registry build cache"
registry-host:
required: false
type: string
default: git.moneywood.site
build-command:
required: false
type: string
default: ""
description: "Script mode: run this command inside the helper container (full mounts: sock/docker-config/buildx/CA/cache + envs) instead of the built-in matrix loop"
install-command:
required: false
type: string
default: ""
description: "Script mode only: runs in the SAME container before build-command (toolchains installed here must survive to the build phase)"
cache-dir:
required: false
type: string
default: /data/cache/ci/build
description: "Script mode: host-side cache dir mounted at /cache"
cache-mode:
required: false
type: string
default: fixed
description: "fixed | per-run (per-run = fresh mktemp child + always() cleanup; for from-zero tool caches)"
commit-env-name:
required: false
type: string
default: ""
description: "Script mode: env var receiving the commit SHA inside the container (e.g. EST_GIT_COMMIT)"
extra-env:
required: false
type: string
default: ""
description: "Script mode: multiline KEY=VALUE extra env (no spaces in values; expressions NOT evaluated)"
workspace:
required: false
type: string
default: /workspace
description: "Script mode: workspace mount point inside the container"
secrets:
REGISTRY_PASSWORD:
required: true
jobs:
build:
runs-on: build-docker
steps:
- name: checkout (mu-ref/actions-checkout; tn host node runs JS actions)
uses: https://git.moneywood.site/mu-ref/actions-checkout@v4
env:
GIT_SSL_CAINFO: /usr/local/share/ca-certificates/caddy-root-ca.crt
- name: verify HEAD equals triggering commit
run: |
set -eu
test "$(git rev-parse HEAD)" = "${GITHUB_SHA}"
- name: authenticate to gitea registry
env:
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
set -eu
printf '%s' "${REGISTRY_PASSWORD}" \
| docker login "${{ inputs.registry-host }}" -u "${{ github.actor }}" --password-stdin
- name: prepare script-mode cache dir
if: inputs.build-command != ''
run: |
set -eu
cache_root="${{ inputs.cache-dir }}"
mkdir -p "$cache_root"
chmod 0777 "$cache_root"
if [ "${{ inputs.cache-mode }}" = "per-run" ]; then
cache_dir="$(mktemp -d "$cache_root/run-XXXXXXXX")"
else
cache_dir="$cache_root"
fi
chmod 0777 "$cache_dir"
find "$cache_dir" -mindepth 1 -maxdepth 1 -type d -exec chmod 0777 {} + 2>/dev/null || true
printf 'TOOL_CACHE_DIR=%s\n' "$cache_dir" >>"$GITHUB_ENV"
- name: run build script (script mode)
if: inputs.build-command != ''
env:
REGISTRY_AUTH_USER: ${{ github.actor }}
REGISTRY_AUTH_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
COMMIT_ENV_NAME: ${{ inputs.commit-env-name }}
run: |
set -eu
mount_args="-v /var/run/docker.sock:/var/run/docker.sock"
if [ -f "$HOME/.docker/config.json" ]; then
mount_args="$mount_args -v $HOME/.docker/config.json:/root/.docker/config.json:ro"
fi
if [ -f /usr/libexec/docker/cli-plugins/docker-buildx ]; then
mount_args="$mount_args -v /usr/libexec/docker/cli-plugins/docker-buildx:/root/.docker/cli-plugins/docker-buildx:ro"
fi
env_args=""
if [ -n "${COMMIT_ENV_NAME}" ]; then
env_args="${env_args} -e ${COMMIT_ENV_NAME}=${GITHUB_SHA}"
fi
env_args="${env_args} -e REGISTRY_AUTH_USER -e REGISTRY_AUTH_PASSWORD"
while IFS= read -r line; do
[ -n "$line" ] && env_args="${env_args} -e $line"
done <<EOF
${{ inputs.extra-env }}
EOF
# shellcheck disable=SC2086
docker run --rm --add-host npm.cache.est:172.17.0.1 \
$mount_args \
-v "$PWD:${{ inputs.workspace }}" -w "${{ inputs.workspace }}" \
-v "${TOOL_CACHE_DIR}:/cache" -e TMPDIR=/cache \
-v /usr/local/share/ca-certificates/est-bundle.crt:/ca/est-bundle.crt:ro \
-e SSL_CERT_FILE=/ca/est-bundle.crt \
-e NODE_EXTRA_CA_CERTS=/ca/est-bundle.crt \
$env_args \
"${{ inputs.node-image }}" sh -euxc '${{ inputs.install-command }} && ${{ inputs.build-command }}'
- name: build push and verify image matrix
if: inputs.build-command == ''
env:
REGISTRY_AUTH_USER: ${{ github.actor }}
REGISTRY_AUTH_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
NODE_IMAGE: ${{ inputs.node-image }}
IMAGE_ROOT: ${{ inputs.image-root }}
PLATFORM: ${{ inputs.platform }}
VERIFY_COMMAND: ${{ inputs.verify-command }}
CACHE_REGISTRY_REF: ${{ inputs.cache-registry-ref }}
REVISION_ARG_NAME: ${{ inputs.revision-arg-name }}
IMAGE_MATRIX: ${{ inputs.image-matrix }}
run: |
set -eu
IMAGE_TAG="git-${GITHUB_SHA}"
printf '%s' "${GITHUB_SHA}" | grep -Eq '^[0-9a-f]{40}$'
mkdir -p .tmp-ci
digest_file=".tmp-ci/digests.txt"
: >"$digest_file"
cache_args=""
if [ -n "${CACHE_REGISTRY_REF}" ]; then
cache_args="--cache-from type=registry,ref=${CACHE_REGISTRY_REF} --cache-to type=registry,ref=${CACHE_REGISTRY_REF},mode=max"
fi
# verify() keeps stdin closed so it cannot steal the outer while-read heredoc.
verify() {
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$HOME/.docker/config.json:/root/.docker/config.json:ro" \
-v /usr/local/share/ca-certificates/est-bundle.crt:/ca/est-bundle.crt:ro \
-v /usr/libexec/docker/cli-plugins/docker-buildx:/root/.docker/cli-plugins/docker-buildx:ro \
-v "$PWD:/w" -w /w \
-e SSL_CERT_FILE=/ca/est-bundle.crt \
-e NODE_EXTRA_CA_CERTS=/ca/est-bundle.crt \
-e IMAGE_CONFIG -e EXPECTED_REVISION -e EXPECTED_BASELINE \
-e EXPECTED_ARCHITECTURE -e MANIFEST_FILE \
-e REGISTRY_AUTH_USER -e REGISTRY_AUTH_PASSWORD \
"${NODE_IMAGE}" "$@" < /dev/null
}
build_args=""
while IFS= read -r line; do
# Values must not contain spaces: expansion results do not go
# through quote removal, so embedded quotes would end up in the
# ARG name (observed: '"KEY=value' never matches a Dockerfile ARG).
[ -n "$line" ] && build_args="${build_args} --build-arg ${line}"
done <<EOF
${{ inputs.build-args }}
EOF
if [ -z "${IMAGE_MATRIX:-}" ]; then
echo "image-matrix is required for the built-in loop (or use build-command script mode)" >&2
exit 64
fi
if [ -n "${REVISION_ARG_NAME}" ]; then
build_args="${build_args} --build-arg ${REVISION_ARG_NAME}=${GITHUB_SHA}"
fi
while IFS='|' read -r service dockerfile target; do
# Multiline inputs can carry a trailing empty line — skip it
# instead of building an invalid "est/:tag" reference.
[ -n "${service:-}" ] || continue
image_repo="${IMAGE_ROOT}/${service}"
image="${image_repo}:${IMAGE_TAG}"
# shellcheck disable=SC2086
docker buildx build \
--file "${dockerfile}" \
--target "${target}" \
--platform "${PLATFORM}" \
${build_args} \
${cache_args} \
--tag "${image}" \
--provenance=mode=max \
--sbom=true \
--push .
manifest_file=".tmp-ci/index-${service}.json"
docker buildx imagetools inspect "${image}" --raw >"$manifest_file"
if [ -n "${VERIFY_COMMAND}" ]; then
# VERIFY_COMMAND carries its own interpreter (e.g. "node x.mjs"
# or "sh x.sh"); flags below are appended for the callee.
# shellcheck disable=SC2086
verify ${VERIFY_COMMAND} \
--image "${image}" \
--index "$manifest_file" \
--target "${target}" \
--architecture "${PLATFORM##*/}" \
--revision "${GITHUB_SHA}"
fi
index_digest=$(docker buildx imagetools inspect "${image}" | awk '/^Digest:/{print $2}')
printf '%s|%s|%s\n' "${service}" "${IMAGE_TAG}" "${index_digest}" >>"$digest_file"
done <<'IMAGES'
${{ inputs.image-matrix }}
IMAGES
{
echo "## V4 fresh image digests"
echo
echo "revision: ${GITHUB_SHA}"
echo
echo '```'
cat "$digest_file"
echo '```'
} >>"${GITHUB_STEP_SUMMARY:-/dev/stdout}"
cat "$digest_file"
- name: digest summary (script mode)
if: inputs.build-command != '' && inputs.image-matrix != ''
env:
IMAGE_ROOT: ${{ inputs.image-root }}
run: |
set -eu
image_tag="git-${GITHUB_SHA}"
digest_file=".tmp-ci/digests-script.txt"
mkdir -p .tmp-ci
: >"$digest_file"
while IFS='|' read -r service dockerfile target; do
[ -n "${service:-}" ] || continue
index_digest=$(docker buildx imagetools inspect "${IMAGE_ROOT}/${service}:${image_tag}" | awk '/^Digest:/{print $2}')
printf '%s|%s|%s\n' "$service" "$image_tag" "$index_digest" >>"$digest_file"
done <<'IMAGES'
${{ inputs.image-matrix }}
IMAGES
{
echo "## V4 fresh image digests"
echo
echo "revision: ${GITHUB_SHA}"
echo
echo '```'
cat "$digest_file"
echo '```'
} >>"${GITHUB_STEP_SUMMARY:-/dev/stdout}"
cat "$digest_file"
# PLAT-014: same ownership fix as node-quality — see the comment there
# (host non-root runner cannot rm container-root-owned cache files).
- name: cleanup per-run cache dir (container root)
if: always() && inputs.build-command != '' && inputs.cache-mode == 'per-run'
run: |
set -eu
[ -n "${TOOL_CACHE_DIR:-}" ] || exit 0
docker run --rm \
-v "${TOOL_CACHE_DIR}":/cleanup-root \
"${{ inputs.node-image }}" \
find /cleanup-root -mindepth 1 -delete
rmdir "${TOOL_CACHE_DIR}"