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