198 lines
8.9 KiB
Bash
198 lines
8.9 KiB
Bash
#!/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
|