Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build-desktop-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
required: false
default: https://github.com/AstrBotDevs/AstrBot.git
source_git_ref:
description: Optional source ref override for `tag-poll` or required explicit source ref for `custom` (branch/tag/commit SHA).
description: Optional stable tag (vMAJOR.MINOR.PATCH) for `tag-poll`, or required source ref for `custom` (branch/tag/commit SHA).
required: false
default: ""
publish_release:
Expand All @@ -19,7 +19,7 @@ on:
build_mode:
description: >-
Build mode (`tag-poll` | `nightly` | `custom`): `nightly` (default) always builds latest upstream commit,
`tag-poll` builds latest upstream tag (or `source_git_ref` override), `custom` builds the explicit `source_git_ref`
`tag-poll` builds latest stable upstream tag (or stable tag override), `custom` builds the explicit `source_git_ref`
required: false
type: choice
default: nightly
Expand Down
2 changes: 2 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ beforeBuildCommand = pnpm run prepare:resources

## 7. CI 与发布说明

- stable(`tag-poll`)只选择 `vMAJOR.MINOR.PATCH` 格式的正式版本标签,忽略 beta、alpha、rc、dev、nightly 等预发布标签及其他非正式标签;没有正式标签时任务明确失败,不回退到预发布版本。
- 手动 `tag-poll` 的 `source_git_ref` 也只接受正式版本标签。需要打包预发布标签、分支或 commit 时使用 `custom`,不会更新 stable 通道。
- 定时构建(`schedule`)检测到上游新 tag 时,会先自动同步版本文件并提交,再继续构建。
- 手动触发(`workflow_dispatch`)默认只构建,不自动回写版本文件。
- Desktop 使用独立 updater manifest,不复用 AstrBot Core 的 `api.soulter.top/releases` 更新索引。
Expand Down
10 changes: 9 additions & 1 deletion scripts/ci/resolve-build-context.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set -euo pipefail
DEFAULT_NIGHTLY_UTC_HOUR='3'
DEFAULT_LS_REMOTE_RETRY_ATTEMPTS='3'
DEFAULT_LS_REMOTE_RETRY_SLEEP_SECONDS='2'
STABLE_TAG_PATTERN='^v(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)$'

temp_dirs=()
cleanup_temp_dirs() {
Expand Down Expand Up @@ -139,9 +140,12 @@ resolve_latest_upstream_tag() {
"${sleep_seconds}"
)" || return 1

# Version sort is safe for stable numeric tags only: it ranks beta/rc
# suffixes above the corresponding final release, unlike SemVer.
latest_tag="$(printf '%s\n' "${tag_remote_output}" \
| awk '{print $2}' \
| sed -e 's#refs/tags/##' -e 's#\^{}$##' \
| awk -v pattern="${STABLE_TAG_PATTERN}" '$0 ~ pattern' \
| LC_ALL=C sort -Vu \
| tail -n 1)"
[ -n "${latest_tag}" ] || return 1
Expand Down Expand Up @@ -348,6 +352,10 @@ if [ "${build_mode}" = "nightly" ]; then
echo "Nightly source resolved from ${nightly_branch}@${source_git_ref} (configured ASTRBOT_NIGHTLY_SOURCE_GIT_REF='${nightly_source_git_ref}')."
elif [ "${build_mode}" = "tag-poll" ]; then
if [ "${workflow_source_git_ref_provided}" = "true" ]; then
if ! printf '%s\n' "${source_git_ref}" | grep -Eq "${STABLE_TAG_PATTERN}"; then
echo "::error::tag-poll only accepts stable release tags in vMAJOR.MINOR.PATCH format; got '${source_git_ref}'. Use custom mode for prerelease tags, branches, or commits." >&2
exit 1
fi
if latest_upstream_tag="$(resolve_latest_upstream_tag "${source_git_url}" "${retry_attempts}" "${retry_sleep_seconds}")"; then
echo "Latest upstream tag is ${latest_upstream_tag}"
else
Expand All @@ -356,7 +364,7 @@ elif [ "${build_mode}" = "tag-poll" ]; then
echo "workflow_dispatch tag-poll mode: using explicit source ref override ${source_git_ref}"
else
if ! latest_upstream_tag="$(resolve_latest_upstream_tag "${source_git_url}" "${retry_attempts}" "${retry_sleep_seconds}")"; then
echo "Unable to resolve latest tag from ${source_git_url}" >&2
echo "Unable to resolve latest stable tag (vMAJOR.MINOR.PATCH) from ${source_git_url}" >&2
exit 1
fi
echo "Latest upstream tag is ${latest_upstream_tag}"
Expand Down
90 changes: 88 additions & 2 deletions scripts/ci/resolve-build-context.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,92 @@ const withSandbox = async (env, fn) => {

const runResolveBuildContext = async (env) => withSandbox(env, runInSandbox);

const remoteTags = (...tags) => tags
.map((tag) => `1111111111111111111111111111111111111111 refs/tags/${tag}`)
.join('|');

const nonStableRefs = [
'v4.28.0-beta.1',
'v4.29.0-alpha.1',
'v4.29.0-rc.1',
'v4.29.0-dev.1',
'v4.29.0-nightly.20260912',
'v4.29.0-preview.1',
'v4.29.0a1',
'v4.29.0.post1',
'v4.29.0+build.1',
'v04.29.0',
'v4x29x0',
'v4.29',
'nightly',
'master',
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
];

for (const event of ['schedule', 'workflow_dispatch']) {
test(`${event} tag-poll selects 4.28.0 over beta and newer prerelease tags`, async () => {
const { result, outputs } = await runResolveBuildContext(makeTagPollEnv({
GITHUB_EVENT_NAME: event,
ASTRBOT_TEST_GIT_TAGS: remoteTags('v4.9.0', 'v4.27.5', 'v4.28.0', 'v4.28.0^{}', ...nonStableRefs),
}));

assert.equal(result.status, 0, result.stderr);
assert.equal(outputs.source_git_ref, 'v4.28.0');
assert.equal(outputs.astrbot_version, '4.28.0');
assert.equal(outputs.should_build, 'true');
assert.equal(outputs.release_tag, 'v4.28.0');
assert.equal(outputs.release_prerelease, 'false');
assert.equal(outputs.release_make_latest, 'true');
});
}

test('scheduled tag-poll skips an existing stable release even with newer prerelease tags', async () => {
const { result, outputs } = await runResolveBuildContext(makeTagPollEnv({
GITHUB_EVENT_NAME: 'schedule',
ASTRBOT_TEST_GIT_TAGS: remoteTags('v4.27.5', ...nonStableRefs),
ASTRBOT_TEST_CURL_HTTP_STATUS: '200',
}));

assert.equal(result.status, 0, result.stderr);
assert.equal(outputs.source_git_ref, 'v4.27.5');
assert.equal(outputs.should_build, 'false');
assert.equal(outputs.release_tag, '');
assert.equal(outputs.release_make_latest, 'false');
});

test('tag-poll fails without a stable tag instead of publishing a prerelease', async () => {
const { result, outputs } = await runResolveBuildContext(makeTagPollEnv({
ASTRBOT_TEST_GIT_TAGS: remoteTags(...nonStableRefs),
}));

assert.notEqual(result.status, 0);
assert.match(result.stderr, /Unable to resolve latest stable tag/);
assert.deepEqual(outputs, {});
});

for (const ref of nonStableRefs) {
test(`manual tag-poll rejects non-stable override ${ref}`, async () => {
const { result, outputs } = await runResolveBuildContext(makeTagPollEnv({
WORKFLOW_SOURCE_GIT_REF: ref,
}));

assert.notEqual(result.status, 0);
assert.match(result.stderr, /tag-poll only accepts stable release tags/);
assert.deepEqual(outputs, {});
});
}

test('manual stable override is marked latest when a newer prerelease exists', async () => {
const { result, outputs } = await runResolveBuildContext(makeTagPollEnv({
WORKFLOW_SOURCE_GIT_REF: 'v4.28.0',
ASTRBOT_TEST_GIT_TAGS: remoteTags('v4.28.0', ...nonStableRefs),
}));

assert.equal(result.status, 0, result.stderr);
assert.equal(outputs.release_tag, 'v4.28.0');
assert.equal(outputs.release_make_latest, 'true');
});

test('workflow_dispatch tag-poll marks latest only when explicit source ref is the latest upstream tag', async () => {
const { result, outputs } = await runResolveBuildContext(makeTagPollEnv({
WORKFLOW_SOURCE_GIT_REF: 'v4.19.0',
Expand All @@ -163,13 +249,13 @@ test('workflow_dispatch tag-poll does not mark latest when explicit source ref i

test('workflow_dispatch tag-poll keeps explicit source ref builds running when tag lookup fails', async () => {
const { result, outputs } = await runResolveBuildContext(makeTagPollEnv({
WORKFLOW_SOURCE_GIT_REF: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
WORKFLOW_SOURCE_GIT_REF: 'v4.19.7',
ASTRBOT_TEST_GIT_TAGS_FAIL: '1',
ASTRBOT_TEST_FETCHED_VERSION: '4.19.7',
}));

assert.equal(result.status, 0, result.stderr);
assert.equal(outputs.source_git_ref, 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef');
assert.equal(outputs.source_git_ref, 'v4.19.7');
assert.equal(outputs.astrbot_version, '4.19.7');
assert.equal(outputs.release_make_latest, 'false');
});
Expand Down