scripts/changelog-extract.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/bash
# =============================================================================
# changelog-extract.sh — 从根 CHANGELOG.md 提取版本号与发版说明
#
# 用法:
# changelog-extract.sh version [CHANGELOG]
# changelog-extract.sh body <version> [--section 名称]... [CHANGELOG]
# changelog-extract.sh ota-text <version> [CHANGELOG]
# changelog-extract.sh apk-ota-text <version> [CHANGELOG]
# changelog-extract.sh sync-desktop [CHANGELOG]
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DEFAULT_CHANGELOG="${MAO_CHANGELOG:-$PROJECT_ROOT/CHANGELOG.md}"
# OTA 用户可见小节(前缀匹配 ### 标题)
OTA_SECTIONS=(
"前端(桌面 / Web / 安卓)"
"安卓原生"
)
usage() {
sed -n '3,12p' "$0" | sed 's/^# //'
exit 1
}
resolve_changelog() {
local last="${*: -1}"
if [[ $# -gt 0 && -f "$last" && "$last" == *.md ]]; then
echo "$last"
else
echo "$DEFAULT_CHANGELOG"
fi
}
cmd_version() {
local file
file="$(resolve_changelog "$@")"
if [[ ! -f "$file" ]]; then
echo "错误:CHANGELOG 不存在: $file" >&2
exit 1
fi
grep -m1 '^## ' "$file" | sed 's/^## \([^ ]*\).*/\1/'
}
# body: 每行一条(已去掉 "- " 前缀)
cmd_body() {
local version="$1"
shift
local sections=()
local changelog_arg=""
while [[ $# -gt 0 ]]; do
case "$1" in
--section) sections+=("$2"); shift 2 ;;
-h|--help) usage ;;
*)
if [[ -f "$1" && "$1" == *.md ]]; then
changelog_arg="$1"
shift
else
echo "未知参数: $1" >&2
exit 1
fi
;;
esac
done
local CHANGELOG_FILE="${changelog_arg:-$DEFAULT_CHANGELOG}"
local section_csv=""
if ((${#sections[@]} > 0)); then
section_csv=$(IFS='|'; echo "${sections[*]}")
fi
awk -v ver="$version" -v want="$section_csv" '
function section_wanted(h, i, n, p) {
if (want == "") return 1
n = split(want, parts, "|")
for (i = 1; i <= n; i++) {
p = parts[i]
if (h == p || index(h, p) == 1) return 1
}
return 0
}
/^## / {
if (in_ver) exit
if ($0 ~ "^## " ver " ") {
in_ver = 1
has_sub = 0
in_sec = 0
}
next
}
in_ver && /^## / { exit }
in_ver && /^### / {
has_sub = 1
heading = $0
sub(/^### /, "", heading)
in_sec = section_wanted(heading)
next
}
in_ver && /^- / {
line = substr($0, 3)
if (!has_sub) print line
else if (in_sec) print line
}
' "$CHANGELOG_FILE"
}
lines_to_pipe() {
echo "$1" | sed '/^[[:space:]]*$/d' | tr '\n' '|' | sed 's/|$//'
}
# ota-text: pipe 分隔;有 ### 分节时仅取前端 + 安卓原生,否则取全部条目
cmd_ota_text() {
local version="$1"
shift
local changelog_arg=""
if [[ $# -ge 1 && -f "$1" && "$1" == *.md ]]; then
changelog_arg="$1"
shift
fi
local CHANGELOG_FILE="${changelog_arg:-$DEFAULT_CHANGELOG}"
local lines
lines=$(cmd_body "$version" --section "${OTA_SECTIONS[0]}" --section "${OTA_SECTIONS[1]}" "$CHANGELOG_FILE" || true)
if [[ -z "$lines" ]]; then
lines=$(cmd_body "$version" "$CHANGELOG_FILE")
fi
if [[ -z "$lines" ]]; then
echo "错误:版本 $version 在 $CHANGELOG_FILE 中无 OTA 条目" >&2
exit 1
fi
echo "$lines" | sed '/^[[:space:]]*$/d' | tr '\n' '|' | sed 's/|$//'
}
# apk-ota-text: 安卓壳 APK OTA 文案(优先 ### 安卓原生,否则回退 ota-text)
cmd_apk_ota_text() {
local version="$1"
shift
local changelog_arg=""
if [[ $# -ge 1 && -f "$1" && "$1" == *.md ]]; then
changelog_arg="$1"
shift
fi
local CHANGELOG_FILE="${changelog_arg:-$DEFAULT_CHANGELOG}"
local lines
lines=$(cmd_body "$version" --section "安卓原生" "$CHANGELOG_FILE" || true)
if [[ -z "$lines" ]]; then
cmd_ota_text "$version" "$CHANGELOG_FILE"
return
fi
lines_to_pipe "$lines"
}
cmd_sync_desktop() {
local file
file="$(resolve_changelog "$@")"
local version
version="$(cmd_version "$file")"
local pkg="$PROJECT_ROOT/desktop/package.json"
if [[ ! -f "$pkg" ]]; then
echo "错误:desktop/package.json 不存在" >&2
exit 1
fi
node -e "
const fs = require('fs');
const p = process.argv[1];
const ver = process.argv[2];
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
if (pkg.version !== ver) {
pkg.version = ver;
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
console.log('desktop/package.json version -> ' + ver);
}
const lockPath = require('path').join(require('path').dirname(p), 'package-lock.json');
if (!fs.existsSync(lockPath)) process.exit(0);
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
let changed = false;
if (lock.version !== ver) { lock.version = ver; changed = true; }
if (lock.packages && lock.packages[''] && lock.packages[''].version !== ver) {
lock.packages[''].version = ver;
changed = true;
}
if (changed) {
fs.writeFileSync(lockPath, JSON.stringify(lock, null, 2) + '\n');
console.log('desktop/package-lock.json version -> ' + ver);
}
" "$pkg" "$version"
}
[[ $# -ge 1 ]] || usage
case "$1" in
version) shift; cmd_version "$@" ;;
body)
shift
[[ $# -ge 1 ]] || usage
cmd_body "$@"
;;
ota-text)
shift
[[ $# -ge 1 ]] || usage
cmd_ota_text "$@"
;;
apk-ota-text)
shift
[[ $# -ge 1 ]] || usage
cmd_apk_ota_text "$@"
;;
sync-desktop) shift; cmd_sync_desktop "$@" ;;
-h|--help) usage ;;
*) echo "未知命令: $1" >&2; usage ;;
esac