Files
MoShiro/ci/release-linux.sh
T
justme 490b008a57
ci/woodpecker/push/dev-image Pipeline is running
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
scripts: unify Linux native builds and retain restore caches [CI]
Build musl and glibc for amd64 and arm64 through one native package path. Reuse musl outputs for OCI images and OpenWrt installers, and export binaries inside a rootless unshare session.

Restore the executable dependency graph into a shared persistent NuGet cache, build managed dependencies once, and run isolated native publishes in parallel.

Clean temporary outputs and expired image layers under store locks, retire inactive legacy stores after seven days, and retain two toolchain exports. Add regression coverage for staging, extraction, packaging and cleanup boundaries.
2026-10-21 12:29:00 +08:00

197 lines
8.9 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Build a glibc NativeAOT Backend OCI image with statically linked SQLite; publishing is optional.
# glibc and a dynamic executable are required: the musl AotAnywhere output is fully static and cannot dlopen libssl, so the signing code fails at startup.
set -euo pipefail
arch="${1:?usage: release-linux.sh <amd64|arm64>}"
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tag="${CI_COMMIT_TAG:-}"
if [[ -z "$tag" ]]; then
[[ "${CI_PIPELINE_EVENT:-}" == push && "${CI_COMMIT_BRANCH:-}" == main && "${CI_COMMIT_MESSAGE:-}" == *"[CI]"* ]] || {
echo "untagged backend builds require a main push with [CI] in the commit message" >&2
exit 2
}
fi
# 版本:tag 发版就是 tag 的版本,开发构建是 v<下一个补丁版本>-dev.<N>-<提交>(ci/version.sh);写进工作区的 Directory.Build.props
source "$root/ci/version.sh"
moshiro_apply_version
version="$MOSHIRO_VERSION"
image_publish="${IMAGE_PUBLISH:-true}"
[[ "$image_publish" == true || "$image_publish" == false ]] || {
echo "IMAGE_PUBLISH must be true or false" >&2; exit 2;
}
if [[ "$image_publish" == true ]]; then
[[ -n "${ZOT_USERNAME:-}" && -n "${ZOT_SECRET:-}" ]] || {
echo "Zot credentials are required" >&2; exit 2;
}
fi
case "$arch" in
amd64) rid=linux-x64 ;;
arm64) rid=linux-arm64 ;;
*) echo "unsupported architecture: $arch" >&2; exit 2 ;;
esac
registry="${ZOT_REGISTRY:-registry.oeo.one}"
image="${ZOT_IMAGE_REPOSITORY:-$registry/moshiro/moshiro-backend}"
output="$root/.release/linux-$arch/backend"
build_root="$root/.release/build/linux-$arch-backend"
container_context="$root/.release/container/linux-$arch"
mkdir -p "$output" "$build_root" "$container_context/app" "$container_context/data"
store_ready=false
cleanup() {
local status=$?
trap - EXIT
if [[ "$store_ready" == true ]]; then
bash "$root/ci/cleanup-build-cache.sh" || echo 'warning: build cache cleanup failed' >&2
fi
if [[ "$image_publish" == true ]]; then
rm -rf "$output" "$build_root" "$container_context"
rm -f "$root/.release/auth/zot-linux-$arch.json"
fi
exit "$status"
}
trap cleanup EXIT
# 构建信息:tag 发布用 tag,开发构建用 v<版本>-dev.<N>-<提交>(MOSHIRO_TAG 已由 ci/version.sh 设好)
export MOSHIRO_COMMIT="${MOSHIRO_COMMIT:-${CI_COMMIT_SHA:0:10}}"
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
# 这里的前端带着这次构建的版本号(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; }
sqlite_cache_root="${MOSHIRO_SQLITE_CACHE:-${NUGET_PACKAGES:-${HOME:-/home/justme}/.nuget/packages}/moshiro-sqlite-static}"
sqlite_cache_key="$(python3 - "$root" <<'PY'
import hashlib, json, sys, xml.etree.ElementTree as ET
from pathlib import Path
root = Path(sys.argv[1])
global_json = json.loads((root / "global.json").read_text())
zig = next(p.attrib["VersionOverride"] for p in ET.parse(root / "backend/Directory.Build.props").iter("PackageReference") if p.attrib.get("Include") == "Vezel.Zig.Toolsets.linux-x64")
packages = ET.parse(root / "Directory.Packages.props").getroot()
sqlite = {p.tag: p.text for p in packages.iter() if p.tag.startswith("MoShiroSqliteSource")}
fingerprint = {
"sdk": global_json["sdk"]["version"],
"aotanywhere": global_json["msbuild-sdks"]["StuDev.AotAnywhere"],
"zig": zig,
"sqlite": sqlite,
"compile-target": (root / "backend/src/MoShiro.Backend/StaticSqlite.targets").read_text(),
}
print(hashlib.sha256(json.dumps(fingerprint, sort_keys=True).encode()).hexdigest()[:24])
PY
)"
mkdir -p "$sqlite_cache_root/locks"
# Stable cache paths let MSBuild reuse the SQLite amalgamation object between
# fresh Woodpecker workspaces. Lock per RID so overlapping retries cannot write
# the same object concurrently; amd64 and arm64 builds remain parallel.
# --close keeps the lock fd out of reusable MSBuild worker processes. Otherwise
# an idle node-reuse worker can inherit it and block later pipelines indefinitely.
(
cd "$root/backend"
flock --close --wait 900 "$sqlite_cache_root/locks/$rid.lock" \
dotnet publish src/MoShiro.Backend \
-m:8 -c Release -r "$rid" --self-contained true \
-p:DebugType=none -p:DebugSymbols=false \
-p:PublishAot=true -p:UseAotAnywhere=true -p:SqliteStaticLink=true -p:OpenApiGenerateDocuments=false \
-p:SqliteStaticCacheRoot="$sqlite_cache_root" -p:SqliteStaticCacheKey="$sqlite_cache_key" \
--artifacts-path "$build_root" -o "$output"
)
[[ -x "$output/moshiro-backend" ]] || { echo "NativeAOT Backend executable is missing" >&2; exit 1; }
[[ "$(wc -c < "$output/moshiro-backend")" -gt 2000000 ]] || { echo "Backend output is an apphost, not a NativeAOT executable" >&2; exit 1; }
install -m 0755 "$output/moshiro-backend" "$container_context/app/moshiro-backend"
: > "$container_context/data/.keep"
# Woodpecker's local backend isolates HOME per workflow. Keep Buildah's image
# store outside that temporary directory so the runtime base image is reusable
# across releases. Separate stores avoid concurrent amd64/arm64 writers.
buildah_home="${BUILDAH_HOME:-/home/justme/woodpecker/buildah-home/$arch}"
mkdir -p "$buildah_home"
exec 8> "$buildah_home/.lock"
flock -w 1800 8
export HOME="$buildah_home"
source "$root/ci/buildah-env.sh"
moshiro_setup_buildah "$buildah_home"
store_ready=true
local_image="localhost/moshiro-backend:ci-$arch"
buildah bud --tls-verify=true --arch "$arch" \
--build-arg "APP_VERSION=$version" \
-f "$root/packaging/container/Backend.Containerfile" \
-t "$local_image" "$container_context"
if [[ "$image_publish" == false ]]; then
echo "built local Backend image for $arch; not publishing it"
exit 0
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
logged_in=true
break
fi
if [[ "$attempt" -lt 4 ]]; then
echo "Zot login attempt $attempt failed; retrying in 3 seconds" >&2
sleep 3
fi
done
[[ "$logged_in" == true ]] || { echo "could not authenticate to Zot after 4 attempts" >&2; exit 1; }
push_tag() {
local target_tag="$1" digest_file="$2" pushed=false
for attempt in 1 2 3; do
if buildah push --tls-verify=true --digestfile "$digest_file" "$local_image" "docker://$image:$target_tag"; then
pushed=true
break
fi
if [[ "$attempt" -lt 3 ]]; then
echo "Zot push attempt $attempt failed; retrying in 5 seconds" >&2
sleep 5
fi
done
[[ "$pushed" == true ]] || { echo "could not push image to Zot after 3 attempts" >&2; return 1; }
echo "pushed Zot image $image:$target_tag"
}
if [[ -n "${IMAGE_TAGS:-}" ]]; then
IFS=',' read -r -a image_tags <<< "$IMAGE_TAGS"
((${#image_tags[@]} > 0)) || { echo "IMAGE_TAGS must contain at least one tag" >&2; exit 2; }
for image_tag in "${image_tags[@]}"; do
[[ "$image_tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || {
echo "invalid image tag: $image_tag" >&2
exit 2
}
done
push_pids=()
digest_files=()
for image_tag in "${image_tags[@]}"; do
digest_file="$output/image-$image_tag.digest"
push_tag "$image_tag" "$digest_file" &
push_pids+=("$!")
digest_files+=("$digest_file")
done
push_status=0
for push_pid in "${push_pids[@]}"; do
wait "$push_pid" || push_status=1
done
[[ "$push_status" == 0 ]] || { echo "one or more image tag pushes failed" >&2; exit 1; }
image_digest="$(cat "${digest_files[0]}")"
for digest_file in "${digest_files[@]}"; do
[[ "$(cat "$digest_file")" == "$image_digest" ]] || { echo "published tags have different image digests" >&2; exit 1; }
done
printf '%s\n' "$image_digest" > "$output/image.digest"
else
push_tag "$version-$arch" "$output/image.digest"
if [[ -z "$tag" ]]; then
push_tag "$version" "$output/image.digest"
fi
fi
if [[ -z "$tag" ]]; then
bash "$root/ci/publish-release-offer.sh" backend "$image" "$(cat "$output/image.digest")" "$MOSHIRO_TAG" dev "$output/release"
fi