-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CI/CD pipeline for automated releases (#35)
Closes @28.
- Loading branch information
Showing
12 changed files
with
586 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
#!/bin/bash | ||
|
||
set -o errexit | ||
set -o pipefail | ||
set -o nounset | ||
set -o noclobber | ||
|
||
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) | ||
|
||
usage() { | ||
cat <<EOF | ||
Usage: $0 [--help] [--tag-prefix TAG] [--branch-prefix BRANCH] <VERSION> | ||
Check if a git tag exists in the current repo that should prevent creating a | ||
specific new tag. | ||
--help, -h Show this help and exit | ||
--tag-prefix, -t Tag prefix. Defaults to 'v' | ||
--branch-prefix, -b Release branch prefix. Defaults to 'release/' | ||
--target-branch Target branch. Defaults to 'master' | ||
The following checks are performed | ||
* A tag which is equivalent to the target version does not exist | ||
* A non-prerelease tag that matches the target version does not already exist | ||
* A separate release branch does not exist for the major.minor of the target | ||
version | ||
A semantic version is defined as equivalent if the major, minor, patch | ||
and prerelease information are all the same. This means that two versions may | ||
not be differentiated by only the build metadata, regardless of if only one of | ||
the versions being compared contains it. | ||
Exits with non-zero if you should not be allowed to create the target version | ||
EOF | ||
} | ||
|
||
check_if_tag_already_exists() { | ||
local -n components="$1" | ||
local tag_prefix="$2" | ||
|
||
# If a tag matching the prerelease and/or metadata exactly is found, then don't allow | ||
# | ||
# If a tag with prerelease and/or metadata is found, but our current version has a different | ||
# prerelease/metadata component, allow it | ||
# | ||
# If there is no prerelease/metadata and the tag matches exactly, then don't allow | ||
# | ||
# Allowed: | ||
# Existing | New | ||
# --------------|----------- | ||
# v1.0.0 | v0.0.1 | ||
# v1.0.0 | v0.0.0-rc1 | ||
# v1.0.0-rc1 | v1.0.0-rc2 | ||
# v1.0.0-rc1+g1 | v1.0.0-rc2+g1 | ||
# | ||
# Disallowed: | ||
# Existing | New | ||
# -------------|------------ | ||
# v1.0.0 | v1.0.0 | ||
# v1.0.0-rc1 | v1.0.0-rc1 | ||
# v1.0.0+g1234 | v1.0.0+gabcd | ||
|
||
# If you're making a prerelease, check to see that a "real" release hasn't been made, and that | ||
# no duplicate prerelease has been made | ||
if [[ -n "${components[prerelease]}" ]]; then | ||
local pattern="${tag_prefix}${components[major]}.${components[minor]}.${components[patch]}-${components[prerelease]}" | ||
local tags | ||
tags="$(git for-each-ref --format='%(refname:short)' "refs/tags/${pattern}*")" | ||
if [[ -n "$tags" ]]; then | ||
echo "Found existing prerelease tags:" >&2 | ||
for tag in $tags; do | ||
echo " $tag" >&2 | ||
done | ||
exit 1 | ||
fi | ||
|
||
# Also check if there's a "real" release for that version | ||
pattern="${tag_prefix}${components[major]}.${components[minor]}.${components[patch]}" | ||
tags="$(git for-each-ref --format='%(refname:short)' "refs/tags/${pattern}")" | ||
if [[ -n "$tags" ]]; then | ||
echo "You tried to make a prerelease of a version that was already released:" >&2 | ||
for tag in $tags; do | ||
echo " $tag" >&2 | ||
done | ||
exit 1 | ||
fi | ||
tags="$(git for-each-ref --format='%(refname:short)' "refs/tags/${pattern}+*")" | ||
if [[ -n "$tags" ]]; then | ||
echo "You tried to make a prerelease of a version that was already released:" >&2 | ||
for tag in $tags; do | ||
echo " $tag" >&2 | ||
done | ||
exit 1 | ||
fi | ||
# If there's no prerelease, we can't even re-use the same major.minor.patch, even if the build | ||
# metadata is different | ||
else | ||
local pattern="${tag_prefix}${components[major]}.${components[minor]}.${components[patch]}" | ||
local tags | ||
tags="$(git for-each-ref --format='%(refname:short)' "refs/tags/${pattern}")" | ||
if [[ -n "$tags" ]]; then | ||
echo "Found existing release tags:" >&2 | ||
for tag in $tags; do | ||
echo " $tag" >&2 | ||
done | ||
exit 1 | ||
fi | ||
tags="$(git for-each-ref --format='%(refname:short)' "refs/tags/${pattern}+*")" | ||
if [[ -n "$tags" ]]; then | ||
echo "Found existing release tag with metadata:" >&2 | ||
for tag in $tags; do | ||
echo " $tag" >&2 | ||
done | ||
exit 1 | ||
fi | ||
fi | ||
|
||
} | ||
|
||
check_if_maintenance_release_made_on_non_maintenance_branch() { | ||
local -n components="$1" | ||
local branch_prefix="$2" | ||
local target_branch="$3" | ||
|
||
local major_minor="${components[major]}.${components[minor]}" | ||
|
||
# Has there been a release branch for the specified version been made? | ||
local branches | ||
branches="$(git for-each-ref --format='%(refname:short)' "refs/heads/${branch_prefix}${major_minor}*")" | ||
if [[ -n "$branches" ]]; then | ||
echo "Found release branch for $major_minor: ${branches}" | ||
for branch in $branches; do | ||
if [[ "$branch" = "$target_branch" ]]; then | ||
echo "...but that branch was the target branch, so that's okay." | ||
return 0 | ||
fi | ||
done | ||
echo "If a release branch has been made for $major_minor, you can't release a new $major_minor version unless done from that release branch" | ||
exit 1 | ||
fi | ||
} | ||
|
||
main() { | ||
local version="" | ||
local tag_prefix="v" | ||
local branch_prefix="release/" | ||
local target_branch="main" | ||
|
||
while test $# -gt 0; do | ||
case "$1" in | ||
--help | -h) | ||
usage | ||
exit | ||
;; | ||
--tag-prefix | -t) | ||
tag_prefix="$2" | ||
shift | ||
;; | ||
--branch-prefix | -b) | ||
branch_prefix="$2" | ||
shift | ||
;; | ||
--target-branch) | ||
target_branch="$2" | ||
shift | ||
;; | ||
*) | ||
if [[ -z "$version" ]]; then | ||
version="$1" | ||
else | ||
echo "Unexpected argument '$1'" >&2 | ||
exit 1 | ||
fi | ||
;; | ||
esac | ||
shift | ||
done | ||
|
||
if [[ -z "$version" ]]; then | ||
echo "Missing required <VERSION> argument" >&2 | ||
exit 1 | ||
fi | ||
|
||
# shellcheck disable=SC1091 | ||
source "${SCRIPT_DIR}/parse-semver-version.sh" | ||
|
||
# shellcheck disable=SC2034 | ||
# nameref array passing confuses shellcheck | ||
local -A version_components | ||
# Exits the script if it fails to parse the version components | ||
get_tag_semver_components "$version" version_components | ||
|
||
check_if_tag_already_exists version_components "$tag_prefix" | ||
check_if_maintenance_release_made_on_non_maintenance_branch version_components "$branch_prefix" "$target_branch" | ||
} | ||
|
||
main "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
#!/bin/bash | ||
set -o errexit | ||
set -o pipefail | ||
set -o nounset | ||
set -o noclobber | ||
|
||
RED="\033[31m" | ||
GREEN="\033[32m" | ||
BLUE="\033[34m" | ||
RESET="\033[0m" | ||
|
||
debug() { | ||
echo -e "${BLUE}DEBUG:${RESET} $*" >&2 | ||
} | ||
|
||
info() { | ||
echo -e "${GREEN}INFO:${RESET} $*" >&2 | ||
} | ||
|
||
error() { | ||
echo -e "${RED}ERROR:${RESET} $*" >&2 | ||
} | ||
|
||
usage() { | ||
echo "Usage: $0 [--help] <VERSION> <DESCRIPTION>" | ||
echo | ||
echo "Positional Arguments:" | ||
echo | ||
echo " <VERSION> The version for which to generate the release notes" | ||
echo " <DESCRIPTION> The project description, because every release notes should describe" | ||
echo " what the project is for new users" | ||
echo | ||
echo "Options:" | ||
echo | ||
echo " --help, -h Show this help and exit" | ||
} | ||
|
||
parse_changelog() { | ||
local -r changelog="$1" | ||
local -r version="$2" | ||
|
||
# 0,/pat1/d deletes from line 1 to pat1 inclusive | ||
# /pat2/Q exits without printing on the first line to match pat2 | ||
sed "0,/^# Herostratus - $version/d;/^# /Q" "$changelog" | ||
} | ||
|
||
main() { | ||
local version="" | ||
local description="" | ||
|
||
while [[ $# -gt 0 ]]; do | ||
case "$1" in | ||
--help | -h) | ||
usage | ||
exit 0 | ||
;; | ||
-*) | ||
error "Unexpected option: $1" | ||
usage >&2 | ||
exit 1 | ||
;; | ||
*) | ||
if [[ -z "$version" ]]; then | ||
version="$1" | ||
elif [[ -z "$description" ]]; then | ||
description="$1" | ||
fi | ||
;; | ||
esac | ||
shift | ||
done | ||
|
||
if [[ -z "$version" ]]; then | ||
error "Missing required <VERSION> positional argument" | ||
exit 1 | ||
elif [[ -z "$description" ]]; then | ||
error "Missing required <DESCRIPTION> positional argument" | ||
exit 1 | ||
fi | ||
|
||
local repo_dir | ||
repo_dir=$(git rev-parse --show-toplevel) | ||
local -r changelog="$repo_dir/CHANGELOG.md" | ||
if [[ ! -f "$changelog" ]]; then | ||
error "Could not find '$changelog'" | ||
exit 1 | ||
fi | ||
|
||
# This outputs the version header to stdout | ||
if ! grep "^# Herostratus - $version" "$changelog"; then | ||
error "Could not find version '$version' in '$changelog'" | ||
exit 1 | ||
fi | ||
# Add the project description to the release notes | ||
echo "$description" | ||
# This outputs from the version header (exclusive) to the next version header (exclusive) | ||
parse_changelog "$changelog" "$version" | ||
} | ||
|
||
main "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
#!/bin/bash | ||
set -o errexit | ||
set -o pipefail | ||
set -o nounset | ||
set -o noclobber | ||
|
||
CARGO_MANIFEST="${1:-Cargo.toml}" | ||
MANIFEST_KEY="${2:-version}" | ||
|
||
cargo metadata \ | ||
--format-version=1 \ | ||
--manifest-path "$CARGO_MANIFEST" \ | ||
--no-deps | | ||
jq ".packages[0].$MANIFEST_KEY" | | ||
tr -d '"' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
#!/bin/bash | ||
# shellcheck disable=SC2154,SC2034 | ||
|
||
# Function to parse the components of a semantic version string with optional | ||
# prefix. | ||
# | ||
# Params: | ||
# $1 - The string to parse | ||
# $2 - The name of the variable to store the output in | ||
# | ||
# Returns: | ||
# The resulting components are stored in an associative array named according | ||
# to the second argument passed to the function. (See usage example) | ||
# | ||
# Exits with code 1 if the given text cannot be parsed | ||
# | ||
# Usage Example: | ||
# | ||
# source ./parse-semver-version.sh | ||
# | ||
# local -A tag_components | ||
# get_tag_semver_components v1.2.3-a+b tag_components | ||
# | ||
# echo "$tag_components[prefix]" | ||
# echo "$tag_components[major].$tag_components[minor].$tag_components[patch]" | ||
# echo "$tag_components[prerelease] -- $tag_components[buildmetadata]" | ||
get_tag_semver_components() { | ||
|
||
local tag="$1" | ||
# This is a "nameref" variable. $2 holds the name of the variable to update. | ||
# We declare it as an associative array in the calling function. | ||
local -n components="$2" | ||
|
||
# Taken from https://gist.github.com/rverst/1f0b97da3cbeb7d93f4986df6e8e5695, which itself is a | ||
# form of the PCRE regex from https://semver.org modified to work with Bash regular expressions, | ||
# and to allow an optional prefix for v1.0.0 style Git tags. | ||
# | ||
# Use the string "v1.2.3-0.ab.0001a.a+gabcd.xyz" to test. | ||
# | ||
# This parses the tag into the following capture groups: | ||
# 1 2 3 4 5 67 8 9 10 11 12 | ||
# (prefix )?(major ).(minor ).(patch )(-((prerelease )( ( )) ))?( +(buildmetadata( ) ))? | ||
local regex='^([a-zA-Z_/-]+)?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$' | ||
|
||
if [[ $tag =~ $regex ]]; then | ||
components[prefix]="${BASH_REMATCH[1]}" | ||
components[major]="${BASH_REMATCH[2]}" | ||
components[minor]="${BASH_REMATCH[3]}" | ||
components[patch]="${BASH_REMATCH[4]}" | ||
components[prerelease]="${BASH_REMATCH[6]}" | ||
components[buildmetadata]="${BASH_REMATCH[11]}" | ||
else | ||
echo "Failed to parse SemVer 2.0.0 tag '$tag'" >&2 | ||
exit 1 | ||
fi | ||
|
||
# The "components" array is "returned" via namerefs, similar to pass-by-reference in C++. | ||
} |
Oops, something went wrong.