release: use isolated changelog fragments
This commit is contained in:
@@ -119,6 +119,17 @@ content-only)
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
if ! awk -F ' ' '
|
||||
$1 == "M" && $2 == "CHANGELOG.md" && NF == 2 { next }
|
||||
$1 == "D" && $2 ~ /^\.changes\/[a-z0-9][a-z0-9._-]*\.md$/ && NF == 2 { next }
|
||||
$1 == "A" && $2 ~ /^\.changes\/released\/[0-9]+\.[0-9]+\.[0-9]+(-beta\.[1-9][0-9]*)?\/[a-z0-9][a-z0-9._-]*\.md$/ && NF == 2 { next }
|
||||
$1 ~ /^R[0-9]+$/ && $2 ~ /^\.changes\/[a-z0-9][a-z0-9._-]*\.md$/ && $3 ~ /^\.changes\/released\/[0-9]+\.[0-9]+\.[0-9]+(-beta\.[1-9][0-9]*)?\/[a-z0-9][a-z0-9._-]*\.md$/ && NF == 3 { next }
|
||||
{ invalid = 1 }
|
||||
END { exit invalid }
|
||||
' "$TMP_ROOT/name-status"; then
|
||||
printf '%s\n' 'error: CHANGELOG.md may accompany only release-fragment archival; ordinary PRs must add .changes/<unique-name>.md without editing CHANGELOG.md' >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
|
||||
usage() { printf '%s\n' 'usage: check-release-fragments.sh BASE HEAD' >&2; }
|
||||
|
||||
[ "$#" -eq 2 ] || { usage; exit 2; }
|
||||
base="$(git -C "$ROOT" rev-parse --verify --quiet "$1^{commit}")" || { usage; exit 2; }
|
||||
head="$(git -C "$ROOT" rev-parse --verify --quiet "$2^{commit}")" || { usage; exit 2; }
|
||||
merge_base="$(git -C "$ROOT" merge-base "$base" "$head")"
|
||||
tmp_root="$(mktemp -d "${TMPDIR:-/tmp}/dws-release-fragment-policy.XXXXXX")"
|
||||
cleanup() { rm -rf "$tmp_root"; }
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
git -C "$ROOT" diff --no-ext-diff --find-renames --name-status "$merge_base" "$head" >"$tmp_root/status"
|
||||
|
||||
archive_changed=false
|
||||
if awk -F '\t' '{ for (field = 2; field <= NF; field++) if ($field ~ /^\.changes\/released\//) found = 1 } END { exit !found }' "$tmp_root/status"; then
|
||||
archive_changed=true
|
||||
fi
|
||||
|
||||
if [ "$archive_changed" = true ]; then
|
||||
release_version="$(git -C "$ROOT" diff --no-ext-diff --unified=0 "$merge_base" "$head" -- CHANGELOG.md | sed -n 's/^+## \[\([0-9][0-9.]*\(-beta\.[1-9][0-9]*\)\{0,1\}\)\] - .*/\1/p')"
|
||||
[ "$(printf '%s\n' "$release_version" | sed '/^$/d' | wc -l | tr -d '[:space:]')" -eq 1 ] || {
|
||||
printf '%s\n' 'error: release-fragment archival requires exactly one newly added versioned CHANGELOG section' >&2
|
||||
exit 1
|
||||
}
|
||||
if ! awk -F '\t' -v version="$release_version" '
|
||||
$1 == "M" && $2 == "CHANGELOG.md" && NF == 2 { changelog = 1; next }
|
||||
$1 ~ /^R100$/ && $2 ~ /^\.changes\/[a-z0-9][a-z0-9._-]*\.md$/ && $3 ~ ("^\\.changes/released/" version "/[a-z0-9][a-z0-9._-]*\\.md$") && NF == 3 {
|
||||
source = $2; target = $3; sub(/^.*\//, "", source); sub(/^.*\//, "", target)
|
||||
if (source != target) invalid = 1
|
||||
moved++; next
|
||||
}
|
||||
{ invalid = 1 }
|
||||
END { exit !(changelog && moved > 0 && !invalid) }
|
||||
' "$tmp_root/status"; then
|
||||
printf '%s\n' 'error: release fragments must be unchanged R100 moves from .changes/<name>.md to .changes/released/<new-version>/<name>.md in the matching release-seal PR' >&2
|
||||
exit 1
|
||||
fi
|
||||
source_changes="$tmp_root/source-changes"
|
||||
mkdir -p "$source_changes"
|
||||
git -C "$ROOT" ls-tree -r --name-only "$merge_base" -- .changes |
|
||||
while IFS= read -r path; do
|
||||
case "$path" in
|
||||
.changes/*.md)
|
||||
name="${path#.changes/}"
|
||||
mkdir -p "$(dirname "$source_changes/$name")"
|
||||
git -C "$ROOT" show "$merge_base:$path" >"$source_changes/$name"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
"$ROOT/scripts/release/render-release-fragments.sh" "$source_changes" >"$tmp_root/expected-notes"
|
||||
git -C "$ROOT" show "$head:CHANGELOG.md" |
|
||||
awk -v heading="## [$release_version] - " '
|
||||
index($0, heading) == 1 { found = 1; next }
|
||||
found && /^## / { exit }
|
||||
found { print }
|
||||
' >"$tmp_root/actual-notes"
|
||||
normalize_notes() {
|
||||
awk '
|
||||
/^[[:space:]]*$/ && !started { next }
|
||||
{ started = 1; lines[++count] = $0 }
|
||||
END {
|
||||
while (count > 0 && lines[count] ~ /^[[:space:]]*$/) count--
|
||||
for (line_no = 1; line_no <= count; line_no++) print lines[line_no]
|
||||
}
|
||||
' "$1"
|
||||
}
|
||||
normalize_notes "$tmp_root/expected-notes" >"$tmp_root/expected-notes.normalized"
|
||||
normalize_notes "$tmp_root/actual-notes" >"$tmp_root/actual-notes.normalized"
|
||||
if ! cmp -s "$tmp_root/expected-notes.normalized" "$tmp_root/actual-notes.normalized"; then
|
||||
printf '%s\n' 'error: release-seal CHANGELOG section does not exactly match the rendered active release fragments' >&2
|
||||
diff -u "$tmp_root/expected-notes.normalized" "$tmp_root/actual-notes.normalized" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if awk -F '\t' '{ for (field = 2; field <= NF; field++) if ($field ~ /^\.changes\/released\//) invalid = 1 } END { exit !invalid }' "$tmp_root/status"; then
|
||||
printf '%s\n' 'error: archived release fragments are immutable outside their release-seal PR' >&2
|
||||
exit 1
|
||||
fi
|
||||
if awk -F '\t' '
|
||||
$1 == "D" && $2 ~ /^\.changes\/[a-z0-9][a-z0-9._-]*\.md$/ { invalid = 1 }
|
||||
$1 ~ /^R[0-9]+$/ && $2 ~ /^\.changes\/[a-z0-9][a-z0-9._-]*\.md$/ { invalid = 1 }
|
||||
END { exit !invalid }
|
||||
' "$tmp_root/status"; then
|
||||
printf '%s\n' 'error: active release fragments may be deleted or renamed only by the matching release-seal archival move' >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if git -C "$ROOT" diff --no-ext-diff --name-only "$merge_base" "$head" -- .changes | grep -Eq '^\.changes/[a-z0-9][a-z0-9._-]*\.md$'; then
|
||||
"$ROOT/scripts/release/render-release-fragments.sh" "$ROOT/.changes" >/dev/null
|
||||
fi
|
||||
@@ -10,6 +10,7 @@ VERSION="${2:-}"
|
||||
FROM_BETA=""
|
||||
FROM_REF=""
|
||||
CHANGELOG="$ROOT/CHANGELOG.md"
|
||||
CHANGES_DIR="$ROOT/.changes"
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
@@ -19,6 +20,7 @@ Options:
|
||||
--from-beta <tag> Required for stable release notes
|
||||
--from-ref <ref> Commit-list baseline for prerelease notes
|
||||
--changelog <path> Override CHANGELOG.md path
|
||||
--changes-dir <path> Override release fragment directory
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -29,6 +31,7 @@ while [ "$#" -gt 0 ]; do
|
||||
--from-beta) [ "$#" -ge 2 ] || { usage; exit 2; }; FROM_BETA="$2"; shift 2 ;;
|
||||
--from-ref) [ "$#" -ge 2 ] || { usage; exit 2; }; FROM_REF="$2"; shift 2 ;;
|
||||
--changelog) [ "$#" -ge 2 ] || { usage; exit 2; }; CHANGELOG="$2"; shift 2 ;;
|
||||
--changes-dir) [ "$#" -ge 2 ] || { usage; exit 2; }; CHANGES_DIR="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) printf 'unknown argument: %s\n' "$1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
@@ -60,21 +63,19 @@ fi
|
||||
release_date="${DWS_RELEASE_DATE:-$(TZ=Asia/Shanghai date +%F)}"
|
||||
section="$(mktemp "${TMPDIR:-/tmp}/dws-changelog-section.XXXXXX")"
|
||||
output="$(mktemp "${TMPDIR:-/tmp}/dws-changelog-output.XXXXXX")"
|
||||
cleanup() { rm -f "$section" "$output"; }
|
||||
fragments="$(mktemp "${TMPDIR:-/tmp}/dws-changelog-fragments.XXXXXX")"
|
||||
cleanup() { rm -f "$section" "$output" "$fragments"; }
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
{
|
||||
printf '## [%s] - %s\n\n' "$semver" "$release_date"
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
printf 'This release promotes the sealed `%s` contents to stable.\n\n' "$FROM_BETA"
|
||||
else
|
||||
printf '<!-- Summarize what this beta validates. Remove every TODO before publishing. -->\n\n'
|
||||
fi
|
||||
printf '### Changed\n\n'
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
printf '### Changed\n\n'
|
||||
printf -- '- TODO: summarize the complete user-visible release promoted from `%s`.\n' "$FROM_BETA"
|
||||
else
|
||||
printf -- '- TODO: summarize this beta candidate and its validation scope.\n'
|
||||
"$SCRIPT_DIR/render-release-fragments.sh" "$CHANGES_DIR" >"$fragments"
|
||||
cat "$fragments"
|
||||
fi
|
||||
} > "$section"
|
||||
|
||||
@@ -105,7 +106,17 @@ fi
|
||||
[ "$inserted" -eq 1 ] || { printf 'CHANGELOG is missing ## [Unreleased]\n' >&2; exit 1; }
|
||||
cp "$output" "$CHANGELOG"
|
||||
|
||||
printf 'Prepared CHANGELOG template for %s. Replace TODO, review, commit, and merge it before release.\n' "$VERSION"
|
||||
if [ "$CHANNEL" = "prerelease" ]; then
|
||||
archive_dir="$CHANGES_DIR/released/$semver"
|
||||
mkdir -p "$archive_dir"
|
||||
find "$CHANGES_DIR" -mindepth 1 -maxdepth 1 -type f -name '*.md' ! -name 'README.md' -exec mv {} "$archive_dir"/ \;
|
||||
fi
|
||||
|
||||
if [ "$CHANNEL" = "stable" ]; then
|
||||
printf 'Prepared CHANGELOG template for %s. Replace TODO, review, commit, and merge it before release.\n' "$VERSION"
|
||||
else
|
||||
printf 'Prepared CHANGELOG and archived release fragments for %s. Review, commit, and merge the release-seal PR before release.\n' "$VERSION"
|
||||
fi
|
||||
if [ -n "$FROM_REF" ] && git rev-parse --verify --quiet "$FROM_REF^{commit}" >/dev/null; then
|
||||
printf '\nCommits since %s:\n' "$FROM_REF"
|
||||
git log --oneline "$FROM_REF..HEAD"
|
||||
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
CHANGES_DIR="${1:-.changes}"
|
||||
|
||||
usage() {
|
||||
printf '%s\n' 'usage: render-release-fragments.sh [changes-dir]' >&2
|
||||
}
|
||||
|
||||
[ "$#" -le 1 ] || { usage; exit 2; }
|
||||
[ -d "$CHANGES_DIR" ] || { printf 'release fragments directory not found: %s\n' "$CHANGES_DIR" >&2; exit 1; }
|
||||
|
||||
tmp_root="$(mktemp -d "${TMPDIR:-/tmp}/dws-release-fragments.XXXXXX")"
|
||||
cleanup() { rm -rf "$tmp_root"; }
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
find "$CHANGES_DIR" -mindepth 1 -maxdepth 1 -type f -name '*.md' ! -name 'README.md' -print |
|
||||
LC_ALL=C sort >"$tmp_root/files"
|
||||
|
||||
[ -s "$tmp_root/files" ] || {
|
||||
printf '%s\n' 'no release fragments found; add .changes/<unique-name>.md before preparing a prerelease changelog' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
validate_fragment() {
|
||||
fragment="$1"
|
||||
base="$(basename "$fragment")"
|
||||
case "$base" in
|
||||
[a-z0-9]*.md) ;;
|
||||
*) printf 'invalid release fragment filename: %s\n' "$fragment" >&2; return 1 ;;
|
||||
esac
|
||||
|
||||
[ "$(sed -n '1p' "$fragment")" = '---' ] &&
|
||||
[ "$(sed -n '3p' "$fragment")" = '---' ] || {
|
||||
printf 'invalid release fragment header: %s\n' "$fragment" >&2
|
||||
return 1
|
||||
}
|
||||
case "$(sed -n '2p' "$fragment")" in
|
||||
'category: Added'|'category: Changed'|'category: Deprecated'|'category: Removed'|'category: Fixed'|'category: Security') ;;
|
||||
*) printf 'invalid release fragment category: %s\n' "$fragment" >&2; return 1 ;;
|
||||
esac
|
||||
|
||||
body="$(sed -n '4,$p' "$fragment")"
|
||||
printf '%s\n' "$body" | grep -Eq '^- [^[:space:]].*' || {
|
||||
printf 'release fragment must contain a non-empty Markdown list item: %s\n' "$fragment" >&2
|
||||
return 1
|
||||
}
|
||||
if printf '%s\n' "$body" | grep -Eqi '(^|[^[:alnum:]_])(TODO|TBD)([^[:alnum:]_]|$)'; then
|
||||
printf 'release fragment must not contain TODO/TBD: %s\n' "$fragment" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
while IFS= read -r fragment; do
|
||||
validate_fragment "$fragment"
|
||||
done <"$tmp_root/files"
|
||||
|
||||
for category in Added Changed Deprecated Removed Fixed Security; do
|
||||
category_files="$tmp_root/$category"
|
||||
: >"$category_files"
|
||||
while IFS= read -r fragment; do
|
||||
if [ "$(sed -n '2p' "$fragment")" = "category: $category" ]; then
|
||||
printf '%s\n' "$fragment" >>"$category_files"
|
||||
fi
|
||||
done <"$tmp_root/files"
|
||||
[ -s "$category_files" ] || continue
|
||||
|
||||
printf '### %s\n\n' "$category"
|
||||
while IFS= read -r fragment; do
|
||||
awk 'NR >= 4 { if ($0 ~ /[^[:space:]]/) started = 1; if (started) print }' "$fragment"
|
||||
printf '\n'
|
||||
done <"$category_files"
|
||||
done
|
||||
Reference in New Issue
Block a user