fix: generate detailed markdown release notes
TallyNote release / linux-x64 (push) Failing after 8m37s
TallyNote release / linux-x64 (push) Failing after 8m37s
This commit is contained in:
@@ -17,6 +17,10 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout tag
|
||||
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
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
|
||||
@@ -12,6 +12,8 @@ TallyNote 的发布包必须在目标 Linux 架构上构建。`better-sqlite3`
|
||||
2. 由 `scripts/publish-gitea-release.sh` 计算所有归档的 `SHA256SUMS`。
|
||||
3. 如果提供 Ed25519 私钥则生成 `SHA256SUMS.sig`,通过 Gitea Releases API 创建/复用对应 Release,并幂等上传归档、清单和可选签名。
|
||||
|
||||
发布脚本会根据当前 tag 与上一个版本 tag 之间的真实 Git 提交自动生成 Release 正文,按“新增功能、问题修复、优化与重构、文档与测试”分类,并以 Markdown 写入 Gitea。Gitea 页面会渲染这些标题和列表;更新中心读取同一份正文后再进行安全的 Markdown 子集渲染,不会显示 Markdown 源代码。旧版本曾使用单行占位正文 `TallyNote <版本>`,新版本发布时不会再使用该占位内容。
|
||||
|
||||
在仓库的 Actions secrets 配置:
|
||||
|
||||
- `GITEA_TOKEN`:仅授予当前仓库 Release 写权限的 token。
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tallynote",
|
||||
"version": "1.1.26",
|
||||
"version": "1.1.27",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@9.0.6",
|
||||
|
||||
@@ -24,6 +24,7 @@ DRY_RUN=0
|
||||
AUTH_CONFIG=''
|
||||
SUMS_TMP=''
|
||||
SIG_TMP=''
|
||||
RELEASE_NOTES_TMP=''
|
||||
SIGNATURE_GENERATED=0
|
||||
|
||||
usage() {
|
||||
@@ -43,6 +44,81 @@ EOF
|
||||
die() { printf 'release publisher: %s\n' "$*" >&2; exit 1; }
|
||||
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'
|
||||
}
|
||||
print_group '新增功能' "${features[@]-}"
|
||||
print_group '问题修复' "${fixes[@]-}"
|
||||
print_group '优化与重构' "${improvements[@]-}"
|
||||
print_group '文档与测试' "${docs[@]-}"
|
||||
print_group '其他变更' "${other[@]-}"
|
||||
if (( ${#features[@]} + ${#fixes[@]} + ${#improvements[@]} + ${#docs[@]} + ${#other[@]} == 0 )); then
|
||||
printf '本版本包含内部维护更新。\n'
|
||||
fi
|
||||
}
|
||||
|
||||
validate_semver() {
|
||||
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
|
||||
@@ -169,6 +245,7 @@ cleanup() {
|
||||
if [[ -n "$AUTH_CONFIG" ]]; then rm -f -- "$AUTH_CONFIG"; fi
|
||||
if [[ -n "$SUMS_TMP" ]]; then rm -f -- "$SUMS_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
|
||||
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
|
||||
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() {
|
||||
"$CURL_BIN" --proto '=https' --tlsv1.2 --fail --silent --show-error --connect-timeout 15 --max-time 120 \
|
||||
--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'
|
||||
if [[ "$status" == 200 ]]; then
|
||||
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
|
||||
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'
|
||||
if [[ "$create_status" == 2* ]]; then
|
||||
release_id=$(jq -r '.id // empty' "$release_json")
|
||||
|
||||
Reference in New Issue
Block a user