From d019eea902ac64a73e80dcf55db26f9f85c3e54a Mon Sep 17 00:00:00 2001 From: Damien Martin-Guillerez Date: Fri, 24 Jul 2015 12:40:48 +0000 Subject: Bazel release notes creation This script uses the RELNOTES: tag (RELNOTES for a simple change, RELNOTES[NEW] for a new feature, RELNOTES[INC] for an incompatible change) to create the CHANGELOG.md file. -- Change-Id: If457a0a85f4a9ceddf822393d0aeb8b60c54136b Reviewed-on: https://bazel-review.googlesource.com/#/c/1583/ MOS_MIGRATED_REVID=99020942 --- BUILD | 6 ++ scripts/release/BUILD | 22 +++++ scripts/release/relnotes.sh | 161 ++++++++++++++++++++++++++++++ scripts/release/relnotes_test.sh | 204 +++++++++++++++++++++++++++++++++++++++ scripts/release/testenv.sh | 43 +++++++++ 5 files changed, 436 insertions(+) create mode 100644 BUILD create mode 100644 scripts/release/BUILD create mode 100755 scripts/release/relnotes.sh create mode 100755 scripts/release/relnotes_test.sh create mode 100755 scripts/release/testenv.sh diff --git a/BUILD b/BUILD new file mode 100644 index 0000000000..0081bc6752 --- /dev/null +++ b/BUILD @@ -0,0 +1,6 @@ +package(default_visibility = ["//scripts/release:__pkg__"]) + +filegroup( + name = "git", + srcs = glob([".git/**"]), +) diff --git a/scripts/release/BUILD b/scripts/release/BUILD new file mode 100644 index 0000000000..b2fb039097 --- /dev/null +++ b/scripts/release/BUILD @@ -0,0 +1,22 @@ +# Scripts for building Bazel releases +package(default_visibility = ["//visibility:private"]) + +sh_library( + name = "relnotes", + srcs = ["relnotes.sh"], +) + +sh_test( + name = "relnotes_test", + srcs = ["relnotes_test.sh"], + data = [ + "testenv.sh", + "//:git", + "//src/test/shell:bashunit", + ], + shard_count = 2, + tags = ["need_git"], + deps = [ + ":relnotes", + ], +) diff --git a/scripts/release/relnotes.sh b/scripts/release/relnotes.sh new file mode 100755 index 0000000000..56888c574e --- /dev/null +++ b/scripts/release/relnotes.sh @@ -0,0 +1,161 @@ +#!/bin/bash -eu + +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Generate the release notes from the git history. + +# It uses the RELNOTES tag in the history to knows the important changes to +# report: +# RELNOTES: indicates a change important the user. +# RELNOTES[NEW]: introduces a new feature. +# RELNOTES[INC]: indicates an incompatible change. +# The previous releases base is detected using the CHANGELOG file from the +# repository. +RELNOTES_TYPES=("INC" "NEW" "") +RELNOTES_DESC=("Incompatible changes" "New features" "Important changes") + +# Get the baseline version and cherry-picks of the previous release +# Parameter: $1 is the path to the changelog file +# Output: "${BASELINE} ${CHERRYPICKS}" +# BASELINE is the hash of the baseline commit of the latest release +# CHERRYPICKS is the list of hash of cherry-picked commits of the latest release +# return 1 if there is no initial release +function get_last_release() { + local changelog=$1 + [ -f "$changelog" ] || return 1 # No changelog = initial release + local BASELINE_LINE=$(grep -m 1 -n '^Baseline: ' "$changelog") || return 1 + [ -n "${BASELINE_LINE}" ] || return 1 # No baseline = initial release + local BASELINE_LINENB=$(echo "${BASELINE_LINE}" | cut -d ":" -f 1) + BASELINE=$(echo "${BASELINE_LINE}" | cut -d " " -f 2) + local CHERRYPICK_LINE=$(($BASELINE_LINENB + 1)) + # grep -B999 looks for all lines before the empty line and after that we + # restrict to only lines with the cherry picked hash then finally we cut + # the hash. + local CHERRY_PICKS=$(tail -n +${CHERRYPICK_LINE} "$changelog" \ + | grep -m 1 "^$" -B999 \ + | grep -E '^ \+ [a-z0-9]+:' \ + | cut -d ":" -f 1 | cut -d "+" -f 2) + echo $BASELINE $CHERRY_PICKS + return 0 +} + +# Now get the list of commit with a RELNOTES since latest release baseline ($1) +# discarding cherry_picks ($2..) and rollbacks. The returned list of commits is +# from the oldest to the newest +function get_release_notes_commits() { + local baseline=$1 + shift + local cherry_picks="$@" + local rollback_commits=$(git log --oneline -E --grep='^Rollback of commit [a-z0-9]+.$' ${baseline}.. \ + | grep -E '^[a-z0-9]+ Rollback of commit [a-z0-9]+.$') + local rollback_hashes=$(echo "$rollback_commits" | cut -d " " -f 1) + local rolledback_hashes=$(echo "$rollback_commits" | cut -d " " -f 5 | sed -E 's/^(.......).*$/\1/') + local exclude_hashes=$(echo $cherry_picks $rollback_hashes $rolledback_hashes | xargs echo | sed 's/ /|/g') + git log --reverse --pretty=format:%h ${baseline}.. -E --grep='^RELNOTES(\[[^\]+\])?:' \ + | grep -Ev "^(${exclude_hashes})" +} + +# Extract the release note from a commit hash ($1). It extracts +# the RELNOTES([??]): lines. A new empty line ends the relnotes tag. +# It adds the relnotes, if not "None" ("None.") or "n/a" ("n/a.") to +# the correct array: +# RELNOTES_INC for incompatible changes +# RELNOTES_NEW for new features changes +# RELNOTES for other changes +function extract_release_note() { + local relnote="$(git show -s $1 --pretty=format:%B | awk '/^RELNOTES(\[[^\]]+\])?:/,/^$/')" + local regex="^RELNOTES(\[([a-zA-Z]*)\])?:[[:space:]]*([^[:space:]].*[^[:space:]])[[:space:]]*$" + if [[ "$relnote" =~ $regex ]]; then + local relnote_kind=${BASH_REMATCH[2]} + local relnote_text="${BASH_REMATCH[3]}" + if [[ ! "$(echo $relnote_text | awk '{print tolower($0)}')" =~ ^(none|n/a)?.?$ ]]; then + eval "RELNOTES_${relnote_kind}+=(\"\${relnote_text}\")" + fi + fi +} + +# Build release notes arrays from a list of commits ($@) and return the release +# note in an array of array. +function get_release_notes() { + for i in "${RELNOTES_TYPES[@]}"; do + eval "RELNOTES_${i}=()" + done + for i in $@; do + extract_release_note $i + done +} + +# Returns the list of release notes in arguments into a list of points in +# a markdown list. The release notes are wrapped to 70 characters so it +# displays nicely in a git history. +function format_release_notes() { + local i + for (( i=1; $i <= $#; i=$i+1 )); do + local relnote="${!i}" + local lines=$(echo "$relnote" | fmt -w 66) # wrap to 70 counting the 4 leading spaces. + echo " - $lines" | head -1 + echo "$lines" | tail -n +2 | sed 's/^/ /' + done +} + +# Create the release notes since commit $1 ($2...${[#]} are the cherry-picks, +# so the commits to ignore. +function release_notes() { + local i + local commits=$(get_release_notes_commits $@) + local length="${#RELNOTES_TYPES[@]}" + get_release_notes "$commits" + for (( i=0; $i < $length; i=$i+1 )); do + local relnotes_title="${RELNOTES_DESC[$i]}" + local relnotes_type=${RELNOTES_TYPES[$i]} + local relnotes="RELNOTES_${relnotes_type}[@]" + local nb_relnotes=$(eval "echo \${#$relnotes}") + if (( "${nb_relnotes}" > 0 )); then + echo "${relnotes_title}:" + echo + format_release_notes "${!relnotes}" + echo + fi + done +} + +# A wrapper around all the previous function, using the CHANGELOG.md +# file in $1 to compute the last release commit hash. +function create_release_notes() { + local last_release=$(get_last_release "$1") || \ + { echo "Initial release."; return 0; } + [ -n "${last_release}" ] || { echo "Initial release."; return 0; } + release_notes ${last_release} +} + +# Create the revision information given a list of commits. The first +# commit should be the baseline, and the other one are the cherry-picks. +# The result is of the form: +# Baseline: BASELINE_COMMIT +# + CHERRY_PICK1: commit message summary of the CHERRY_PICK1. This +# message will be wrapped into 70 columns. +# + CHERRY_PICK2: commit message summary of the CHERRY_PICK2. +function create_revision_information() { + echo "Baseline: $1" + shift + while [ -n "${1-}" ]; do + local hash="$1" + local subject=$(git show -s --pretty=format:%s $hash) + local lines=$(echo "$subject" | fmt -w 56) # 14 leading spaces. + echo " + $hash: $lines" | head -1 + echo "$lines" | tail -n +2 | sed 's/^/ /' + shift + done +} diff --git a/scripts/release/relnotes_test.sh b/scripts/release/relnotes_test.sh new file mode 100755 index 0000000000..32d571134e --- /dev/null +++ b/scripts/release/relnotes_test.sh @@ -0,0 +1,204 @@ +#!/bin/bash + +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Tests release notes generation (relnotes.sh) +set -eu + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +source ${SCRIPT_DIR}/testenv.sh || { echo "testenv.sh not found!" >&2; exit 1; } + +### Setup a git repository +setup_git_repository + +### Load the relnotes script +source ${SCRIPT_DIR}/relnotes.sh || { echo "relnotes.sh not found!" >&2; exit 1; } + +### Tests method + +function set_up() { + cd ${MASTER_ROOT} +} + +function test_format_release_notes() { + local expected=' - Lorem ipsus I do not know more of latin than that but I need to + type random text that spans multiple line so we can test that the + wrapping of lines works as intended. + - Another thing I must type. + - Yet another test that spans across multiple lines so I must type + some random stuff to test wrapping.' + local input=("Lorem ipsus I do not know more of latin \ +than that but I need to type random text that spans multiple line so we \ +can test that the wrapping of lines works as intended." +"Another thing I must type." +"Yet another test that spans across multiple lines so I must type \ +some random stuff to test wrapping.") + assert_equals "${expected}" "$(format_release_notes "${input[@]}")" +} + +function test_get_release_notes_commits() { + # Generated with git log --grep RELNOTES. + # Only 6d98f6c 53c0748 are removed (rollback). + commits="0188971 957934c 7a99c7f b5ba24a c9041bf 8232d9b 422c731 e9029d4 \ +cc44636 06b09ce 29b05c8 67944d8 e8f6647 6d9fb36 f7c9922 5c0e4b2 9e387dd \ +98c9274 db4d861 a689f29 db487ce 965c392 bb59d88 d3461db cef25c4 14d905b" + assert_equals "$commits" "$(get_release_notes_commits 00d7223 | xargs)" + assert_equals "$(echo "$commits" | sed 's/957934c //')" \ + "$(get_release_notes_commits 00d7223 957934c | xargs)" +} + +TEST_INC_CHANGE='Incompatible changes: + + - Remove deprecated "make var" INCDIR + +' +TEST_NEW_CHANGE='New features: + + - added --with_aspect_deps to blaze query, that prints additional + information about aspects of target when --output is set to {xml, + proto, record}. + +' +TEST_CHANGE='Important changes: + + - Use a default implementation of a progress message, rather than + defaulting to null for all SpawnActions. + - Attribute error messages related to Android resources are easier + to understand now.' + +function test_release_notes() { + assert_equals "$TEST_INC_CHANGE$(echo)$TEST_NEW_CHANGE$(echo)$TEST_CHANGE" \ + "$(release_notes 965c392)" + assert_equals "$TEST_NEW_CHANGE$(echo)$TEST_CHANGE" \ + "$(release_notes 965c392 bb59d88)" +} + +function test_get_last_release() { + rm -f ${TEST_TMPDIR}/CHANGELOG.md + if (get_last_release "${TEST_TMPDIR}/CHANGELOG.md"); then + fail "Should have returned false for initial release" + fi + cat <${TEST_TMPDIR}/CHANGELOG.md +## No release +EOF + if (get_last_release "${TEST_TMPDIR}/CHANGELOG.md"); then + fail "Should have returned false when no release exists" + fi + cat <${TEST_TMPDIR}/CHANGELOG.md +## New release + +Baseline: 965c392 + +Initial release without cherry-picks + +EOF + assert_equals "965c392" \ + "$(get_last_release "${TEST_TMPDIR}/CHANGELOG.md")" + + + mv ${TEST_TMPDIR}/CHANGELOG.md ${TEST_TMPDIR}/CHANGELOG.md.bak + cat <${TEST_TMPDIR}/CHANGELOG.md +## Cherry-picking bb59d88 + +Baseline: 965c392 + + bb59d88: RELNOTES[INC]: Remove deprecated "make var" INCDIR + +$TEST_INC_CHANGE +EOF + cat ${TEST_TMPDIR}/CHANGELOG.md.bak >>${TEST_TMPDIR}/CHANGELOG.md + rm ${TEST_TMPDIR}/CHANGELOG.md.bak + assert_equals "965c392 bb59d88" \ + "$(get_last_release "${TEST_TMPDIR}/CHANGELOG.md")" + + mv ${TEST_TMPDIR}/CHANGELOG.md ${TEST_TMPDIR}/CHANGELOG.md.bak + cat <${TEST_TMPDIR}/CHANGELOG.md +## Cherry-picking bb59d88 and 14d905b + +Baseline: 965c392 + + bb59d88: RELNOTES[INC]: Remove deprecated "make var" INCDIR + + 14d905b: Add --with_aspect_deps flag to blaze query. This flag + should produce additional information about aspect + dependencies when --output is set to {xml, proto}. + +$TEST_INC_CHANGE +$TEST_NEW_CHANGE +EOF + cat ${TEST_TMPDIR}/CHANGELOG.md.bak >>${TEST_TMPDIR}/CHANGELOG.md + rm ${TEST_TMPDIR}/CHANGELOG.md.bak + assert_equals "965c392 bb59d88 14d905b" \ + "$(get_last_release "${TEST_TMPDIR}/CHANGELOG.md")" + +} + +function test_create_release_notes() { + cat <${TEST_TMPDIR}/CHANGELOG.md +## New release + +Baseline: 965c392 + +Initial release without cherry-picks + +EOF + assert_equals "$TEST_INC_CHANGE$(echo)$TEST_NEW_CHANGE$(echo)$TEST_CHANGE" \ + "$(create_release_notes ${TEST_TMPDIR}/CHANGELOG.md)" + + cat <<'EOF' >${TEST_TMPDIR}/CHANGELOG.md +## Cherry-picking bb59d88 + +``` +Baseline: 965c392 + + bb59d88: RELNOTES[INC]: Remove deprecated "make var" INCDIR +``` + +EOF + cat <>${TEST_TMPDIR}/CHANGELOG.md +$TEST_INC_CHANGE +EOF + assert_equals "$TEST_NEW_CHANGE$(echo)$TEST_CHANGE" \ + "$(create_release_notes ${TEST_TMPDIR}/CHANGELOG.md)" + assert_equals "965c392 bb59d88" \ + "$(get_last_release "${TEST_TMPDIR}/CHANGELOG.md")" + + cat <<'EOF' >${TEST_TMPDIR}/CHANGELOG.md +## Cherry-picking bb59d88 and 14d905b + +``` +Baseline: 965c392 + + bb59d88: RELNOTES[INC]: Remove deprecated "make var" INCDIR + + 14d905b: Add --with_aspect_deps flag to blaze query. This flag + should produce additional information about aspect + dependencies when --output is set to {xml, proto}. +``` + +EOF + cat <>${TEST_TMPDIR}/CHANGELOG.md +$TEST_INC_CHANGE +$TEST_NEW_CHANGE +EOF + assert_equals "$TEST_CHANGE" \ + "$(create_release_notes ${TEST_TMPDIR}/CHANGELOG.md)" +} + +function test_create_revision_information() { + expected='Baseline: 965c392 + + bb59d88: RELNOTES[INC]: Remove deprecated "make var" INCDIR + + 14d905b: Add --with_aspect_deps flag to blaze query. This flag + should produce additional information about aspect + dependencies when --output is set to {xml, proto}.' + assert_equals "$expected" \ + "$(create_revision_information 965c392 bb59d88 14d905b)" +} + +run_suite "Release notes generation tests" diff --git a/scripts/release/testenv.sh b/scripts/release/testenv.sh new file mode 100755 index 0000000000..9a57ebe753 --- /dev/null +++ b/scripts/release/testenv.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Copyright 2015 Google Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Setting up the environment for Bazel release scripts test. + +[ -z "$TEST_SRCDIR" ] && { echo "TEST_SRCDIR not set!" >&2; exit 1; } + +# Load the unit-testing framework +source "${TEST_SRCDIR}/src/test/shell/unittest.bash" || \ + { echo "Failed to source unittest.bash" >&2; exit 1; } + +# Commit at which we cut the master to do the test so we always take the git +# repository in a consistent state. +: ${MASTER_COMMIT:=7d41d7417fc34f7fa8aac7130a0588b8557e4b57} + +# Set-up a copy of the git repository in ${MASTER_ROOT}, pointing master +# to ${MASTER_COMMIT}. +function setup_git_repository() { + local origin_git_root=${TEST_SRCDIR} + MASTER_ROOT=${TEST_TMPDIR}/git/root + local orig_dir=${PWD} + # Create a new origin with the good starting point + mkdir -p ${MASTER_ROOT} + cd ${MASTER_ROOT} + cp -RL ${origin_git_root}/.git .git + rm -f .git/hooks/* # Do not keep custom hooks + git reset -q --hard HEAD + git checkout -q -B master ${MASTER_COMMIT} + cd ${orig_dir} +} -- cgit v1.2.3