From a9f78f99dfab2871d1adabf45d02594ed7d0f2dd Mon Sep 17 00:00:00 2001 From: JustMe Date: Fri, 9 Oct 2026 13:20:30 +0800 Subject: [PATCH] ci: version from the tag or the commits since it; dev images from main gated on tests; recognise -dev. tags [CI SKIP] --- .../{main-amd64-build.yml => dev-image.yml} | 13 ++++- .woodpecker/linux-release.yml | 2 + .woodpecker/verify.yml | 2 +- .woodpecker/web-build.yml | 2 +- .../Updates/BackendUpdates.cs | 7 ++- .../BackendUpdateTests.cs | 12 ++++ ...build-main-amd64.sh => build-dev-image.sh} | 30 +++++----- ci/package-cross-platform.ps1 | 8 +-- ci/release-cross-platform.ps1 | 10 ++-- ci/release-linux.sh | 22 ++------ ci/release-manifest.sh | 3 +- ci/release-node-images.sh | 8 +-- ci/version.sh | 56 +++++++++++++++++++ 13 files changed, 124 insertions(+), 51 deletions(-) rename .woodpecker/{main-amd64-build.yml => dev-image.yml} (77%) rename ci/{build-main-amd64.sh => build-dev-image.sh} (67%) create mode 100644 ci/version.sh diff --git a/.woodpecker/main-amd64-build.yml b/.woodpecker/dev-image.yml similarity index 77% rename from .woodpecker/main-amd64-build.yml rename to .woodpecker/dev-image.yml index feacd4e..0485a00 100644 --- a/.woodpecker/main-amd64-build.yml +++ b/.woodpecker/dev-image.yml @@ -7,12 +7,19 @@ clone: image: plugin-git settings: lfs: false + # 要拿到 tag 才算得出开发版本号(ci/version.sh:自最近 tag 的提交数) + tags: true when: - event: push branch: main evaluate: 'CI_COMMIT_MESSAGE contains "[CI]"' +# 先等后端测试和前端构建都通过再出镜像(提交信息写 [SKIP TESTS] 只跳过后端测试) +depends_on: + - verify + - web-build + concurrency: 1 steps: @@ -28,7 +35,7 @@ steps: RELEASE_SIGNING_KEY: from_secret: release_signing_key commands: - - bash ci/build-main-amd64.sh backend + - bash ci/build-dev-image.sh backend depends_on: [] - name: build-linux-amd64-agent @@ -43,7 +50,7 @@ steps: RELEASE_SIGNING_KEY: from_secret: release_signing_key commands: - - bash ci/build-main-amd64.sh agent + - bash ci/build-dev-image.sh agent depends_on: [] - name: build-linux-amd64-probe @@ -58,5 +65,5 @@ steps: RELEASE_SIGNING_KEY: from_secret: release_signing_key commands: - - bash ci/build-main-amd64.sh probe + - bash ci/build-dev-image.sh probe depends_on: [] diff --git a/.woodpecker/linux-release.yml b/.woodpecker/linux-release.yml index 861c08d..771f40c 100644 --- a/.woodpecker/linux-release.yml +++ b/.woodpecker/linux-release.yml @@ -22,6 +22,8 @@ steps: environment: NPM_CONFIG_CACHE: /home/justme/.npm commands: + # 版本取自 tag:先写进工作区(仓库里的版本号只是占位),前端才会带上对的版本 + - bash ci/version.sh apply - npm ci --prefix web --prefer-offline - timeout 600 npm run build --prefix web depends_on: [] diff --git a/.woodpecker/verify.yml b/.woodpecker/verify.yml index c9b527e..8ee56eb 100644 --- a/.woodpecker/verify.yml +++ b/.woodpecker/verify.yml @@ -11,7 +11,7 @@ clone: when: - event: pull_request - event: push - branch: dev + branch: main # On Gitea PR events, CI_COMMIT_MESSAGE is the PR title. steps: diff --git a/.woodpecker/web-build.yml b/.woodpecker/web-build.yml index a364415..761fdaa 100644 --- a/.woodpecker/web-build.yml +++ b/.woodpecker/web-build.yml @@ -11,7 +11,7 @@ clone: when: - event: pull_request - event: push - branch: dev + branch: main depends_on: - verify diff --git a/backend/src/MoShiro.Backend.Application/Updates/BackendUpdates.cs b/backend/src/MoShiro.Backend.Application/Updates/BackendUpdates.cs index 7af71ca..42a48a5 100644 --- a/backend/src/MoShiro.Backend.Application/Updates/BackendUpdates.cs +++ b/backend/src/MoShiro.Backend.Application/Updates/BackendUpdates.cs @@ -55,9 +55,14 @@ public sealed class BackendUpdates : IHostedService return "release"; } - return BuildInfo.Tag is { } tag && (tag.StartsWith("dev-", StringComparison.Ordinal) || tag.StartsWith("main-", StringComparison.Ordinal)) ? "dev" : "release"; + return IsDevTag(BuildInfo.Tag) ? "dev" : "release"; } + /// 开发构建的 tag:新格式 v0.4.1-dev.12-abc1234(含 -dev.),以及旧的 dev-<sha>、main-<sha>。 + public static bool IsDevTag(string? tag) => + tag is not null + && (tag.Contains("-dev.", StringComparison.Ordinal) || tag.StartsWith("dev-", StringComparison.Ordinal) || tag.StartsWith("main-", StringComparison.Ordinal)); + public async ValueTask GetAsync(CancellationToken ct) { string channel = ChannelOf(_options); diff --git a/backend/tests/MoShiro.Backend.Tests/BackendUpdateTests.cs b/backend/tests/MoShiro.Backend.Tests/BackendUpdateTests.cs index aee6078..a13e83a 100644 --- a/backend/tests/MoShiro.Backend.Tests/BackendUpdateTests.cs +++ b/backend/tests/MoShiro.Backend.Tests/BackendUpdateTests.cs @@ -30,6 +30,18 @@ public sealed class BackendUpdateTests : IAsyncLifetime Assert.Equal(expected, BackendUpdates.ChannelOf(new BackendOptions { UpdateChannel = "auto", ImageNamespace = imageNamespace })); } + [Theory] + [InlineData("v0.4.1-dev.12-044ce0c", true)] + [InlineData("dev-725fe17a7de8", true)] + [InlineData("main-725fe17a7d", true)] + [InlineData("v0.4.0", false)] + [InlineData("v0.4.1-rc.1", false)] + [InlineData(null, false)] + public void IsDevTag_RecognisesDevelopmentBuilds(string? tag, bool expected) + { + Assert.Equal(expected, BackendUpdates.IsDevTag(tag)); + } + private async Task PublishAsync(long seq, string channel = "dev") { string manifest = $$"""{"component":"backend","channel":"{{channel}}","image":"registry.example/moshiro-{{channel}}/moshiro-backend","digest":"{{Digest}}","tag":"{{channel}}-{{seq}}","version":"0.1.1","seq":{{seq}}}"""; diff --git a/ci/build-main-amd64.sh b/ci/build-dev-image.sh similarity index 67% rename from ci/build-main-amd64.sh rename to ci/build-dev-image.sh index 15d75ab..9f2b5c9 100644 --- a/ci/build-main-amd64.sh +++ b/ci/build-dev-image.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash -# Build and push one opt-in main Linux amd64 image to the dev repository in Zot. +# Build and push one opt-in Linux amd64 development image (main push with [CI]) to the moshiro-dev repository in Zot. set -euo pipefail [[ "${CI_PIPELINE_EVENT:-}" == push && "${CI_COMMIT_BRANCH:-}" == main && "${CI_COMMIT_MESSAGE:-}" == *"[CI]"* ]] || { - echo "main amd64 builds require a main push with [CI] in the commit message" >&2 + echo "development image builds require a main push with [CI] in the commit message" >&2 exit 2 } -component="${1:?usage: build-main-amd64.sh }" +component="${1:?usage: build-dev-image.sh }" case "$component" in backend|agent|probe) ;; *) echo "unsupported component: $component" >&2; exit 2 ;; @@ -22,20 +22,22 @@ esac } root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +# 版本:v<下一个补丁版本>-dev.<自最近 tag 的提交数>-<提交前 7 位>(见 ci/version.sh);写进工作区的 Directory.Build.props,镜像标签、构建信息、签名清单都用它 +source "$root/ci/version.sh" +moshiro_apply_version registry="${ZOT_REGISTRY:-registry.oeo.one}" repository="$registry/moshiro-dev/moshiro-$component" -work="$root/.release/main-amd64-build" -base_home="/home/justme/woodpecker/buildah-home/main-amd64-build-$component" -short_sha="${CI_COMMIT_SHA:0:12}" -dev_tag="dev-$short_sha" +work="$root/.release/dev-image-build" +base_home="/home/justme/woodpecker/buildah-home/dev-image-build-$component" +# 镜像标签不带 v(和正式版一致):latest 和 <版本>;构建信息和清单里的 tag 带 v +image_version="$MOSHIRO_VERSION" mkdir -p "$work" "$base_home" if [[ "$component" == backend ]]; then - export MOSHIRO_TAG="$dev_tag" BUILDAH_HOME="$base_home" \ ZOT_REGISTRY="$registry" \ ZOT_IMAGE_REPOSITORY="$repository" \ - IMAGE_TAGS="latest,$dev_tag" \ + IMAGE_TAGS="latest,$image_version" \ IMAGE_PUBLISH=true \ bash "$root/ci/release-linux.sh" amd64 backend exit 0 @@ -45,12 +47,12 @@ export HOME="$base_home" export BUILDAH_HOME="$base_home" export REGISTRY_AUTH_FILE="$work/$component-auth.json" context="$work/$component" -local_image="localhost/moshiro-$component:main-amd64-check" +local_image="localhost/moshiro-$component:dev-amd64-check" python3 "$root/ci/stage-node-image.py" "$component" "$context" buildah bud --layers --network host --tls-verify=true --arch amd64 \ - --build-arg APP_VERSION=main \ - --build-arg "MOSHIRO_TAG=$dev_tag" --build-arg "MOSHIRO_COMMIT=${CI_COMMIT_SHA:0:10}" --build-arg "MOSHIRO_BUILT_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --build-arg "APP_VERSION=$image_version" \ + --build-arg "MOSHIRO_TAG=$MOSHIRO_TAG" --build-arg "MOSHIRO_COMMIT=${CI_COMMIT_SHA:0:10}" --build-arg "MOSHIRO_BUILT_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ -f "$context/Containerfile" \ -t "$local_image" "$context" @@ -67,7 +69,7 @@ for attempt in 1 2 3 4; do done [[ "$logged_in" == true ]] || { echo "could not authenticate to Zot after 4 attempts" >&2; exit 1; } -for tag in latest "$dev_tag"; do +for tag in latest "$image_version"; do pushed=false for attempt in 1 2 3; do if buildah push --tls-verify=true --digestfile "$work/$component.digest" "$local_image" "docker://$repository:$tag"; then @@ -86,5 +88,5 @@ done # 发布清单:给镜像的 digest 签名(私钥只在 CI 密钥里),放进镜像仓库(<仓库>-manifest:dev),面板在线时再登记一份加速。 # 没有配置签名密钥(例如 fork)就只构建镜像、不发清单。 if [[ -n "${RELEASE_SIGNING_KEY:-}" ]]; then - bash "$root/ci/publish-release-offer.sh" "$component" "$repository" "$(cat "$work/$component.digest")" "$dev_tag" dev "$work/$component-release" + bash "$root/ci/publish-release-offer.sh" "$component" "$repository" "$(cat "$work/$component.digest")" "$MOSHIRO_TAG" dev "$work/$component-release" fi diff --git a/ci/package-cross-platform.ps1 b/ci/package-cross-platform.ps1 index fba5216..290e77f 100644 --- a/ci/package-cross-platform.ps1 +++ b/ci/package-cross-platform.ps1 @@ -10,10 +10,10 @@ if (-not $env:GITEA_TOKEN) { $version = $Matches[1] $root = Split-Path -Parent $PSScriptRoot $backend = Join-Path $root 'backend' -$projectVersion = [regex]::Match((Get-Content (Join-Path $backend 'Directory.Build.props') -Raw), '([^<]+)').Groups[1].Value -if ($version -ne $projectVersion) { - throw "Tag version $version does not match project version $projectVersion." -} +# 版本取自 tag:写进工作区的 Directory.Build.props(仓库里的 只是占位,发版不用再提交 bump) +$propsPath = Join-Path $backend 'Directory.Build.props' +$props = Get-Content $propsPath -Raw +Set-Content -LiteralPath $propsPath -Value ([regex]::Replace($props, '[^<]*', "$version")) -NoNewline $tarProject = Join-Path $PSScriptRoot 'MoShiro.Tar\MoShiro.Tar.csproj' $tarTool = Join-Path $PSScriptRoot 'MoShiro.Tar\bin\Release\net10.0\MoShiro.Tar.dll' diff --git a/ci/release-cross-platform.ps1 b/ci/release-cross-platform.ps1 index 2d3d60b..e83b2ff 100644 --- a/ci/release-cross-platform.ps1 +++ b/ci/release-cross-platform.ps1 @@ -20,10 +20,10 @@ if (-not $env:CI_COMMIT_TAG -or $env:CI_COMMIT_TAG -notmatch '^v(\d+\.\d+\.\d+(? $version = $Matches[1] $root = Split-Path -Parent $PSScriptRoot $backend = Join-Path $root 'backend' -$projectVersion = [regex]::Match((Get-Content (Join-Path $backend 'Directory.Build.props') -Raw), '([^<]+)').Groups[1].Value -if ($version -ne $projectVersion) { - throw "Tag version $version does not match project version $projectVersion." -} +# 版本取自 tag:写进工作区的 Directory.Build.props(仓库里的 只是占位,发版不用再提交 bump) +$propsPath = Join-Path $backend 'Directory.Build.props' +$props = Get-Content $propsPath -Raw +Set-Content -LiteralPath $propsPath -Value ([regex]::Replace($props, '[^<]*', "$version")) -NoNewline if ($RuntimeIdentifier -in @('win-x64', 'win-arm64')) { $vcArchitecture = if ($RuntimeIdentifier -eq 'win-x64') { 'x64' } else { 'arm64' } @@ -52,7 +52,7 @@ $buildRoot = Join-Path $root ".release\build\$RuntimeIdentifier-$Component" $null = New-Item -ItemType Directory -Force -Path $output # 构建信息:程序里显示的 tag、提交、构建时间 -$env:MOSHIRO_TAG = if ($env:CI_COMMIT_TAG) { $env:CI_COMMIT_TAG } else { "main-$($env:CI_COMMIT_SHA.Substring(0, 10))" } +$env:MOSHIRO_TAG = $env:CI_COMMIT_TAG $env:MOSHIRO_COMMIT = $env:CI_COMMIT_SHA.Substring(0, 10) $env:MOSHIRO_BUILT_AT = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') diff --git a/ci/release-linux.sh b/ci/release-linux.sh index 2d403e3..a48ea72 100644 --- a/ci/release-linux.sh +++ b/ci/release-linux.sh @@ -6,19 +6,16 @@ set -euo pipefail arch="${1:?usage: release-linux.sh }" root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" tag="${CI_COMMIT_TAG:-}" -if [[ -n "$tag" ]]; then - [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { - echo "release must run from a vMAJOR.MINOR.PATCH tag" >&2 - exit 2 - } - version="${tag#v}" -else +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 } - version=main fi +# 版本:tag 发版就是 tag 的版本,开发构建是 v<下一个补丁版本>-dev.-<提交>(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; @@ -34,12 +31,6 @@ case "$arch" in *) echo "unsupported architecture: $arch" >&2; exit 2 ;; esac -project_version="$(sed -n 's#.*\(.*\).*#\1#p' "$root/backend/Directory.Build.props" | head -1)" -if [[ -n "$tag" && "$version" != "$project_version" ]]; then - echo "tag version $version does not match project version $project_version" >&2 - exit 1 -fi - registry="${ZOT_REGISTRY:-registry.oeo.one}" image="${ZOT_IMAGE_REPOSITORY:-$registry/moshiro/moshiro-backend}" output="$root/.release/linux-$arch/backend" @@ -47,8 +38,7 @@ 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" -# 构建信息:tag 发布用 tag,main 构建用 main-<提交> -export MOSHIRO_TAG="${MOSHIRO_TAG:-${tag:-main-${CI_COMMIT_SHA:0:10}}}" +# 构建信息:tag 发布用 tag,开发构建用 v<版本>-dev.-<提交>(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)}" diff --git a/ci/release-manifest.sh b/ci/release-manifest.sh index 2594838..902a214 100644 --- a/ci/release-manifest.sh +++ b/ci/release-manifest.sh @@ -14,7 +14,8 @@ repository="${2:?}"; digest="${3:?}"; tag="${4:?}"; channel="${5:?}"; outdir="${ [[ -n "${RELEASE_SIGNING_KEY:-}" ]] || { echo "RELEASE_SIGNING_KEY is required" >&2; exit 2; } root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -version="$(sed -n 's#.*\(.*\).*#\1#p' "$root/backend/Directory.Build.props" | head -1)" +source "$root/ci/version.sh" +version="$MOSHIRO_VERSION" commit="$(git -C "$root" rev-parse --short=10 HEAD 2>/dev/null || true)" built_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" seq="$(date +%s)" diff --git a/ci/release-node-images.sh b/ci/release-node-images.sh index 0d58b2c..31cdf0b 100644 --- a/ci/release-node-images.sh +++ b/ci/release-node-images.sh @@ -15,11 +15,9 @@ tag="${CI_COMMIT_TAG:-}" version="${tag#v}" root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -project_version="$(sed -n 's#.*\(.*\).*#\1#p' "$root/backend/Directory.Build.props" | head -1)" -if [[ "$version" != "$project_version" ]]; then - echo "tag version $version does not match project version $project_version" >&2 - exit 1 -fi +# 版本取自 tag:写进工作区的 Directory.Build.props(仓库里的 只是占位,发版不用再提交 bump) +source "$root/ci/version.sh" +moshiro_apply_version gitea_base="${GITEA_BASE:-http://127.0.0.1:3100}" gitea_base="${gitea_base%/}" diff --git a/ci/version.sh b/ci/version.sh new file mode 100644 index 0000000..42ffab3 --- /dev/null +++ b/ci/version.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# 算出这次构建的版本号。用法: +# source ci/version.sh # 得到 MOSHIRO_VERSION(不带 v)和 MOSHIRO_TAG(带 v) +# bash ci/version.sh apply # 同上,并把 MOSHIRO_VERSION 写进工作区里的 backend/Directory.Build.props 和三个 web/*/package.json(只改工作区,不提交) +# +# 规则: +# - 打 tag 发版(CI_COMMIT_TAG 是 vMAJOR.MINOR.PATCH):版本就是 tag 的版本。仓库里的 只是占位,发版不用再提交一次 bump; +# - 其余(main 上带 [CI] 的开发构建):vMAJOR.MINOR.(PATCH+1)-dev.-<提交前 7 位>, +# 基准是最近一个 v* tag,N 是这个 tag 之后的提交数。开发版本按语义化版本排在下一个正式版之前,同一个提交算出来永远一样。 +# 最近的 tag 要拿得到:CI 的克隆要带 tags(见 .woodpecker/dev-image.yml 的 clone 设置);没有任何 tag 时基准是 0.0.0。 +_moshiro_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +_moshiro_compute_version() { + local tag="${CI_COMMIT_TAG:-}" + if [[ -n "$tag" ]]; then + [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { + echo "release must run from a vMAJOR.MINOR.PATCH tag" >&2 + return 2 + } + MOSHIRO_VERSION="${tag#v}" + else + local sha="${CI_COMMIT_SHA:-$(git -C "$_moshiro_root" rev-parse HEAD)}" + local last base count major minor patch + last="$(git -C "$_moshiro_root" describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "$sha" 2>/dev/null || true)" + if [[ -n "$last" ]]; then + base="${last#v}" + base="${base%%-*}" + count="$(git -C "$_moshiro_root" rev-list --count "$last..$sha")" + else + base="0.0.0" + count="$(git -C "$_moshiro_root" rev-list --count "$sha")" + fi + IFS=. read -r major minor patch <<< "$base" + MOSHIRO_VERSION="$major.$minor.$((patch + 1))-dev.$count-${sha:0:7}" + fi + MOSHIRO_TAG="v$MOSHIRO_VERSION" + export MOSHIRO_VERSION MOSHIRO_TAG +} + +# 把算出来的版本写进工作区的 backend/Directory.Build.props(所有 .NET 项目、清单、镜像标签都从那里读版本)和三个 package.json +moshiro_apply_version() { + [[ -n "${MOSHIRO_VERSION:-}" ]] || _moshiro_compute_version + sed -i "s#[^<]*#$MOSHIRO_VERSION#" "$_moshiro_root/backend/Directory.Build.props" + # 前端在构建时从 web/package.json 读版本(web/build-info.mjs),其余两个工作区包的版本也一起对上 + local manifest + for manifest in web/package.json web/display/package.json web/admin/package.json; do + sed -i -E '0,/"version": "[^"]*"/s//"version": "'"$MOSHIRO_VERSION"'"/' "$_moshiro_root/$manifest" + done +} + +_moshiro_compute_version + +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + if [[ "${1:-}" == apply ]]; then moshiro_apply_version; fi + echo "$MOSHIRO_VERSION" +fi