#!/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 [] [--changelog ] [--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 ! 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