Files
MoShiro/ci/release-linux.sh
justme 099c34c715
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was canceled
ci/woodpecker/push/verify Pipeline was canceled
Raise build parallelism for the 44-core CI host [CI]
2026-10-21 15:34:41 +08:00

198 lines
8.9 KiB
Bash
Raw Permalink 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 步骤先构建好了;main 的 [CI] 构建用同一条流水线里 web-build workflow 的产物(带这次的版本号),
# 等不到(它没跑或失败了)才自己构建兜底,依赖共用 node_modules 缓存
if [[ ! -f "$root/web/display/dist/index.html" || ! -f "$root/web/admin/dist/index.html" ]]; then
bash "$root/ci/shared-web.sh" fetch || 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"
bash "$root/ci/with-aot-slot.sh" flock --close --wait 900 "$sqlite_cache_root/locks/$rid.lock" \
dotnet publish src/MoShiro.Backend \
-m:8 -nodeReuse:false -c Release -r "$rid" --self-contained true \
--no-restore \
-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