ceremony-ci-probe/bin/changelog-assemble

126 lines
3.4 KiB
Text
Raw Normal View History

#!/usr/bin/env bash
# Fold the changelog.d/ fragments into one release section (#112, #114).
# Run by hand in the release PR, from a checkout of ceremony at the
# consumer's pin — deliberately not a CI step, because the assembled
# section must land in the PR diff where the panel reads it.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=lib/changelog.sh
source "$ROOT/lib/changelog.sh"
usage() {
echo "usage: changelog-assemble <version> [<date>] [--changelog <file>] [--dir <dir>] [--check]" >&2
exit 2
}
refuse() {
printf 'changelog-assemble: %s\n' "$1" >&2
exit 1
}
ver=""
stamp=""
changelog="CHANGELOG.md"
dir="changelog.d"
checkmode=0
while [ $# -gt 0 ]; do
case "$1" in
--changelog)
[ $# -ge 2 ] || usage
changelog="$2"
shift 2
;;
--dir)
[ $# -ge 2 ] || usage
dir="$2"
shift 2
;;
--check)
checkmode=1
shift
;;
-*)
usage
;;
*)
if [ -z "$ver" ]; then
ver="$1"
elif [ -z "$stamp" ]; then
stamp="$1"
else
usage
fi
shift
;;
esac
done
[ -n "$ver" ] || usage
[ -n "$stamp" ] || stamp="$(date -u +%F)"
[ -f "$changelog" ] || refuse "no such file: $changelog"
# Every entry in the directory must be a publishable fragment. A stray file
# in a machine-assembled directory is a mistake to surface, never to skip —
# except README.md, the directory's marker (#112 D1). This runs before the
# zero-fragments check so a directory holding only 'notes.txt' names the
# stray file instead of claiming emptiness.
for f in "$dir"/*; do
[ -e "$f" ] || continue
[ "${f##*/}" = "README.md" ] && continue
if ! diagnosis="$(changelog_fragment_problem "$f")"; then
refuse "$diagnosis"
fi
done
fragments="$(changelog_fragments "$dir")"
[ -n "$fragments" ] || refuse "zero fragments in '$dir' — a release publishes prose; refusing to publish an empty release"
if ! diagnosis="$(changelog_shape_problem "$changelog" "$dir")"; then
refuse "$diagnosis"
fi
if ! body="$(changelog_assemble "$dir")"; then
refuse "$body"
fi
# Whole-version match, as everywhere in this family: 0.2.0-rc1 in the
# changelog never blocks assembling 0.2.0.
if awk -v ver="$ver" '/^## / && $2 == ver { found = 1; exit } END { exit !found }' "$changelog"; then
refuse "$changelog already has a section for '$ver' — the ceremony was already run"
fi
if [ "$checkmode" = 1 ]; then
# --check prints the body, not the heading: the body is the invariant a
# caller can compare — #116 checks it against the release PR's stamped
# section, whose date the PR chose, not the day the check runs.
printf '%s\n' "$body"
exit 0
fi
heading="## $ver — $stamp"
lineno="$(grep -n -m1 '^## ' "$changelog" | cut -d: -f1)" || lineno=""
tmp="$(mktemp "$changelog.XXXXXX")"
if [ -n "$lineno" ]; then
{
head -n "$((lineno - 1))" "$changelog"
printf '%s\n\n%s\n\n' "$heading" "$body"
tail -n +"$lineno" "$changelog"
} >"$tmp"
else
# No section yet: the whole file is preamble, and the section goes after it.
{
cat "$changelog"
printf '\n%s\n\n%s\n' "$heading" "$body"
} >"$tmp"
fi
mv "$tmp" "$changelog"
count=0
while IFS= read -r f; do
rm -- "$f"
count=$((count + 1))
done <<<"$fragments"
printf "changelog-assemble: wrote '%s' to %s, consumed %d fragment(s)\n" "$heading" "$changelog" "$count" >&2