From 34f87315f25ba518159ff5fea476c74ae6fec211 Mon Sep 17 00:00:00 2001 From: Roman Tkachenko Date: Mon, 11 Mar 2024 09:29:36 -0700 Subject: [PATCH] Add changelog script (#39148) --- Makefile | 7 +- build.assets/changelog.py | 31 ------ build.assets/changelog.sh | 204 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 32 deletions(-) delete mode 100755 build.assets/changelog.py create mode 100755 build.assets/changelog.sh diff --git a/Makefile b/Makefile index a06416634f140..eda4cc96d84cd 100644 --- a/Makefile +++ b/Makefile @@ -1486,7 +1486,12 @@ rustup-install-target-toolchain: # changelog generates PR changelog between the provided base tag and the tip of # the specified branch. # +# usage: make changelog +# usage: make changelog BASE_BRANCH=branch/v13 BASE_TAG=13.2.0 # usage: BASE_BRANCH=branch/v13 BASE_TAG=13.2.0 make changelog +# +# BASE_BRANCH and BASE_TAG will be automatically determined if not specified. +# See ./build.assets/changelog.sh .PHONY: changelog changelog: - @python3 ./build.assets/changelog.py BASE_BRANCH=$(BASE_BRANCH) BASE_TAG=$(BASE_TAG) + @BASE_BRANCH=$(BASE_BRANCH) BASE_TAG=$(BASE_TAG) ./build.assets/changelog.sh diff --git a/build.assets/changelog.py b/build.assets/changelog.py deleted file mode 100755 index 65e5993ce5bed..0000000000000 --- a/build.assets/changelog.py +++ /dev/null @@ -1,31 +0,0 @@ -import subprocess -import json -import os -import re - -changelog_re = re.compile(r'^changelog:(.*)', re.IGNORECASE | re.MULTILINE) - -base_tag = os.getenv("BASE_TAG") -base_branch = os.getenv("BASE_BRANCH") - -commit = subprocess.run( - f"git rev-list -n 1 v{base_tag}", - shell=True, capture_output=True, text=True).stdout - -date = subprocess.run( - f"git show -s --date=format:'%Y-%m-%dT%H:%M:%S%z' --format=%cd {commit}", - shell=True, capture_output=True, text=True).stdout - -result = subprocess.run( - f'gh pr list --search "base:{base_branch} merged:>{date} -label:no-changelog" --limit 200 --json number,title,body', - shell=True, capture_output=True, text=True).stdout - -for pr in json.loads(result): - number = pr["number"] - title = pr["title"] - - match = changelog_re.search(pr["body"]) - if match: - title = match.group(1).strip() - - print(f"* {title} [#{number}](https://github.com/gravitational/teleport/pull/{number})") diff --git a/build.assets/changelog.sh b/build.assets/changelog.sh new file mode 100755 index 0000000000000..8dfe85fc5c64b --- /dev/null +++ b/build.assets/changelog.sh @@ -0,0 +1,204 @@ +#!/bin/bash +# +# This script generates a changelog for a release. +# +# It can optionally take two input variables: BASE_BRANCH: The base release +# branch to generate the changelog for. It will be of the form "branch/v*". +# BASE_TAG: The tag/version to generate the changelog from. It will be of the +# form "vXX.Y.Z", e.g. "v15.1.1" +# +# If neither are provided, the values will be automatically determined if +# possible: +# * The current branch will be used as the base branch if it matches the +# pattern branch/v* +# * If the current branch is forked from a base branch, the base branch will be +# used. e.g. if you create release/15.1.1 from branch/v15, the branch/v15 +# will be the base branch. +# * The base tag will be determined by running "make print-version" from the +# root of the repo. +# +# Enterprise PR changelogs will be listed after the OSS changelogs. You need to +# determine if it is suitable to include them. If you do, remove the markdown +# link from each changelog when adding the changelog to CHANGELOG.md. These +# links wont work for the general public. Keep the links when adding the +# changelog to the release PR so that the enterprise PRs will link to the +# release PR. +# +# A changelog line may be marked with "NOCL:". This means there was no +# "Changelog:" line on the PR, and the PR title was used instead. You will +# likely need to reword these. +# +# If you reword changelogs, it is best to go to the source PR and change it +# there and then regenerate the changelog. +# +# Caveats: +# * If you update the "e" ref in your release PR, and you also run `make +# changelog` from the release PR branch, if you have already updated the +# version in the makefile, you will need to run `make changelog +# BASE_TAG=X.Y.Z` as this script will determine the base tag to be the +# current release version not the last released version. +# +# One preferred way of using this script is to run `make changelog` from the +# base branch and save it: `make changelog > /tmp/changelog`. If any PRs are +# merged to the base branch after you have created your release PR but before +# you have merged it, you can see any new entries with: +# +# git checkout branch/vNN +# diff -u /tmp/changelog $(make changelog) +# +# If there are changes, you can update your changelog and rebase your branch: +# +# git pull # on branch/vNN +# make changelog > /tmp/changelog +# git checkout release/XX.Y.Z +# git rebase branch/vNN +# +# git add CHANGELOG.md && git commit --amend --no-edit && git push -f +# +# Ensure you update the PR body with the new changelog, doable on the command +# line with `gh`: +# +# gh pr edit --body-file /tmp/changelog +# + +# Set by check_prereq - either jq or gojq +JQ= + +main() { + set -eu + + check_prereq || exit 1 + + local branch last_version + branch=$(get_branch) || exit 1 + last_version=$(get_last_version) || exit 1 + + since=$(git show -s --date=format:'%Y-%m-%dT%H:%M:%S%z' --format=%cd "${last_version}") || { + echo 'Cant get timestamp of last release' >&2 + return 1 + } + since_e=$(git -C e show -s --date=format:'%Y-%m-%dT%H:%M:%S%z' --format=%cd "${last_version}") || { + echo 'Cant get enterprise timestamp of last release' >&2 + return 1 + } + e_time=$(git log -n 1 --date=format:'%Y-%m-%dT%H:%M:%S%z' --format=%cd e) || { + echo 'Cant get last modified time of "e" ref' >&2 + return 1 + } + + list_prs "${branch}" "${since}" + printf '\nEnterprise:\n' + (cd e && list_prs "${branch}" "${since_e}" "${e_time}") + + return 0 +} + +check_prereq() { + if command -v jq >/dev/null 2>&1; then + JQ=jq + return 0 + fi + if command -v gojq >/dev/null 2>&1; then + JQ=gojq + return 0 + fi + + echo 'jq or gojq not installed. Install gojq easily with:' >&2 + echo 'go install github.com/itchyny/gojq/cmd/gojq@latest' >&2 + return 1 +} + +get_branch() { + # If BASE_BRANCH is set, just use that. Otherwise try to figure it out. + if [[ -n "${BASE_BRANCH-}" ]]; then + echo "${BASE_BRANCH}" + return 0 + fi + + local ref branch + ref=$(git symbolic-ref HEAD 2>/dev/null) || { + echo 'Not on a branch' >&2 + return 1 + } + branch="${ref#refs/heads/}" + if [[ "${ref}" == "${branch}" ]]; then + echo "Not on a branch: ${ref}" >&2 + return 1 + fi + + if [[ "${branch}" != branch/v* ]]; then + # If we're already on the branch cut for the release, try to + # determine the root branch name + local fbranch + fbranch=$( + git branch \ + --list 'branch/v*' \ + --contains "$(git merge-base --fork-point HEAD)" \ + --format '%(refname:short)' + ) + branch="${fbranch:-${branch}}" # Don't overwrite $branch with empty + fi + if ! [[ "${branch}" == branch/v* ]]; then + echo "Not on a release branch: ${branch}" >&2 + return 1 + fi + + echo "${branch}" + return 0 +} + +get_last_version() { + # If BASE_TAG is set, just use that, otherwise figure out the last version + if [[ -n "${BASE_TAG-}" ]]; then + echo "${BASE_TAG}" + return 0 + fi + + cd "$(git rev-parse --show-toplevel 2>/dev/null)" || { + echo 'ugh. cant cd to repo root' >&2 + return 1 + } + local last_version + last_version=$(make -s print-version) || { + echo 'Cant get last released version' >&2 + return 1 + } + + echo "v${last_version}" + return 0 +} + +list_prs() { + local branch="$1" from="$2" to="${3-}" + local merged_query + if [[ -z "${to}" ]]; then + merged_query="merged:>${from}" + else + merged_query="merged:${from}..${to}" + fi + + local gh_query="base:${branch} ${merged_query} -label:no-changelog" + + # shellcheck disable=SC2016 # We're not trying to expand in single quotes + jq_expr=' + def extract_cl: gsub("\r"; "") | split("\n") | [.[] | scan("^[Cc]hangelog: +(.*)$") | .[]]; + def promote_title: {number, url, changelog: (if .changelog == [] then ["NOCL: " + .title] else .changelog end)}; + def flatten_changes: . as $pr | .changelog[] | $pr + {changelog: .}; + def clean: sub("\\s*$"; "") | if . | endswith(".") then . else . + "." end; + def as_entry: "* \(.changelog | clean) [#\(.number)](\(.url))"; + map({number, url, title, changelog: .body | extract_cl}) | + map(promote_title) | + map(flatten_changes) | + map(as_entry) | + join("\n") + ' + + gh pr list \ + --search "${gh_query}" \ + --limit 200 \ + --json number,url,title,body | + "${JQ}" -r "${jq_expr}" +} + +# Only run main if executed as a script and not sourced. +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then main "$@"; fi