-
Notifications
You must be signed in to change notification settings - Fork 894
feat: Add release.sh
script and detect breaking changes
#5366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
19a39b4
7d372f9
0f5883f
a5468e2
318be9a
8e89727
1cf1c84
3e6c295
c871cb7
10f15fc
906a324
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
release.sh
script and detect breaking changes
This commit introduces three new scripts: - `release.sh` To be run by a user on their local machine to preview and create a new release (tag + push) - `check_commit_metadata.sh` For e.g. detecting breaking changes - `genereate_release_notes.sh` To display the generated release notes, used for previews and in `publish_release.sh` The `release.sh` script can be run without arguments, and it will automatically determine if we're to do a patch or minor release. A minor release can be forced via `--minor` flag. Breaking changes can be annotated either via commit/merge title prefix (`feat!:`, `feat(api)!:`), or by adding the `release/breaking` label to the PR that was merged (on GitHub). Related #5233 This work will be followed up by changes to move the tag creation from `release.sh` to CI workflows, the `release.sh` will then become a lightweight wrapper to run the workflow.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
#!/usr/bin/env bash | ||
|
||
# Usage: source ./check_commit_tags.sh <revision range> | ||
# Usage: ./check_commit_tags.sh <revision range> | ||
# | ||
# Example: ./check_commit_tags.sh v0.13.1..971e3678 | ||
# | ||
# When sourced, this script will populate the COMMIT_METADATA_* variables | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applicable only to squash & merge - Do you think that we may need another script executed in CI to check if the title of PR is consistent? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, what do you mean by Applicable only to squash & merge? I guess we could make it clearer this script is only intended for the commit log on the Yes, we'll add an action/lint for PR title 👍🏻 (follow up PR) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍
I thought that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah right, yeah we'll only validate PR titles, I made the assumption when writing that we'll only squash merge. Obviously owners/admins could bypass this by pushing straight to |
||
# with the commit metadata for each commit in the revision range. | ||
# | ||
# Because this script does some expensive lookups via the GitHub API, it's | ||
mafredri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# results will be cached in the environment and restored if this script is | ||
# sourced a second time with the same arguments. | ||
|
||
set -euo pipefail | ||
# shellcheck source=scripts/lib.sh | ||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" | ||
|
||
range=${1:-} | ||
|
||
if [[ -z $range ]]; then | ||
error "No revision range specified" | ||
fi | ||
|
||
# Check dependencies. | ||
dependencies gh | ||
|
||
COMMIT_METADATA_BREAKING=0 | ||
declare -A COMMIT_METADATA_TITLE COMMIT_METADATA_CATEGORY | ||
|
||
main() { | ||
# Match a commit prefix pattern, e.g. feat: or feat(site):. | ||
prefix_pattern="^([a-z]+)(\([a-z]*\))?:" | ||
|
||
# If a commit contains this title prefix or the source PR contains the | ||
# label, patch releases will not be allowed. | ||
# This regex matches both `feat!:` and `feat(site)!:`. | ||
breaking_title="^[a-z]+(\([a-z]*\))?!:" | ||
breaking_label=release/breaking | ||
breaking_category=breaking | ||
|
||
mapfile -t commits < <(git log --no-merges --pretty=format:"%h %s" "$range") | ||
|
||
for commit in "${commits[@]}"; do | ||
mapfile -d ' ' -t parts <<<"$commit" | ||
commit_sha=${parts[0]} | ||
commit_prefix=${parts[1]} | ||
|
||
# Store the commit title for later use. | ||
title=${parts[*]:1} | ||
title=${title%$'\n'} | ||
COMMIT_METADATA_TITLE[$commit_sha]=$title | ||
|
||
# First, check the title for breaking changes. This avoids doing a | ||
# GH API request if there's a match. | ||
if [[ $commit_prefix =~ $breaking_title ]]; then | ||
COMMIT_METADATA_CATEGORY[$commit_sha]=$breaking_category | ||
COMMIT_METADATA_BREAKING=1 | ||
continue | ||
fi | ||
|
||
# Get the labels for the PR associated with this commit. | ||
mapfile -t labels < <(gh api -H "Accept: application/vnd.github+json" "/repos/coder/coder/commits/${commit_sha}/pulls" -q '.[].labels[].name') | ||
|
||
if [[ " ${labels[*]} " = *" ${breaking_label} "* ]]; then | ||
COMMIT_METADATA_CATEGORY[$commit_sha]=$breaking_category | ||
COMMIT_METADATA_BREAKING=1 | ||
continue | ||
fi | ||
|
||
if [[ $commit_prefix =~ $prefix_pattern ]]; then | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I still have a feeling that we should switch to Python or mage at this point :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I won't get behind Python, but I'm indifferent to mage, so it's not me you have to convince. 😉 |
||
commit_prefix=${BASH_REMATCH[1]} | ||
fi | ||
case $commit_prefix in | ||
feat | fix) | ||
COMMIT_METADATA_CATEGORY[$commit_sha]=$commit_prefix | ||
;; | ||
*) | ||
COMMIT_METADATA_CATEGORY[$commit_sha]=other | ||
;; | ||
esac | ||
done | ||
} | ||
|
||
declare_print_commit_metadata() { | ||
declare -p COMMIT_METADATA_BREAKING COMMIT_METADATA_TITLE COMMIT_METADATA_CATEGORY | ||
} | ||
|
||
export_commit_metadata() { | ||
_COMMIT_METADATA_CACHE="${range}:$(declare_print_commit_metadata)" | ||
export _COMMIT_METADATA_CACHE COMMIT_METADATA_BREAKING COMMIT_METADATA_TITLE COMMIT_METADATA_CATEGORY | ||
} | ||
|
||
# _COMMIT_METADATA_CACHE is used to cache the results of this script in | ||
# the environment because bash arrays are not passed on to subscripts. | ||
if [[ ${_COMMIT_METADATA_CACHE:-} == "${range}:"* ]]; then | ||
eval "${_COMMIT_METADATA_CACHE#*:}" | ||
else | ||
main | ||
fi | ||
|
||
export_commit_metadata | ||
|
||
# Make it easier to debug this script by printing the associative array | ||
# when it's not sourced. | ||
if ! issourced; then | ||
declare_print_commit_metadata | ||
fi |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
#!/usr/bin/env bash | ||
|
||
# Usage: ./generate_release_notes.sh --old-version <old version> --new-version <new version> --ref <ref> | ||
# | ||
# Example: ./generate_release_notes.sh --old-version v0.13.0 --new-version v0.13.1 --ref 1e6b244c | ||
# | ||
# This script generates release notes for the given version. It will generate | ||
# release notes for all commits between the old version and the new version. | ||
# | ||
# Ref must be set to the commit that the new version will be tagget at. This | ||
# is used to determine the commits that are included in the release. If the | ||
# commit is already tagged, ref can be set to the tag name. | ||
|
||
set -euo pipefail | ||
# shellcheck source=scripts/lib.sh | ||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" | ||
|
||
old_version= | ||
new_version= | ||
ref= | ||
|
||
args="$(getopt -o '' -l old-version:,new-version:,ref: -- "$@")" | ||
eval set -- "$args" | ||
while true; do | ||
case "$1" in | ||
--old-version) | ||
old_version="$2" | ||
shift 2 | ||
;; | ||
--new-version) | ||
new_version="$2" | ||
shift 2 | ||
;; | ||
--ref) | ||
ref="$2" | ||
shift 2 | ||
;; | ||
--) | ||
shift | ||
break | ||
;; | ||
*) | ||
error "Unrecognized option: $1" | ||
;; | ||
esac | ||
done | ||
|
||
# Check dependencies. | ||
dependencies gh sort | ||
|
||
if [[ -z $old_version ]]; then | ||
error "No old version specified" | ||
fi | ||
if [[ -z $new_version ]]; then | ||
error "No new version specified" | ||
fi | ||
if [[ -z $ref ]]; then | ||
error "No ref specified" | ||
fi | ||
|
||
# shellcheck source=scripts/check_commit_metadata.sh | ||
source "$SCRIPT_DIR/check_commit_metadata.sh" "${old_version}..${ref}" | ||
|
||
# Sort commits by title prefix, then by date, only return sha at the end. | ||
mapfile -t commits < <(git log --no-merges --pretty=format:"%ct %h %s" "${old_version}..${ref}" | sort -k3,3 -k1,1n | cut -d' ' -f2) | ||
|
||
breaking_changelog= | ||
feat_changelog= | ||
fix_changelog= | ||
other_changelog= | ||
|
||
for commit in "${commits[@]}"; do | ||
line="- $commit ${COMMIT_METADATA_TITLE[$commit]}\n" | ||
|
||
case "${COMMIT_METADATA_CATEGORY[$commit]}" in | ||
breaking) | ||
breaking_changelog+="$line" | ||
;; | ||
feat) | ||
feat_changelog+="$line" | ||
;; | ||
fix) | ||
fix_changelog+="$line" | ||
;; | ||
*) | ||
other_changelog+="$line" | ||
;; | ||
esac | ||
done | ||
|
||
changelog="$( | ||
if ((${#breaking_changelog} > 0)); then | ||
echo -e "### BREAKING CHANGES\n" | ||
echo -e "$breaking_changelog" | ||
fi | ||
if ((${#feat_changelog} > 0)); then | ||
echo -e "### Features\n" | ||
echo -e "$feat_changelog" | ||
fi | ||
if ((${#fix_changelog} > 0)); then | ||
echo -e "### Bug fixes\n" | ||
echo -e "$fix_changelog" | ||
fi | ||
if ((${#other_changelog} > 0)); then | ||
echo -e "### Other changes\n" | ||
echo -e "$other_changelog" | ||
fi | ||
)" | ||
|
||
image_tag="$(execrelative ./image_tag.sh --version "$new_version")" | ||
|
||
echo -e "## Changelog: | ||
|
||
$changelog | ||
|
||
https://github.com/coder/coder/compare/${old_version}...${new_version} | ||
mafredri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
## Container image: | ||
|
||
- \`docker pull $image_tag\` | ||
" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -123,6 +123,9 @@ log() { | |
# error prints an error message and returns an error exit code. | ||
error() { | ||
log "ERROR: $*" | ||
if issourced; then | ||
return 1 | ||
fi | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you put a comment here explaining why this is necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I'll just remove it. It's not really necessary if we always want to exit on |
||
exit 1 | ||
} | ||
|
||
|
@@ -131,6 +134,12 @@ isdarwin() { | |
[[ "${OSTYPE:-darwin}" == *darwin* ]] | ||
} | ||
|
||
# issourced returns true if the script that sourced this script is being | ||
# sourced by another. | ||
issourced() { | ||
[[ "${BASH_SOURCE[1]}" != "$0" ]] | ||
} | ||
|
||
# We don't need to check dependencies more than once per script, but some | ||
# scripts call other scripts that also `source lib.sh`, so we set an environment | ||
# variable after successfully checking dependencies once. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
#!/usr/bin/env bash | ||
|
||
# This script should be called to create a new release. | ||
# | ||
# When run, this script will display the new version number and optionally a | ||
# preview of the release notes. The new version will be selected automatically | ||
# based on if the release contains breaking changes or not. If the release | ||
# contains breaking changes, a new minor version will be created. Otherwise, a | ||
# new patch version will be created. | ||
# | ||
# Set --ref if you need to specify a specific commit that the new version will | ||
# be tagged at, otherwise the latest commit will be used. | ||
# | ||
# Set --minor to force a minor version bump, even when there are no breaking | ||
# changes. | ||
# | ||
# To mark a release as containing breaking changes, the commit title should | ||
# either contain a known prefix with an exclamation mark ("feat!:", | ||
# "feat(api)!:") or the PR that was merged can be tagged with the | ||
# "release/breaking" label. | ||
# | ||
# Usage: ./release.sh [--ref <ref>] [--minor] | ||
|
||
set -euo pipefail | ||
# shellcheck source=scripts/lib.sh | ||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" | ||
cdroot | ||
|
||
ref= | ||
minor=0 | ||
|
||
args="$(getopt -o n -l ref:,minor -- "$@")" | ||
eval set -- "$args" | ||
while true; do | ||
case "$1" in | ||
--ref) | ||
ref="$2" | ||
shift 2 | ||
;; | ||
--minor) | ||
minor=1 | ||
shift | ||
;; | ||
--) | ||
shift | ||
break | ||
;; | ||
*) | ||
error "Unrecognized option: $1" | ||
;; | ||
esac | ||
done | ||
|
||
# Check dependencies. | ||
dependencies gh sort | ||
|
||
# Make sure the repository is up-to-date before generating release notes. | ||
log "Fetching main and tags from origin..." | ||
git fetch --quiet --tags origin main | ||
|
||
# Resolve to the latest ref on origin/main unless otherwise specified. | ||
ref=$(git rev-parse --short "${ref:-origin/main}") | ||
|
||
# Make sure that we're running the latest release script. | ||
if [[ -n $(git diff --name-status origin/main -- ./scripts/release.sh) ]]; then | ||
error "Release script is out-of-date. Please check out the latest version and try again." | ||
fi | ||
|
||
# Check the current version tag from GitHub (by number) using the API to | ||
# ensure no local tags are considered. | ||
mapfile -t versions < <(gh api -H "Accept: application/vnd.github+json" /repos/coder/coder/git/refs/tags -q '.[].ref | split("/") | .[2]' | grep '^v' | sort -r -V) | ||
old_version=${versions[0]} | ||
|
||
log "Checking commit metadata for changes since $old_version..." | ||
# shellcheck source=scripts/check_commit_metadata.sh | ||
source "$SCRIPT_DIR/check_commit_metadata.sh" "$old_version..$ref" | ||
|
||
mapfile -d . -t version_parts <<<"$old_version" | ||
if [[ $minor == 1 ]] || [[ $COMMIT_METADATA_BREAKING == 1 ]]; then | ||
if [[ $COMMIT_METADATA_BREAKING == 1 ]]; then | ||
log "Breaking change detected, incrementing minor version..." | ||
else | ||
log "Forcing minor version bump..." | ||
fi | ||
version_parts[1]=$((version_parts[1] + 1)) | ||
version_parts[2]=0 | ||
else | ||
log "No breaking changes detected, incrementing patch version..." | ||
version_parts[2]=$((version_parts[2] + 1)) | ||
fi | ||
new_version="${version_parts[0]}.${version_parts[1]}.${version_parts[2]}" | ||
|
||
log "Old version: ${old_version}" | ||
log "New version: ${new_version}" | ||
|
||
release_notes="$(execrelative ./generate_release_notes.sh --old-version "$old_version" --new-version "$new_version" --ref "$ref")" | ||
|
||
echo | ||
read -p "Preview release notes? (y/n) " -n 1 -r show_reply | ||
echo | ||
if [[ $show_reply =~ ^[Yy]$ ]]; then | ||
echo -e "$release_notes\n" | ||
fi | ||
|
||
read -p "Create release? (y/n) " -n 1 -r create | ||
echo | ||
if [[ $create =~ ^[Yy]$ ]]; then | ||
log "Tagging commit $ref as $new_version..." | ||
echo git tag -a "$new_version" -m "$new_version" "$ref" | ||
log "Pushing tag to origin..." | ||
echo git push -u origin "$new_version" | ||
mafredri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WDYT about placing all these scripts in the separate
release
directory?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we have enough release related scripts to put them there, I'd still like to keep
/scripts/release.sh
(because used by humans) but the rest can go in/scripts/release/*.sh
.