Compare commits
5
Commits
f060f917a0
...
v1.1.31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32a73c5b50 | ||
|
|
45de0ef759 | ||
|
|
65b5d95937 | ||
|
|
89a8edad88 | ||
|
|
283c1d77b4 |
@@ -17,6 +17,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout tag
|
- name: Checkout tag
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Release notes are derived from the previous version tag. A shallow
|
||||||
|
# checkout would leave only the synthetic release commit available.
|
||||||
|
fetch-depth: 0
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
@@ -29,7 +33,10 @@ jobs:
|
|||||||
test "$(node -p 'require("./package.json").version')" = "${GITHUB_REF_NAME#v}"
|
test "$(node -p 'require("./package.json").version')" = "${GITHUB_REF_NAME#v}"
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm check
|
pnpm check
|
||||||
pnpm test
|
# better-sqlite3 is a native addon; a single Vitest worker avoids a
|
||||||
|
# Node cleanup race observed on the hosted runner while preserving
|
||||||
|
# the complete test suite.
|
||||||
|
pnpm test -- --pool=forks --poolOptions.forks.singleFork=true --maxWorkers=1 --minWorkers=1
|
||||||
pnpm test:installer
|
pnpm test:installer
|
||||||
- name: Build Linux release
|
- name: Build Linux release
|
||||||
run: pnpm release:build "${GITHUB_REF_NAME#v}" ./release
|
run: pnpm release:build "${GITHUB_REF_NAME#v}" ./release
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ TallyNote 的发布包必须在目标 Linux 架构上构建。`better-sqlite3`
|
|||||||
2. 由 `scripts/publish-gitea-release.sh` 计算所有归档的 `SHA256SUMS`。
|
2. 由 `scripts/publish-gitea-release.sh` 计算所有归档的 `SHA256SUMS`。
|
||||||
3. 如果提供 Ed25519 私钥则生成 `SHA256SUMS.sig`,通过 Gitea Releases API 创建/复用对应 Release,并幂等上传归档、清单和可选签名。
|
3. 如果提供 Ed25519 私钥则生成 `SHA256SUMS.sig`,通过 Gitea Releases API 创建/复用对应 Release,并幂等上传归档、清单和可选签名。
|
||||||
|
|
||||||
|
发布脚本会根据当前 tag 与上一个版本 tag 之间的真实 Git 提交自动生成 Release 正文,按“新增功能、问题修复、优化与重构、文档与测试”分类,并以 Markdown 写入 Gitea。Gitea 页面会渲染这些标题和列表;更新中心读取同一份正文后再进行安全的 Markdown 子集渲染,不会显示 Markdown 源代码。旧版本曾使用单行占位正文 `TallyNote <版本>`,新版本发布时不会再使用该占位内容。
|
||||||
|
|
||||||
在仓库的 Actions secrets 配置:
|
在仓库的 Actions secrets 配置:
|
||||||
|
|
||||||
- `GITEA_TOKEN`:仅授予当前仓库 Release 写权限的 token。
|
- `GITEA_TOKEN`:仅授予当前仓库 Release 写权限的 token。
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tallynote",
|
"name": "tallynote",
|
||||||
"version": "1.1.26",
|
"version": "1.1.31",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@9.0.6",
|
"packageManager": "pnpm@9.0.6",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ DRY_RUN=0
|
|||||||
AUTH_CONFIG=''
|
AUTH_CONFIG=''
|
||||||
SUMS_TMP=''
|
SUMS_TMP=''
|
||||||
SIG_TMP=''
|
SIG_TMP=''
|
||||||
|
RELEASE_NOTES_TMP=''
|
||||||
SIGNATURE_GENERATED=0
|
SIGNATURE_GENERATED=0
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -43,6 +44,81 @@ EOF
|
|||||||
die() { printf 'release publisher: %s\n' "$*" >&2; exit 1; }
|
die() { printf 'release publisher: %s\n' "$*" >&2; exit 1; }
|
||||||
log() { printf 'release publisher: %s\n' "$*"; }
|
log() { printf 'release publisher: %s\n' "$*"; }
|
||||||
|
|
||||||
|
generate_release_notes() {
|
||||||
|
local current=${TAG#v} previous='' subject kind line count=0
|
||||||
|
local -a commits
|
||||||
|
commits=()
|
||||||
|
|
||||||
|
# A workflow checks out the tag with history. Prefer an explicitly supplied
|
||||||
|
# notes file for mirrors, then derive notes from the immutable tag range.
|
||||||
|
if [[ -n "${TALLYNOTE_RELEASE_NOTES_FILE:-}" && -f "$TALLYNOTE_RELEASE_NOTES_FILE" ]]; then
|
||||||
|
# Read at most the API's bounded notes size without a pipe that can turn a
|
||||||
|
# deliberately truncated input into a SIGPIPE failure under pipefail.
|
||||||
|
LC_ALL=C awk 'BEGIN { remaining = 65536 } { if (remaining <= 0) exit; line=$0; gsub(/[[:cntrl:]]/, "", line); bytes=length(line)+1; if (bytes > remaining) { print substr(line, 1, remaining); exit } print line; remaining-=bytes }' "$TALLYNOTE_RELEASE_NOTES_FILE"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -n "$line" ]] || continue
|
||||||
|
[[ "$line" == "v${current}" ]] && continue
|
||||||
|
previous="$line"
|
||||||
|
break
|
||||||
|
done < <(git tag --sort=-version:refname --list 'v*')
|
||||||
|
if [[ -n "$previous" && "$previous" != "v${current}" ]]; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -n "$line" ]] && commits+=("$line")
|
||||||
|
done < <(git log --format='%s' "${previous}..${TAG}")
|
||||||
|
else
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ -n "$line" ]] && commits+=("$line")
|
||||||
|
done < <(git log -n 30 --format='%s' "$TAG")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '# TallyNote %s\n\n' "$current"
|
||||||
|
if [[ -n "$previous" ]]; then
|
||||||
|
printf '> 从 `%s` 到 `%s` 的变更\n\n' "$previous" "v${current}"
|
||||||
|
else
|
||||||
|
printf '> 本版本变更\n\n'
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a features fixes improvements docs other
|
||||||
|
features=(); fixes=(); improvements=(); docs=(); other=()
|
||||||
|
for subject in "${commits[@]-}"; do
|
||||||
|
# Do not expose merge noise or the synthetic release commit in user notes.
|
||||||
|
[[ "$subject" != Merge\ * && "$subject" != release:* ]] || continue
|
||||||
|
kind=${subject%%:*}
|
||||||
|
if [[ "$subject" == *:* ]]; then subject=${subject#*: }; fi
|
||||||
|
subject=${subject# }
|
||||||
|
[[ -n "$subject" ]] || continue
|
||||||
|
case "$kind" in
|
||||||
|
feat|feature) features+=("$subject") ;;
|
||||||
|
fix|bugfix) fixes+=("$subject") ;;
|
||||||
|
refactor|perf|style|improvement) improvements+=("$subject") ;;
|
||||||
|
docs|doc|test|tests) docs+=("$subject") ;;
|
||||||
|
*) other+=("$subject") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
print_group() {
|
||||||
|
local title=$1; shift
|
||||||
|
local item
|
||||||
|
(($# > 0)) || return 0
|
||||||
|
printf '## %s\n\n' "$title"
|
||||||
|
for item in "$@"; do printf -- '- %s\n' "$item"; done
|
||||||
|
printf '\n'
|
||||||
|
}
|
||||||
|
((${#features[@]})) && print_group '新增功能' "${features[@]}"
|
||||||
|
((${#fixes[@]})) && print_group '问题修复' "${fixes[@]}"
|
||||||
|
((${#improvements[@]})) && print_group '优化与重构' "${improvements[@]}"
|
||||||
|
((${#docs[@]})) && print_group '文档与测试' "${docs[@]}"
|
||||||
|
((${#other[@]})) && print_group '其他变更' "${other[@]}"
|
||||||
|
if (( ${#features[@]} + ${#fixes[@]} + ${#improvements[@]} + ${#docs[@]} + ${#other[@]} == 0 )); then
|
||||||
|
printf '本版本包含内部维护更新。\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
validate_semver() {
|
validate_semver() {
|
||||||
local value=$1 prerelease part
|
local value=$1 prerelease part
|
||||||
[[ "$value" =~ ^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]] || return 1
|
[[ "$value" =~ ^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]] || return 1
|
||||||
@@ -169,6 +245,7 @@ cleanup() {
|
|||||||
if [[ -n "$AUTH_CONFIG" ]]; then rm -f -- "$AUTH_CONFIG"; fi
|
if [[ -n "$AUTH_CONFIG" ]]; then rm -f -- "$AUTH_CONFIG"; fi
|
||||||
if [[ -n "$SUMS_TMP" ]]; then rm -f -- "$SUMS_TMP"; fi
|
if [[ -n "$SUMS_TMP" ]]; then rm -f -- "$SUMS_TMP"; fi
|
||||||
if [[ -n "$SIG_TMP" ]]; then rm -f -- "$SIG_TMP"; fi
|
if [[ -n "$SIG_TMP" ]]; then rm -f -- "$SIG_TMP"; fi
|
||||||
|
if [[ -n "$RELEASE_NOTES_TMP" ]]; then rm -f -- "$RELEASE_NOTES_TMP"; fi
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
if [[ -n "$SIGNING_KEY_FILE" ]]; then
|
if [[ -n "$SIGNING_KEY_FILE" ]]; then
|
||||||
@@ -204,6 +281,13 @@ command -v jq >/dev/null 2>&1 || die 'jq is required for Gitea API publishing'
|
|||||||
write_auth_config
|
write_auth_config
|
||||||
unset TOKEN
|
unset TOKEN
|
||||||
|
|
||||||
|
# Keep the release body deterministic and human-readable. Gitea renders this
|
||||||
|
# Markdown in the Release page; the update API later exposes the same body as
|
||||||
|
# text for the safe client-side Markdown renderer.
|
||||||
|
RELEASE_NOTES_TMP=$(mktemp)
|
||||||
|
generate_release_notes > "$RELEASE_NOTES_TMP"
|
||||||
|
release_notes=$(<"$RELEASE_NOTES_TMP")
|
||||||
|
|
||||||
api_curl() {
|
api_curl() {
|
||||||
"$CURL_BIN" --proto '=https' --tlsv1.2 --fail --silent --show-error --connect-timeout 15 --max-time 120 \
|
"$CURL_BIN" --proto '=https' --tlsv1.2 --fail --silent --show-error --connect-timeout 15 --max-time 120 \
|
||||||
--config "$AUTH_CONFIG" "$@"
|
--config "$AUTH_CONFIG" "$@"
|
||||||
@@ -221,8 +305,17 @@ release_json=$(mktemp)
|
|||||||
status=$(api_curl_status --max-time 30 -o "$release_json" -w '%{http_code}' "$API_ROOT/repos/$repo_path/releases/tags/$TAG") || die '无法读取 Gitea Release'
|
status=$(api_curl_status --max-time 30 -o "$release_json" -w '%{http_code}' "$API_ROOT/repos/$repo_path/releases/tags/$TAG") || die '无法读取 Gitea Release'
|
||||||
if [[ "$status" == 200 ]]; then
|
if [[ "$status" == 200 ]]; then
|
||||||
release_id=$(jq -r '.id // empty' "$release_json")
|
release_id=$(jq -r '.id // empty' "$release_json")
|
||||||
|
existing_body=$(jq -r '.body // ""' "$release_json")
|
||||||
|
# Older releases used a one-line placeholder. Upgrade that placeholder when
|
||||||
|
# a tag is republished, while leaving deliberately authored release notes
|
||||||
|
# untouched.
|
||||||
|
if [[ "$existing_body" == "TallyNote $TAG" || -z "$existing_body" ]]; then
|
||||||
|
patch_body=$(jq -cn --arg body "$release_notes" '{body:$body}')
|
||||||
|
patch_status=$(api_curl_status -X PATCH -H 'Content-Type: application/json' -d "$patch_body" -o /dev/null -w '%{http_code}' "$API_ROOT/repos/$repo_path/releases/$release_id") || die '无法更新 Gitea Release 日志'
|
||||||
|
[[ "$patch_status" == 2* ]] || die "无法更新 Gitea Release 日志(HTTP $patch_status)"
|
||||||
|
fi
|
||||||
elif [[ "$status" == 404 ]]; then
|
elif [[ "$status" == 404 ]]; then
|
||||||
body=$(jq -cn --arg tag "$TAG" --arg name "$TAG" --arg body "TallyNote $TAG" '{tag_name:$tag,name:$name,body:$body,draft:false,prerelease:false}')
|
body=$(jq -cn --arg tag "$TAG" --arg name "$TAG" --arg body "$release_notes" '{tag_name:$tag,name:$name,body:$body,draft:false,prerelease:false}')
|
||||||
create_status=$(api_curl_status -H 'Content-Type: application/json' -d "$body" -o "$release_json" -w '%{http_code}' "$API_ROOT/repos/$repo_path/releases") || die '无法创建 Gitea Release'
|
create_status=$(api_curl_status -H 'Content-Type: application/json' -d "$body" -o "$release_json" -w '%{http_code}' "$API_ROOT/repos/$repo_path/releases") || die '无法创建 Gitea Release'
|
||||||
if [[ "$create_status" == 2* ]]; then
|
if [[ "$create_status" == 2* ]]; then
|
||||||
release_id=$(jq -r '.id // empty' "$release_json")
|
release_id=$(jq -r '.id // empty' "$release_json")
|
||||||
|
|||||||
Reference in New Issue
Block a user