@@ -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 " )