ci: build the dev images in parallel with the checks and gate only the push [CI]
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/dev-image Pipeline failed

This commit is contained in:
justme committed 2026-10-19 13:58:21 +08:00
1 parent b33e50696d
commit 1ccbb847b5
6 files changed
+99 -8

No files matched your search

+2 -4
View File
@@ -15,10 +15,8 @@ when:
branch: main
evaluate: 'CI_COMMIT_MESSAGE contains "[CI]"'
# 先等后端测试和前端构建都通过再出镜像(提交信息写 [SKIP TESTS] 只跳过后端测试)
depends_on:
- verify
- web-build
# 镜像构建和 verify(后端快速测试)、web-build 并行跑,不再等它们:构建好之后、推送之前由 ci/wait-for-checks.sh 等这两个 workflow 都成功
# (任何一个失败就不推;提交信息写 [SKIP TESTS] 只跳过后端测试,verify 照样是成功)。机器有 6 个 workflow 槽(WOODPECKER_MAX_WORKFLOWS),三个同时跑放得下。
concurrency: 1
+4
View File
@@ -96,6 +96,7 @@ if [[ "$component" == backend ]]; then
ZOT_IMAGE_REPOSITORY="$repository" \
IMAGE_TAGS="latest,$image_version" \
IMAGE_PUBLISH=true \
MOSHIRO_PUSH_GATE="$root/ci/wait-for-checks.sh" \
run_bounded 30m bash "$root/ci/release-linux.sh" "$arch" backend
exit 0
fi
@@ -150,6 +151,9 @@ run_bounded 30m buildah bud --layers --network host --tls-verify=true --ulimit n
-f "$context/Containerfile" \
-t "$local_image" "$context"
# 构建和测试是并行的:镜像已经好了,推送之前等 verify 和 web-build 都过(任何一个失败就不推)
bash "$root/ci/wait-for-checks.sh"
logged_in=false
for attempt in 1 2 3 4; do
if run_bounded 2m buildah login --tls-verify=true --username "$ZOT_USERNAME" --password-stdin "$registry" <<< "$ZOT_SECRET" >/dev/null; then
+4 -2
View File
@@ -45,8 +45,8 @@ export MOSHIRO_BUILT_AT="${MOSHIRO_BUILT_AT:-$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
# 前端先构建:Backend.Api 在 web/*/dist 存在时把它们内嵌进程序(展示端 /,管理端 /admin/),这样 passkey 能绑定在面板自己的域名上
# 发版流水线里有单独的 web-build 步骤先构建好了(并行的两个架构不能同时 npm ci);main 的 [CI] 构建只有一个 Backend 步骤,这里自己构建
if [[ ! -f "$root/web/display/dist/index.html" || ! -f "$root/web/admin/dist/index.html" ]]; then
if [[ -z "${NPM_CONFIG_CACHE:-}" && -d /home/justme/.npm ]]; then export NPM_CONFIG_CACHE=/home/justme/.npm; fi
(cd "$root" && npm ci --prefix web --prefer-offline && npm run build --prefix web)
# 这里的前端带着这次构建的版本号(apply_version 已经改了 package.json),不能用 web-build workflow 的产物;但依赖共用它的 node_modules 缓存(flock 防止同时读写),不再 npm ci
WEB_SKIP_LINT=1 bash "$root/ci/web-build.sh"
fi
[[ -f "$root/web/display/dist/index.html" && -f "$root/web/admin/dist/index.html" ]] || { echo "the web build did not produce both apps" >&2; exit 1; }
@@ -110,6 +110,8 @@ if [[ "$image_publish" == false ]]; then
fi
export REGISTRY_AUTH_FILE="$root/.release/auth/zot-linux-$arch.json"
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
# 开发镜像的构建和测试是并行的:镜像已经好了,推送之前等 verify 和 web-build 都过(见 ci/wait-for-checks.sh;发版流水线不设这个)
[[ -z "${MOSHIRO_PUSH_GATE:-}" ]] || bash "$MOSHIRO_PUSH_GATE"
logged_in=false
for attempt in 1 2 3 4; do
if buildah login --tls-verify=true --username "$ZOT_USERNAME" --password-stdin "$registry" <<< "$ZOT_SECRET" >/dev/null; then
+16 -1
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import argparse
import shutil
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
@@ -28,6 +29,20 @@ CONTAINERFILES = {
}
def copy_for_restore(path: Path, relative: Path, target: Path) -> None:
"""The restore layer must not change when only the build version does: CI rewrites <Version> in the workspace
Directory.Build.props (ci/version.sh), which used to invalidate the cached dependency restore on every commit.
Restore only needs the committed props (package versions); the source copy still carries the real version."""
if path.name == "Directory.Build.props":
committed = subprocess.run(
["git", "show", f"HEAD:backend/{relative.as_posix()}"], cwd=ROOT, capture_output=True, check=False
)
if committed.returncode == 0:
target.write_bytes(committed.stdout)
return
shutil.copy2(path, target)
def stage(component: str, runtime_identifier: str, destination: Path) -> None:
if destination.exists():
shutil.rmtree(destination)
@@ -49,7 +64,7 @@ def stage(component: str, runtime_identifier: str, destination: Path) -> None:
):
target = restore / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target)
copy_for_restore(path, relative, target)
target = source / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(path, target)
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# 推镜像之前的闸门:等这个提交的 verify(后端快速测试)和 web-build 两个 workflow 都成功。
# 镜像构建不再等它们(开发镜像的构建和测试并行跑,关键路径少了测试那一段);构建好之后、推送之前才来这里等结果,
# 任何一个失败就不推(和以前「测试没过不出镜像」一样)。
# 用法:wait-for-checks.sh (环境:CI_COMMIT_SHA、CI_REPO、CI_FORGE_URL,都是 Woodpecker 提供的)
set -euo pipefail
sha="${CI_COMMIT_SHA:?CI_COMMIT_SHA is required}"
repo="${CI_REPO:?CI_REPO is required}"
forge="${CI_FORGE_URL:-https://git.oeo.one}"
required=("ci/woodpecker/push/verify" "ci/woodpecker/push/web-build")
# 总共最多等这么久(秒);一个 workflow 在这么久之后还没出现,说明它没有被触发,当作失败,免得一直挂着
timeout_s="${MOSHIRO_CHECKS_TIMEOUT:-1200}"
interval_s="${MOSHIRO_CHECKS_INTERVAL:-5}"
auth=()
[[ -n "${GITEA_TOKEN:-}" ]] && auth=(-H "Authorization: token $GITEA_TOKEN")
status_of() {
# 每个 context 的最新状态;没有这个 context 输出 absent
local body="$1" context="$2"
python3 - "$context" "$body" <<'PY'
import json, sys
context, body = sys.argv[1], sys.argv[2]
try:
statuses = json.loads(body).get("statuses") or []
except ValueError:
print("unknown"); sys.exit(0)
matches = [s for s in statuses if s.get("context") == context]
print(matches[0].get("status", "unknown") if matches else "absent")
PY
}
deadline=$((SECONDS + timeout_s))
last=""
unreachable=0
while :; do
# 先直连(git.oeo.one 就在这台机器上,走代理回环反而可能不通),不行再用环境里的代理
url="$forge/api/v1/repos/$repo/commits/$sha/status"
body="$(curl -fsS --noproxy "*" --max-time 20 "${auth[@]}" "$url" 2>/dev/null || curl -fsS --max-time 20 "${auth[@]}" "$url" 2>/dev/null || true)"
if [[ -z "$body" ]]; then
unreachable=$((unreachable + 1))
if ((unreachable >= 12)); then echo "could not read the commit status from $forge; not pushing the images" >&2; exit 1; fi
sleep "$interval_s"
continue
fi
unreachable=0
summary=""
pending=0
for context in "${required[@]}"; do
state="$(status_of "$body" "$context")"
summary+="${context##*/}=$state "
case "$state" in
success) ;;
failure|error|killed|declined)
echo "check $context did not pass ($state); not pushing the images" >&2
exit 1
;;
*) pending=1 ;;
esac
done
[[ "$summary" == "$last" ]] || { echo "checks: $summary"; last="$summary"; }
[[ "$pending" == 0 ]] && exit 0
if ((SECONDS >= deadline)); then
echo "checks did not finish within ${timeout_s}s ($summary); not pushing the images" >&2
exit 1
fi
sleep "$interval_s"
done
+5 -1
View File
@@ -9,6 +9,7 @@ export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-/home/justme/.npm}"
web="$root/web"
dirs=(node_modules display/node_modules admin/node_modules)
mkdir -p "$cache"
exec 9> "$cache/.lock"
key="$(cat "$web/package-lock.json" "$web/package.json" "$web/display/package.json" "$web/admin/package.json" | sha256sum | cut -c1-16)"
@@ -36,6 +37,8 @@ save() {
(cd "$cache" && ls -1t | grep -v '\.tmp\.' | tail -n +4 | xargs -r rm -rf)
}
# 并行的 workflow(web-build 和开发镜像里的前端构建)会同时读写这份缓存:恢复和保存期间持锁
flock -w 600 9 || { echo "could not get the node_modules cache lock" >&2; exit 1; }
if restore; then
echo "restored node_modules from the cache ($key)"
else
@@ -44,6 +47,7 @@ else
npm ci --prefix "$web" --prefer-offline
save || echo "could not save the node_modules cache" >&2
fi
flock -u 9
# 展示端和管理端并行构建,再检查 CSS
pids=()
@@ -52,4 +56,4 @@ pids=()
failed=0
for pid in "${pids[@]}"; do wait "$pid" || failed=1; done
[[ "$failed" == 0 ]] || { echo "web build failed" >&2; exit 1; }
npm run lint:css --prefix "$web"
[[ -n "${WEB_SKIP_LINT:-}" ]] || npm run lint:css --prefix "$web"