#!/usr/bin/env bash # # Copyright (c) 2020 ETH Zurich, University of Bologna # SPDX-License-Identifier: Apache-2.0 # # See `usage()` for description, or execute with the `--help` flag. # # Authors: # - Andreas Kurth <akurth@iis.ee.ethz.ch> set -euo pipefail readonly THIS_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" readonly REPO_ROOT="$(readlink -f "$THIS_DIR/..")" ### Settings ####################################################################################### # Changelog readonly CHANGELOG_FILE="$REPO_ROOT/CHANGELOG.md" # FuseSoC readonly FUSESOC_FILE="$REPO_ROOT/axi.core" readonly FUSESOC_IP_VLN='pulp-platform.org::axi' # Git readonly GIT_MAIN_BRANCH='master' readonly GIT_REMOTE='origin' # Version file readonly VERSION_FILE="$REPO_ROOT/VERSION" #################################################################################################### ### Generic helper functions ####################################################################### git_cur_ver_tag() { git describe --match 'v*' --abbrev=0 } semver_major() { echo "$1" | cut -d. -f1 } semver_minor() { echo "$1" | cut -d. -f2 } semver_noprerel() { echo "$1" | cut -d- -f1 } semver_patch() { echo "$1" | cut -d. -f3 } git_cur_semver_noprerel() { local -r GIT_CUR_VER_TAG="$(git_cur_ver_tag)" semver_noprerel "${GIT_CUR_VER_TAG:1}" } stderr() { echo "$@" >&2 } confirm() { read -n 1 -p "$@ [yN] " yn case "$yn" in [Yy]) stderr "";; *) exit 1;; esac } #################################################################################################### usage() { stderr "Do a release: update version numbers in files, update Changelog, and publish on GitHub." stderr -e "\nUsage:" stderr -e "\t$(basename $0) [flags] <major|minor|patch>" stderr -e "\nArguments:" stderr -e "\t<major|minor|patch>\tType of release to create" stderr -e "\nFlags:" stderr -e "\t --dev\tIncrement development version but do not create a release" stderr -e "\t\t\tand do not push anything." stderr -e "\t-h, --help\tPrint usage information and exit." } # Parse flags. DEV_ONLY=false PARAMS="" while (( "$#" )); do case "$1" in --dev) DEV_ONLY=true shift;; -h|--help) usage exit 0;; -*|--*) # unsupported flags stderr "Error: Unsupported flag '$1'" exit 1;; *) # preserve positional arguments PARAMS="$PARAMS $1" shift;; esac done eval set -- "$PARAMS" readonly DEV_ONLY # Parse positional arguments. if test "$#" -ne 1; then usage exit 1 fi case "$1" in major|minor|patch) readonly REL_TYPE="$1";; *) usage exit 1 esac ### Implementation functions ####################################################################### # Update FuseSoC core description. fusesoc() { sed -i -e "s/name\s*:\s*$FUSESOC_IP_VLN.*/name : $FUSESOC_IP_VLN:$1/" "$FUSESOC_FILE" git add "$FUSESOC_FILE" } # Update `VERSION` file. version_file() { echo "$1" > "$VERSION_FILE" git add "$VERSION_FILE" } # Calculate the next SemVer. # First argument: current SemVer (without pre-release suffixes). # Second argument: type of increment (major|minor|patch). semver_next() { local -r CUR_MAJOR=$(semver_major $1) local -r CUR_MINOR=$(semver_minor $1) local -r CUR_PATCH=$(semver_patch $1) case $2 in major) local -r NEW_MAJOR=$(($CUR_MAJOR + 1)) local -r NEW_MINOR=0 local -r NEW_PATCH=0 ;; minor) local -r NEW_MAJOR=$CUR_MAJOR local -r NEW_MINOR=$(($CUR_MINOR + 1)) local -r NEW_PATCH=0 ;; patch) local -r NEW_MAJOR=$CUR_MAJOR local -r NEW_MINOR=$CUR_MINOR local -r NEW_PATCH=$(($CUR_PATCH + 1)) ;; *) stderr "Fatal: unreachable code reached!" exit 1 ;; esac echo "$NEW_MAJOR.$NEW_MINOR.$NEW_PATCH" } # Increment development version incr_dev_ver() { local -r DEV_VER="$(semver_next "$(git_cur_semver_noprerel)" "$1")-dev" fusesoc "$DEV_VER" version_file "$DEV_VER" git commit -m "Increment development version towards next $1 release" stderr git show stderr 'Updated files to increment development version and created commit shown above.' confirm 'Is this commit correct?' } #################################################################################################### # Make sure there are no staged changes. if ! git diff --staged --quiet; then stderr "Error: You have staged changes." stderr "Commit them before running this script." exit 1 fi # Make sure the files we are about to modify contain no uncommitted changes. ensure_clean() { if ! git diff --quiet -- "$1"; then stderr "Error: '$(realpath --relative-to="$REPO_ROOT" "$1")' contains uncommitted changes!" exit 1 fi } for f in "$CHANGELOG_FILE" "$FUSESOC_FILE" "$VERSION_FILE"; do ensure_clean "$f" done if $DEV_ONLY; then incr_dev_ver $REL_TYPE # If we only have to increment the development version, we are done. exit 0 fi # Calculate the next version. readonly NEW_VER="$(semver_next "$(git_cur_semver_noprerel)" "$REL_TYPE")" readonly NEW_GIT_VER_TAG="v$NEW_VER" # Create release branch. readonly NEW_BRANCH="release-$NEW_VER" git checkout -b "$NEW_BRANCH" # Update Changelog. # First, remove unused subsections from `Unreleased` section. gawk -i inplace ' BEGIN { unreleased=0; subhdr=""; subhdrprinted=1; } { # Determine if we are in the "Unreleased" section. if (unreleased) { if ($0 ~ /^##\s+.*$/) { unreleased=0; } } else if ($0 ~ /^##\s+Unreleased$/) { unreleased=1; } if (unreleased) { # If we are in the "Unreleased" section, process subheaders. if ($0 ~ /^###\s+.*$/) { # Current line is a subheader subhdr=$0; subhdrprinted=0; } else { # Current line is not a subheader. if (subhdrprinted) { # Print current line if the subheader was already printed. print $0; } else if (!($0 ~ /^\s*$/)) { # Otherwise, if current line is not empty, print the subheader and the current line. print subhdr; subhdrprinted=1; print $0; } } } else { # If we are outside the "Unreleased" section, print each line as-is. print $0; } prev=$0; }' "$CHANGELOG_FILE" # Second, extract release notes to temporary file. readonly REL_NOTES_FILE_2="$(mktemp)" gawk ' BEGIN { unreleased=0; } { # Determine if we are in the "Unreleased" section. if (unreleased) { if ($0 ~ /^##\s+.*$/) { nextfile; } } else if ($0 ~ /^##\s+Unreleased$/) { unreleased=1; next; } if (unreleased) { print $0; } }' "$CHANGELOG_FILE" > "$REL_NOTES_FILE_2" readonly REL_NOTES_FILE="$(mktemp)" # Remove empty lines from beginning and end of release notes. # Source: https://unix.stackexchange.com/a/552198 awk 'NF {p=1} p' <<< "$(< $REL_NOTES_FILE_2)" > "$REL_NOTES_FILE" rm "$REL_NOTES_FILE_2" # Third, make sure there are (still) two empty lines above every section header. gawk -i inplace '{ if ($0 ~ /^##\s+.*$/) { if (!(prev ~ /^\s*$/)) { print ""; print ""; } else if (!(pprev ~ /^\s*$/)) { print ""; } } print $0; pprev=prev; prev=$0; }' "$CHANGELOG_FILE" # Fourth, rename 'Unreleased' to release version and UTC date. sed -i -e "s/Unreleased/$NEW_VER - $(date -u --iso-8601=date)/" "$CHANGELOG_FILE" # Finally, stage Changelog modifications. git add "$CHANGELOG_FILE" # Update other files for release. fusesoc $NEW_VER version_file $NEW_VER # Create release commit. git commit -m "Release v$NEW_VER" # Let user review release commit, then ask for confirmation to continue. git show stderr 'Updated files and created release commit; it is shown above.' confirm 'Is the release commit correct?' # Create Git tag for release and push tag. git tag -a "$NEW_GIT_VER_TAG" -m "Release v$NEW_VER" stderr "Git-tagged release commit as '$NEW_GIT_VER_TAG'." # Create commit to continue development. sed -i -e '7a\ ## Unreleased\ \ ### Added\ \ ### Changed\ \ ### Fixed\ \ ' "$CHANGELOG_FILE" git add "$CHANGELOG_FILE" incr_dev_ver 'patch' # Push branch with release and post-release commit and let user review it. git push -u "$GIT_REMOTE" "$NEW_BRANCH" stderr stderr "Up to this point, all commits have been created on the '$NEW_BRANCH' branch," stderr "and the release tag only exists on your local machine." stderr "Now review the '$NEW_BRANCH' branch." stderr stderr 'Then confirm that I shall continue to:' stderr "1. Merge (using fast-forward) '$NEW_BRANCH' to '$GIT_MAIN_BRANCH'." stderr "2. Push '$GIT_MAIN_BRANCH'." stderr "3. Push the '$NEW_GIT_VER_TAG' tag to '$GIT_REMOTE'." confirm 'Those are non-reversible operations. Do you want to continue?' # Fast-forward main branch to release branch and push main branch. git checkout "$GIT_MAIN_BRANCH" git merge --ff-only "$NEW_BRANCH" git push "$GIT_REMOTE" "$GIT_MAIN_BRANCH" # Push Git tag of release. git push "$GIT_REMOTE" "$NEW_GIT_VER_TAG" # Create release with GitHub CLI. if which gh >/dev/null; then readonly GH_VERSION=$(gh --version | grep -o 'version\s\+\S\+' | \ grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') if test $(semver_major $GH_VERSION) -ge 1; then stderr stderr "Creating Release 'v$NEW_VER' on GitHub." gh release create "$NEW_GIT_VER_TAG" --title "v$NEW_VER" -F "$REL_NOTES_FILE" else echo "GitHub CLI >= 1.0.0 required but $GH_VERSION installed." echo "Please create GitHub release manually." fi else echo "GitHub CLI executable not found; please create GitHub release manually." fi # Remove temporary file for release notes. rm "$REL_NOTES_FILE" # Remove release branch, both remotely and locally. stderr stderr "Deleting '$NEW_BRANCH' locally and remotely." git push "$GIT_REMOTE" --delete "$NEW_BRANCH" git branch -d "$NEW_BRANCH" stderr stderr "All done!"