diff --git a/release/create-release-branch.sh b/release/create-release-branch.sh index eb57472..f1b881f 100755 --- a/release/create-release-branch.sh +++ b/release/create-release-branch.sh @@ -1,80 +1,107 @@ #!/usr/bin/env bash # -# See README.adoc +# See README.md # set -euo pipefail -# set -x -REMOTE="origin" -#---------------------------------------------------------------------------------------------------- -# tags should be semver-compatible e.g. 23.1 and not 23.01 -# this is needed for cargo commands to work properly: although it is not strictly needed -# for the name of the release branch, the branch naming will be consistent with the cargo versioning. -#---------------------------------------------------------------------------------------------------- -RELEASE_REGEX="^[0-9][0-9]\.([1-9]|[1][0-2])$" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + +warn_if_branch_exists() { + local repo="$1" + ( # subshell to isolate cd + ensure_clone "$repo" + cd "$TEMP_RELEASE_FOLDER/$repo" + + if remote_branch_exists "$RELEASE_BRANCH"; then + >&2 echo "WARNING: Release branch ${RELEASE_BRANCH} already exists in ${repo}." + >&2 echo "For patch releases, use create-release-candidate-branch.sh instead." + >&2 echo "Continue anyway? (y/n)" + read -r response + if [[ "$response" != "y" && "$response" != "Y" ]]; then + >&2 echo "Aborting." + exit 1 + fi + fi + ) +} -update_products() { - if [ -d "$BASE_DIR/$DOCKER_IMAGES_REPO" ]; then - echo "Directory exists. Switching to ${RELEASE_BRANCH} branch and Updating..." - cd "$BASE_DIR/$DOCKER_IMAGES_REPO" - git pull && git switch "${RELEASE_BRANCH}" # Switch to local branch (remote doesn't yet exist) - else - echo "Repo directory ($BASE_DIR/$DOCKER_IMAGES_REPO) doesn't exist. Cloning and switching to ${RELEASE_BRANCH} branch" - git clone --branch main --depth 1 "git@github.com:stackabletech/${DOCKER_IMAGES_REPO}.git" "$BASE_DIR/$DOCKER_IMAGES_REPO" - cd "$BASE_DIR/$DOCKER_IMAGES_REPO" - # try to switch to the release branch (if continuing from someone else), or create it - git switch "${RELEASE_BRANCH}" 2> /dev/null || git switch -c "${RELEASE_BRANCH}" +check_existing_branches() { + if [ "products" == "$WHAT" ] || [ "all" == "$WHAT" ]; then + warn_if_branch_exists "$DOCKER_IMAGES_REPO" + fi + if [ "operators" == "$WHAT" ] || [ "all" == "$WHAT" ]; then + for_each_operator warn_if_branch_exists fi + if [ "demos" == "$WHAT" ] || [ "all" == "$WHAT" ]; then + warn_if_branch_exists "$DEMOS_REPO" + fi +} + +update_products() { + ( # subshell to isolate cd + if [ -d "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" ]; then + echo "Directory exists. Switching to ${RELEASE_BRANCH} branch and Updating..." + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + require_clean_worktree "$DOCKER_IMAGES_REPO" + git pull && git switch "${RELEASE_BRANCH}" + else + echo "Repo directory ($TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO) doesn't exist. Cloning and switching to ${RELEASE_BRANCH} branch" + git clone --branch main --depth 1 "git@github.com:stackabletech/${DOCKER_IMAGES_REPO}.git" "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + git switch "${RELEASE_BRANCH}" 2> /dev/null || git switch -c "${RELEASE_BRANCH}" + fi - push_branch "$DOCKER_IMAGES_REPO" + push_branch "$DOCKER_IMAGES_REPO" - echo - echo "Check $BASE_DIR/$DOCKER_IMAGES_REPO" + echo + echo "Check $TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + ) } -update_operators() { - while IFS="" read -r operator || [ -n "$operator" ] - do - if [ -d "$BASE_DIR/${operator}" ]; then +update_single_operator() { + local operator="$1" + ( # subshell to isolate cd + if [ -d "$TEMP_RELEASE_FOLDER/${operator}" ]; then echo "Directory exists. Switching to ${RELEASE_BRANCH} branch and Updating..." - cd "$BASE_DIR/${operator}" - git pull && git switch "${RELEASE_BRANCH}" # Switch to local branch (remote doesn't yet exist) + cd "$TEMP_RELEASE_FOLDER/${operator}" + require_clean_worktree "$operator" + git pull && git switch "${RELEASE_BRANCH}" else - echo "Repo directory ($BASE_DIR/$operator) doesn't exist. Cloning and switching to ${RELEASE_BRANCH} branch" - git clone --branch main --depth 1 "git@github.com:stackabletech/${operator}.git" "$BASE_DIR/${operator}" - cd "$BASE_DIR/${operator}" - # try to switch to the release branch (if continuing from someone else), or create it + echo "Repo directory ($TEMP_RELEASE_FOLDER/$operator) doesn't exist. Cloning and switching to ${RELEASE_BRANCH} branch" + git clone --branch main --depth 1 "git@github.com:stackabletech/${operator}.git" "$TEMP_RELEASE_FOLDER/${operator}" + cd "$TEMP_RELEASE_FOLDER/${operator}" git switch "${RELEASE_BRANCH}" || git switch -c "${RELEASE_BRANCH}" fi push_branch "$operator" - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + ) } update_demos() { - if [ -d "$BASE_DIR/$DEMOS_REPO" ]; then - cd "$BASE_DIR/$DEMOS_REPO" - git pull && git switch "${RELEASE_BRANCH}" - else - git clone --branch main --depth 1 "git@github.com:stackabletech/${DEMOS_REPO}.git" "$BASE_DIR/$DEMOS_REPO" - cd "$BASE_DIR/$DEMOS_REPO" - git switch "${RELEASE_BRANCH}" 2> /dev/null || git switch -c "${RELEASE_BRANCH}" - fi + ( # subshell to isolate cd + if [ -d "$TEMP_RELEASE_FOLDER/$DEMOS_REPO" ]; then + cd "$TEMP_RELEASE_FOLDER/$DEMOS_REPO" + require_clean_worktree "$DEMOS_REPO" + git pull && git switch "${RELEASE_BRANCH}" + else + git clone --branch main --depth 1 "git@github.com:stackabletech/${DEMOS_REPO}.git" "$TEMP_RELEASE_FOLDER/$DEMOS_REPO" + cd "$TEMP_RELEASE_FOLDER/$DEMOS_REPO" + git switch "${RELEASE_BRANCH}" 2> /dev/null || git switch -c "${RELEASE_BRANCH}" + fi - # Search and replace known references to stackableRelease, container images, branch references. - # https://github.com/stackabletech/demos/blob/main/.scripts/update_refs.sh - .scripts/update_refs.sh commit + .scripts/update_refs.sh commit - push_branch "$DEMOS_REPO" + push_branch "$DEMOS_REPO" + ) } update_repos() { - local BASE_DIR="$1"; - if [ "products" == "$WHAT" ] || [ "all" == "$WHAT" ]; then update_products fi if [ "operators" == "$WHAT" ] || [ "all" == "$WHAT" ]; then - update_operators + for_each_operator update_single_operator fi if [ "demos" == "$WHAT" ] || [ "all" == "$WHAT" ]; then update_demos @@ -82,25 +109,16 @@ update_repos() { } push_branch() { - local REPOSITORY="$1"; + local repository="$1" if $PUSH; then - echo "Pushing changes to $REPOSITORY" + echo "Pushing changes to $repository" git push -u "$REMOTE" "$RELEASE_BRANCH" else - echo "Dry-run: not pushing changes to $REPOSITORY" + echo "Dry-run: not pushing changes to $repository" git push --dry-run -u "$REMOTE" "$RELEASE_BRANCH" fi } -cleanup() { - local BASE_DIR="$1"; - - if $CLEANUP; then - echo "Cleaning up..." - rm -rf "$BASE_DIR" - fi -} - parse_inputs() { RELEASE="" PUSH=false @@ -117,42 +135,31 @@ parse_inputs() { esac shift done - #----------------------------------------------------------- - # remove leading and trailing quotes - #----------------------------------------------------------- - RELEASE="${RELEASE%\"}" - RELEASE="${RELEASE#\"}" - RELEASE_BRANCH="release-$RELEASE" + + RELEASE="$(strip_quotes "$RELEASE")" INITIAL_DIR="$PWD" - DOCKER_IMAGES_REPO=$(yq '... comments="" | .images-repo ' "$INITIAL_DIR"/release/config.yaml) - DEMOS_REPO=$(yq '... comments="" | .demos-repo ' "$INITIAL_DIR"/release/config.yaml) - TEMP_RELEASE_FOLDER="/tmp/stackable-$RELEASE_BRANCH" + derive_branch_vars "$RELEASE" echo "Settings: ${RELEASE_BRANCH}: Push: $PUSH: Cleanup: $CLEANUP" } main() { parse_inputs "$@" - #----------------------------------------------------------- - # check if tag argument provided - #----------------------------------------------------------- + if [ -z "${RELEASE}" ]; then echo "Usage: create-release-branch.sh -b [-p] [-c] [-w products|operators|demos|all]" exit 1 fi - #----------------------------------------------------------- - # check if argument matches our tag regex - #----------------------------------------------------------- - if [[ ! $RELEASE =~ $RELEASE_REGEX ]]; then - echo "Provided branch name [$RELEASE] does not match the required regex pattern [$RELEASE_REGEX]" - exit 1 - fi + + validate_what "$WHAT" products operators demos all + validate_release "$RELEASE" echo "Creating temporary working directory if it doesn't exist [$TEMP_RELEASE_FOLDER]" mkdir -p "$TEMP_RELEASE_FOLDER" - update_repos "$TEMP_RELEASE_FOLDER" - cleanup "$TEMP_RELEASE_FOLDER" + check_existing_branches + update_repos + cleanup } main "$@" diff --git a/release/create-release-candidate-branch.sh b/release/create-release-candidate-branch.sh index 5e1a9ae..ef83cf3 100755 --- a/release/create-release-candidate-branch.sh +++ b/release/create-release-candidate-branch.sh @@ -1,61 +1,71 @@ #!/usr/bin/env bash # -# See README.adoc +# See README.md # set -euo pipefail -# set -x - -# tags should be semver-compatible e.g. 23.1.1 not 23.01.1 -# this is needed for cargo commands to work properly -# optional release-candidate suffixes are in the form: -# - rc-1, e.g. 23.1.1-rc1, 23.12.1-rc12 etc. -TAG_REGEX="^[0-9][0-9]\.([1-9]|[1][0-2])\.[0-9]+(-rc[0-9]+)?$" -REMOTE="origin" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + PR_MSG="> [!CAUTION] > ## DO NOT MERGE MANUALLY! > This branch will be merged (and the commit tagged) by stackable-utils once any necessary commits have been cherry-picked to here from the main branch." rc_branch_products() { - # assume that the branch exists and has either been pushed or has been created locally - cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + ( # subshell to isolate cd + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + + git switch "$PR_BRANCH" + update_changelog ./CHANGELOG.md "$RELEASE_TAG" - # the PR branch should already exist - git switch "$PR_BRANCH" - update_product_images_changelogs + verify_release "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" "$RELEASE_TAG" "$RELEASE" - git commit -sam "chore: Release $RELEASE_TAG" - push_branch + git add CHANGELOG.md + git diff --cached --quiet && echo "No changes to commit for products" && return + git commit -sm "chore: Release $RELEASE_TAG" + push_branch + ) } rc_branch_operators() { - while IFS="" read -r operator || [ -n "$operator" ]; do + for_each_operator rc_branch_single_operator +} + +rc_branch_single_operator() { + local operator="$1" + ( # subshell to isolate cd cd "${TEMP_RELEASE_FOLDER}/${operator}" git switch "$PR_BRANCH" - # Update git submodules if needed if [ -f .gitmodules ]; then git submodule update --recursive --init fi - # set tag version where relevant cargo set-version --offline --workspace "$RELEASE_TAG" cargo update --workspace - # Run via nix-shell for the correct dependencies. Makefile already calls - # nix stuff, so it shouldn't be a problem for non-nix users. - nix-shell --run 'make regenerate-charts' + # LIBGIT2_NO_PKG_CONFIG forces libgit2-sys to statically link its bundled + # libgit2, avoiding a runtime crash where nix provides the library for + # compilation but not on LD_LIBRARY_PATH at runtime. + LIBGIT2_NO_PKG_CONFIG=1 nix-shell --run 'make regenerate-charts' nix-shell --run 'make regenerate-nix' update_code "$TEMP_RELEASE_FOLDER/${operator}" - # ensure .j2 changes are resolved "$TEMP_RELEASE_FOLDER/${operator}"/scripts/docs_templating.sh - # inserts a single line with tag and date - update_changelog "$TEMP_RELEASE_FOLDER/${operator}" + update_changelog "$TEMP_RELEASE_FOLDER/${operator}/CHANGELOG.md" "$RELEASE_TAG" - git commit -sam "chore: Release $RELEASE_TAG" + verify_release "$TEMP_RELEASE_FOLDER/${operator}" "$RELEASE_TAG" "$RELEASE" + + git add Cargo.toml Cargo.lock Cargo.nix \ + deploy/helm/ extra/ \ + docs/ tests/ \ + CHANGELOG.md + git diff --cached --quiet && echo "No changes to commit for ${operator}" && return + git commit -sm "chore: Release $RELEASE_TAG" push_branch - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + ) } rc_branch_repos() { @@ -67,101 +77,46 @@ rc_branch_repos() { fi } -check_tag_is_valid() { - git fetch --tags - - # check tags: N.B. look for exact match - if git tag --list | grep -E "^$RELEASE_TAG\$"; then - >&2 echo "Tag $RELEASE_TAG already exists!" - exit 1 - fi - - # Do we want proper semver version checking? - # We should switch this script to python if so. - #EXISTING_TAGS=$(git tag --list | grep -E "$RELEASE" | sort -V) - #for EXISTING_TAG in $EXISTING_TAGS; do - # if [[ "$RELEASE_TAG" < "$EXISTING_TAG" ]]; then - # >&2 echo "Error: Proposed tag $RELEASE_TAG is earlier than existing tag $EXISTING_TAG." - # exit 1 - # fi - #done -} - check_products() { - echo "Checking products" + ( # subshell to isolate cd + echo "Checking products" - if [ ! -d "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" ]; then - echo "Cloning folder: $TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - # $TEMP_RELEASE_FOLDER has already been created in main() - git clone "git@github.com:stackabletech/${DOCKER_IMAGES_REPO}.git" "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - fi - cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - - # Need to update here because if we deleted the local state, or someone else continues - # we might be back on main, or on the release branch without having pulled updates from fixes. - git fetch && git switch "$RELEASE_BRANCH" && git pull - - # switch to the release branch, which should exist as tagging - # is subsequent to creating the branch. - # Note, if this needs to check the branch exists locally, then use: - # "^[ *]*$RELEASE_BRANCH\$" - if ! git branch -a | grep -E "$RELEASE_BRANCH\$"; then - >&2 echo "Expected release branch is missing: $RELEASE_BRANCH" - exit 1 - fi + ensure_clone "$DOCKER_IMAGES_REPO" + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - # the new PR should not exist, otherwise a duplicate commit - # will be prepared - # Note, if this needs to check the branch exists locally, then use: - # "^[ *]*$PR_BRANCH\$" - if git branch -a | grep -E "$PR_BRANCH\$"; then - >&2 echo "PR branch already exists: ${REMOTE}/$PR_BRANCH" - exit 1 - fi + require_release_branch "$DOCKER_IMAGES_REPO" + git switch "$RELEASE_BRANCH" && git pull - # create a new branch for the PR off of this - git switch -c "$PR_BRANCH" "$RELEASE_BRANCH" + if local_or_remote_branch_exists "$PR_BRANCH"; then + echo "PR branch already exists, switching to it (resuming prior run)" + git switch "$PR_BRANCH" + else + git switch -c "$PR_BRANCH" "$RELEASE_BRANCH" + fi - check_tag_is_valid + check_tag_is_valid "$RELEASE_TAG" "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + ) } -check_operators() { - echo "Checking operators" - - while IFS="" read -r operator || [ -n "$operator" ]; do +check_single_operator() { + local operator="$1" + ( # subshell to isolate cd echo "Operator: $operator" - if [ ! -d "$TEMP_RELEASE_FOLDER/${operator}" ]; then - echo "Cloning folder: $TEMP_RELEASE_FOLDER/${operator}" - # $TEMP_RELEASE_FOLDER has already been created in main() - git clone "git@github.com:stackabletech/${operator}.git" "$TEMP_RELEASE_FOLDER/${operator}" - - fi + ensure_clone "$operator" cd "$TEMP_RELEASE_FOLDER/${operator}" - # Need to update here because if we deleted the local state, or someone else continues - # we might be back on main, or on the release branch without having pulled updates from fixes. - git fetch && git switch "$RELEASE_BRANCH" && git pull - # Note, if this needs to check the branch exists locally, then use: - # "^[ *]*$RELEASE_BRANCH\$" - if ! git branch -a | grep -E "$RELEASE_BRANCH\$"; then - >&2 echo "Expected release branch is missing: ${operator}/$RELEASE_BRANCH" - exit 1 - fi + require_release_branch "$operator" + git switch "$RELEASE_BRANCH" && git pull - # the new PR should not exist, otherwise a duplicate commit - # will be prepared - # Note, if this needs to check the branch exists locally, then use: - # "^[ *]*$PR_BRANCH\$" - if git branch -a | grep -E "$PR_BRANCH\$"; then - >&2 echo "PR branch already exists: ${operator}/$PR_BRANCH" - exit 1 + if local_or_remote_branch_exists "$PR_BRANCH"; then + echo "PR branch already exists for ${operator}, switching to it (resuming prior run)" + git switch "$PR_BRANCH" + else + git switch -c "$PR_BRANCH" "$RELEASE_BRANCH" fi - # create a new branch for the PR off of this - git switch -c "$PR_BRANCH" "$RELEASE_BRANCH" - - check_tag_is_valid - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + check_tag_is_valid "$RELEASE_TAG" "$TEMP_RELEASE_FOLDER/${operator}" + ) } checks() { @@ -169,7 +124,7 @@ checks() { check_products fi if [ "operators" == "$WHAT" ] || [ "all" == "$WHAT" ]; then - check_operators + for_each_operator check_single_operator fi } @@ -177,28 +132,16 @@ update_code() { if [ -d "$1/docs" ]; then echo "Updating antora docs for $1" - # antora version should be major.minor, not patch level yq -i ".version = \"${RELEASE}\"" "$1/docs/antora.yml" yq -i '.prerelease = false' "$1/docs/antora.yml" - # Not all operators have a getting started guide - # that's why we verify if templating_vars.yaml exists. if [ -f "$1/docs/templating_vars.yaml" ]; then - - # for an initial tag for a given release... yq -i "(.versions.[] | select(. == \"*dev\")) |= \"${RELEASE_TAG}\"" "$1/docs/templating_vars.yaml" - - # ...consider for patch releases/release candidates too - # We assume that the tag (e.g. 23.7.1) is applied to an earlier tag in the same - # release (e.g. 23.7.0) so search+replace on the major.minor tag will suffice. - # TODO: this may pick up versions of external components as well. yq -i "(.versions.[] | select(. == \"${RELEASE}*\")) |= \"${RELEASE_TAG}\"" "$1/docs/templating_vars.yaml" - yq -i ".helm.repo_name |= sub(\"stackable-dev\", \"stackable-stable\")" "$1/docs/templating_vars.yaml" yq -i ".helm.repo_url |= sub(\"helm-dev\", \"helm-stable\")" "$1/docs/templating_vars.yaml" fi - # Replace "nightly" link so the documentation refers to the current version for file in $(find "$1/docs" -name "*.adoc"); do sed -i "s/nightly@home/home/g" "$file" done @@ -206,23 +149,15 @@ update_code() { echo "No docs found under $1." fi - # Update operator version for the integration tests - # (used when installing the operators). yq -i ".releases.tests.products[].operatorVersion |= sub(\"0.0.0-dev\", \"${RELEASE_TAG}\")" "$1/tests/release.yaml" - - # do this for patch releases/release candidates too. - # i.e. replace 24.11.0-rc1 with 24.11.0, 24.7.0 with 24.7.1 etc. yq -i "(.releases.tests.products[].operatorVersion | select(. == \"${RELEASE}*\")) |= \"${RELEASE_TAG}\"" "$1/tests/release.yaml" - # Some tests perform **label** inspection and for (only) these cases specific labels should be updated. - # N.B. don't do this for all test files as not all images will necessarily exist for the given release tag. find "$1/tests/templates/kuttl" -type f -print0 | xargs -0 sed -E -i "s#(app\.kubernetes\.io/version: \".*-stackable)[^\"]*#\1$RELEASE_TAG#" } push_branch() { if $PUSH; then echo "Pushing changes..." - # the branch must be updated before the PR can be created git push -u "$REMOTE" "$PR_BRANCH" gh pr create --reviewer stackabletech/developers --base "${RELEASE_BRANCH}" --head "${PR_BRANCH}" --title "chore: Release ${RELEASE_TAG}" --body "${PR_MSG}" else @@ -232,23 +167,6 @@ push_branch() { fi } -cleanup() { - if $CLEANUP; then - echo "Cleaning up..." - rm -rf "$TEMP_RELEASE_FOLDER" - fi -} - -update_changelog() { - TODAY=$(date +'%Y-%m-%d') - sed -i "s/^.*unreleased.*/## [Unreleased]\n\n## [$RELEASE_TAG] - $TODAY/I" "$1"/CHANGELOG.md -} - -update_product_images_changelogs() { - TODAY=$(date +'%Y-%m-%d') - sed -i "s/^.*unreleased.*/## [Unreleased]\n\n## [$RELEASE_TAG] - $TODAY/I" ./CHANGELOG.md -} - parse_inputs() { RELEASE_TAG="" PUSH=false @@ -275,75 +193,28 @@ parse_inputs() { shift done - # remove leading and trailing quotes - RELEASE_TAG="${RELEASE_TAG%\"}" - RELEASE_TAG="${RELEASE_TAG#\"}" - - # for a tag of e.g. 23.1.1, the release branch (already created) will be 23.1 - RELEASE="$(cut -d'.' -f1,2 <<< "$RELEASE_TAG")" - RELEASE_BRANCH="release-$RELEASE" - # N.B. this has to match what is used in other scripts - PR_BRANCH="pr-$RELEASE_TAG" + RELEASE_TAG="$(strip_quotes "$RELEASE_TAG")" INITIAL_DIR="$PWD" - DOCKER_IMAGES_REPO=$(yq '... comments="" | .images-repo ' "$INITIAL_DIR"/release/config.yaml) - TEMP_RELEASE_FOLDER="/tmp/stackable-$RELEASE_BRANCH" + derive_tag_vars "$RELEASE_TAG" echo "Settings: ${RELEASE_BRANCH}: Push: $PUSH: Cleanup: $CLEANUP" } -check_dependencies() { - # check for a globally configured git user - if ! git_user=$(git config --global --includes --get user.name) \ - || ! git_email=$(git config --global --includes --get user.email); then - >&2 echo "Error: global git user name/email is not set." - exit 1 - else - echo "global git user: $git_user <$git_email>" - echo "Is this correct? (y/n)" - read -r response - if [[ "$response" == "y" || "$response" == "Y" ]]; then - echo "Proceeding with $git_user <$git_email>" - else - >&2 echo "User not accepted. Exiting." - exit 1 - fi - fi - - # check gh authentication: if this fails you will need to e.g. gh auth login - gh auth status - yq --version - python --version - cargo --version - cargo set-version --version - # check for jinja2-cli including pyyaml package - jinja2 --version - python -m pip show pyyaml -} - main() { parse_inputs "$@" - # check if tag argument provided if [ -z "${RELEASE_TAG}" ]; then >&2 echo "Usage: create-release-candidate-branch.sh -t [-p] [-c] [-w products|operators|all]" exit 1 fi - # check if argument matches our tag regex - if [[ ! $RELEASE_TAG =~ $TAG_REGEX ]]; then - >&2 echo "Provided tag [$RELEASE_TAG] does not match the required tag regex pattern [$TAG_REGEX]" - exit 1 - fi - - if [ ! -d "$TEMP_RELEASE_FOLDER" ]; then - echo "Creating folder for cloning docker images and/or operators: [$TEMP_RELEASE_FOLDER]" - mkdir -p "$TEMP_RELEASE_FOLDER" - fi + validate_what "$WHAT" products operators all + validate_tag "$RELEASE_TAG" "$TAG_REGEX" - check_dependencies + ensure_temp_folder + check_build_dependencies - # sanity checks before we start: folder, branches etc. checks echo "Cloning docker-images and/or operators to [$TEMP_RELEASE_FOLDER]" diff --git a/release/image-checks.sh b/release/image-checks.sh index d5247e0..26cf63d 100755 --- a/release/image-checks.sh +++ b/release/image-checks.sh @@ -81,6 +81,7 @@ check_tags_for_operator secret-operator "$SDP_RELEASE" check_tags_for_operator spark-k8s-operator "$SDP_RELEASE" check_tags_for_operator superset-operator "$SDP_RELEASE" check_tags_for_operator trino-operator "$SDP_RELEASE" +check_tags_for_operator opensearch-operator "$SDP_RELEASE" check_tags_for_operator zookeeper-operator "$SDP_RELEASE" # Be sure to check the product versions for the release you a checking for. diff --git a/release/lib.sh b/release/lib.sh new file mode 100755 index 0000000..52ee3fb --- /dev/null +++ b/release/lib.sh @@ -0,0 +1,358 @@ +#!/usr/bin/env bash +# +# Shared library for release scripts. +# Source this file — do not execute it directly. +# + +set -euo pipefail + +REMOTE="origin" + +# Tag with optional RC suffix (e.g. 26.3.0, 26.3.1-rc1) +TAG_REGEX="^[0-9][0-9]\.([1-9]|[1][0-2])\.[0-9]+(-rc[0-9]+)?$" + +# Tag without RC suffix — used by post-release which only applies to final releases +TAG_REGEX_FINAL="^[0-9][0-9]\.([1-9]|[1][0-2])\.[0-9]+$" + +# Release branch format (e.g. 26.3) +RELEASE_REGEX="^[0-9][0-9]\.([1-9]|[1][0-2])$" + +# Validate the -w parameter. Pass the valid options as arguments. +# Usage: validate_what "$WHAT" products operators all +# or: validate_what "$WHAT" products operators demos all +validate_what() { + local value="$1" + shift + local valid=("$@") + + if [ -z "$value" ]; then + >&2 echo "Error: -w is required ($(IFS='|'; echo "${valid[*]}"))" + exit 1 + fi + + for v in "${valid[@]}"; do + if [ "$value" == "$v" ]; then + return 0 + fi + done + + >&2 echo "Error: invalid -w value '$value' (expected: $(IFS='|'; echo "${valid[*]}"))" + exit 1 +} + +validate_tag() { + local tag="$1" + local regex="$2" + + if [ -z "$tag" ]; then + >&2 echo "Error: tag is required" + exit 1 + fi + + if [[ ! $tag =~ $regex ]]; then + >&2 echo "Provided tag [$tag] does not match the required regex pattern [$regex]" + exit 1 + fi +} + +validate_release() { + local release="$1" + + if [ -z "$release" ]; then + >&2 echo "Error: release branch name is required" + exit 1 + fi + + if [[ ! $release =~ $RELEASE_REGEX ]]; then + >&2 echo "Provided branch name [$release] does not match the required regex pattern [$RELEASE_REGEX]" + exit 1 + fi +} + +# Derive common variables from a release tag. +# Sets: RELEASE, RELEASE_BRANCH, PR_BRANCH, DOCKER_IMAGES_REPO, TEMP_RELEASE_FOLDER +derive_tag_vars() { + local tag="$1" + + RELEASE="$(cut -d'.' -f1,2 <<< "$tag")" + RELEASE_BRANCH="release-$RELEASE" + PR_BRANCH="pr-$tag" + DOCKER_IMAGES_REPO=$(yq '... comments="" | .images-repo ' "$INITIAL_DIR"/release/config.yaml) + TEMP_RELEASE_FOLDER="/tmp/stackable-$RELEASE_BRANCH" +} + +# Derive common variables from a release branch name (major.minor only). +# Sets: RELEASE_BRANCH, DOCKER_IMAGES_REPO, DEMOS_REPO, TEMP_RELEASE_FOLDER +derive_branch_vars() { + local release="$1" + + RELEASE_BRANCH="release-$release" + DOCKER_IMAGES_REPO=$(yq '... comments="" | .images-repo ' "$INITIAL_DIR"/release/config.yaml) + DEMOS_REPO=$(yq '... comments="" | .demos-repo ' "$INITIAL_DIR"/release/config.yaml) + TEMP_RELEASE_FOLDER="/tmp/stackable-$RELEASE_BRANCH" +} + +# Check git user and gh auth. Prompts for confirmation. +check_git_user() { + if ! git_user=$(git config --global --includes --get user.name) \ + || ! git_email=$(git config --global --includes --get user.email); then + >&2 echo "Error: global git user name/email is not set." + exit 1 + else + echo "global git user: $git_user <$git_email>" + echo "Is this correct? (y/n)" + read -r response + if [[ "$response" == "y" || "$response" == "Y" ]]; then + echo "Proceeding with $git_user <$git_email>" + else + >&2 echo "User not accepted. Exiting." + exit 1 + fi + fi +} + +check_gh_auth() { + gh auth status +} + +# Full dependency check for scripts that do code modifications. +check_build_dependencies() { + check_git_user + check_gh_auth + yq --version + python --version + cargo --version + cargo set-version --version + jinja2 --version + python -m pip show pyyaml +} + +# Lightweight dependency check for scripts that only do git/gh operations. +check_basic_dependencies() { + check_git_user + check_gh_auth +} + +# Check if a branch exists on the remote. +# Must be called with cwd inside the target git repo. +# Uses ls-remote to avoid modifying local refs (#19). +remote_branch_exists() { + local branch="$1" + git ls-remote --exit-code --heads "$REMOTE" "refs/heads/${branch}" > /dev/null 2>&1 +} + +# Check if a branch exists locally or in remote tracking refs. +local_or_remote_branch_exists() { + local branch="$1" + git branch -a | grep -qE "(^[* ] |remotes/${REMOTE}/)${branch}$" +} + +# Require that a release branch exists on the remote. +# Exits with an error if missing. +require_release_branch() { + local label="$1" + if ! remote_branch_exists "$RELEASE_BRANCH"; then + >&2 echo "Expected release branch is missing: ${label}/${RELEASE_BRANCH}" + exit 1 + fi +} + +# Check the working tree is clean. Exits if dirty. +require_clean_worktree() { + local label="$1" + if ! git diff-index --quiet HEAD --; then + >&2 echo "Dirty git index for $label. Check working tree or staged changes. Exiting." + exit 2 + fi +} + +# Check that a tag does not already exist on the remote. +# Uses ls-remote to avoid modifying local refs (#19). +check_tag_is_valid() { + local tag="$1" + local repo_dir="$2" + + cd "$repo_dir" + + if git ls-remote --tags "$REMOTE" "refs/tags/${tag}" | grep -q "refs/tags/${tag}"; then + >&2 echo "Tag $tag already exists on remote!" + exit 1 + fi +} + +# Ensure the temp release folder exists. +ensure_temp_folder() { + if [ ! -d "$TEMP_RELEASE_FOLDER" ]; then + echo "Creating folder: [$TEMP_RELEASE_FOLDER]" + mkdir -p "$TEMP_RELEASE_FOLDER" + fi +} + +# Clone a repo if not already present in the temp folder. +ensure_clone() { + local repo="$1" + + if [ ! -d "$TEMP_RELEASE_FOLDER/$repo" ]; then + echo "Cloning: $TEMP_RELEASE_FOLDER/$repo" + git clone "git@github.com:stackabletech/${repo}.git" "$TEMP_RELEASE_FOLDER/$repo" + fi +} + +# Clean up the temp folder if CLEANUP is true. +cleanup() { + if "${CLEANUP:-false}"; then + echo "Cleaning up..." + rm -rf "$TEMP_RELEASE_FOLDER" + fi +} + +# Iterate over operators from config.yaml, calling a function for each. +# Usage: for_each_operator my_function +for_each_operator() { + local func="$1" + shift + + while IFS="" read -r operator || [ -n "$operator" ]; do + "$func" "$operator" "$@" + done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) +} + +# Idempotent changelog update — skips if the tag is already present. +update_changelog() { + local changelog="$1" + local tag="$2" + + if grep -q "## \[$tag\]" "$changelog"; then + echo "Changelog already contains $tag, skipping" + return + fi + + local today + today=$(date +'%Y-%m-%d') + sed -i "s/^.*unreleased.*/## [Unreleased]\n\n## [$tag] - $today/I" "$changelog" +} + +# Verify release transformations are correct before committing. +# Checks whichever files exist — safe to call for both operators and products. +verify_release() { + local dir="$1" + local tag="$2" + local release="$3" + local errors=0 + + echo "Verifying release transformations in $(basename "$dir")..." + + # Cargo.toml workspace version + if [ -f "$dir/Cargo.toml" ] && grep -q '^\[workspace\.package\]' "$dir/Cargo.toml"; then + local cargo_ver + cargo_ver=$(grep -A 20 '^\[workspace\.package\]' "$dir/Cargo.toml" | grep -m1 '^version' | grep -oP '"\K[^"]+' || true) + if [ -n "$cargo_ver" ] && [ "$cargo_ver" != "$tag" ]; then + >&2 echo " FAIL: Cargo.toml workspace version is '$cargo_ver', expected '$tag'" + errors=$((errors + 1)) + fi + fi + + # Helm Chart.yaml version and appVersion + local chart_yaml + chart_yaml=$(find "$dir/deploy/helm" -maxdepth 2 -name "Chart.yaml" -print -quit 2>/dev/null || true) + if [ -n "$chart_yaml" ]; then + local chart_ver chart_app_ver + chart_ver=$(yq '.version' "$chart_yaml") + chart_app_ver=$(yq '.appVersion' "$chart_yaml") + if [ "$chart_ver" != "$tag" ]; then + >&2 echo " FAIL: Chart.yaml version is '$chart_ver', expected '$tag'" + errors=$((errors + 1)) + fi + if [ "$chart_app_ver" != "$tag" ]; then + >&2 echo " FAIL: Chart.yaml appVersion is '$chart_app_ver', expected '$tag'" + errors=$((errors + 1)) + fi + fi + + # antora.yml: version should be major.minor, prerelease should be false + if [ -f "$dir/docs/antora.yml" ]; then + local antora_ver antora_prerelease + antora_ver=$(yq '.version' "$dir/docs/antora.yml") + antora_prerelease=$(yq '.prerelease' "$dir/docs/antora.yml") + if [ "$antora_ver" != "$release" ]; then + >&2 echo " FAIL: antora.yml version is '$antora_ver', expected '$release'" + errors=$((errors + 1)) + fi + if [ "$antora_prerelease" != "false" ]; then + >&2 echo " FAIL: antora.yml prerelease is '$antora_prerelease', expected 'false'" + errors=$((errors + 1)) + fi + fi + + # templating_vars.yaml: no *dev* versions remaining + if [ -f "$dir/docs/templating_vars.yaml" ]; then + local dev_entries + dev_entries=$(yq '.versions | to_entries[] | select(.value | test("dev")) | .key' "$dir/docs/templating_vars.yaml" 2>/dev/null || true) + if [ -n "$dev_entries" ]; then + >&2 echo " FAIL: templating_vars.yaml still has dev versions:" + >&2 echo "$dev_entries" | sed 's/^/ /' + errors=$((errors + 1)) + fi + fi + + # tests/release.yaml: all operatorVersion entries should match the tag + if [ -f "$dir/tests/release.yaml" ]; then + local bad_versions + bad_versions=$(yq '.releases.tests.products[] | select(.operatorVersion != "'"$tag"'") | .operatorVersion' "$dir/tests/release.yaml" 2>/dev/null || true) + if [ -n "$bad_versions" ]; then + >&2 echo " FAIL: tests/release.yaml has non-release operatorVersions:" + >&2 echo "$bad_versions" | sort -u | sed 's/^/ /' + errors=$((errors + 1)) + fi + fi + + # CHANGELOG.md should contain the release tag + if [ -f "$dir/CHANGELOG.md" ]; then + if ! grep -q "## \[$tag\]" "$dir/CHANGELOG.md"; then + >&2 echo " FAIL: CHANGELOG.md does not contain '## [$tag]'" + errors=$((errors + 1)) + fi + fi + + # No nightly@ references remaining in .adoc files + if [ -d "$dir/docs" ]; then + local nightly_refs + nightly_refs=$(grep -rl 'nightly@' "$dir/docs/" 2>/dev/null || true) + if [ -n "$nightly_refs" ]; then + >&2 echo " FAIL: docs still contain 'nightly@' references:" + >&2 echo "$nightly_refs" | sed 's/^/ /' + errors=$((errors + 1)) + fi + fi + + # Workflow trigger: release tag should match at least one on.push.tags pattern + if [ -f "$dir/.github/workflows/build.yaml" ]; then + local tag_matched=false + while IFS= read -r pattern; do + local regex + regex=$(echo "$pattern" | sed 's/\./\\./g; s/\*/.*/g') + if [[ $tag =~ ^${regex}$ ]]; then + tag_matched=true + break + fi + done < <(yq '.on.push.tags[]' "$dir/.github/workflows/build.yaml" 2>/dev/null) + if ! $tag_matched; then + >&2 echo " FAIL: Tag '$tag' does not match any on.push.tags pattern in build.yaml" + errors=$((errors + 1)) + fi + fi + + if [ "$errors" -gt 0 ]; then + >&2 echo "Verification failed with $errors error(s)" + return 1 + fi + echo "Verification passed" +} + +# Strip leading and trailing double quotes from a variable value. +strip_quotes() { + local val="$1" + val="${val%\"}" + val="${val#\"}" + echo "$val" +} diff --git a/release/merge-release-candidate.sh b/release/merge-release-candidate.sh index 213acad..13ed4cc 100755 --- a/release/merge-release-candidate.sh +++ b/release/merge-release-candidate.sh @@ -3,12 +3,14 @@ # See README.md # set -euo pipefail -# set -x + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" parse_inputs() { RELEASE_TAG="" PUSH=false - PR_BRANCH="" WHAT="" while [[ "$#" -gt 0 ]]; do @@ -30,47 +32,43 @@ parse_inputs() { shift done - # remove leading and trailing quotes - RELEASE_TAG="${RELEASE_TAG%\"}" - RELEASE_TAG="${RELEASE_TAG#\"}" - # N.B. this has to match what is used in other scripts - PR_BRANCH="pr-$RELEASE_TAG" + RELEASE_TAG="$(strip_quotes "$RELEASE_TAG")" INITIAL_DIR="$PWD" - DOCKER_IMAGES_REPO=$(yq '... comments="" | .images-repo ' "$INITIAL_DIR"/release/config.yaml) + derive_tag_vars "$RELEASE_TAG" echo "Settings: ${PR_BRANCH}: Push: $PUSH:" } -merge_operators() { - read -p "Ask someone to approve all of the operator PRs, then press Enter" - while IFS="" read -r operator || [ -n "$operator" ]; do - echo "Operator: $operator" +merge_single_operator() { + local operator="$1" + + echo "Operator: $operator" + if $PUSH; then + STATE=$(gh pr view "${PR_BRANCH}" -R stackabletech/"${operator}" --jq '.state' --json state) + else + echo "Dry-run: pretending the PR exists and is open" + STATE="OPEN" + fi + if [[ "$STATE" == "OPEN" ]]; then + echo "Processing ${operator} in branch ${PR_BRANCH} with state ${STATE}" if $PUSH; then - STATE=$(gh pr view "${PR_BRANCH}" -R stackabletech/"${operator}" --jq '.state' --json state) - else - # It is possible to dry-run with the PR existing, but we will simply use OPEN - echo "Dry-run: pretending the PR exists and is open" - STATE="OPEN" - fi - if [[ "$STATE" == "OPEN" ]]; then - echo "Processing ${operator} in branch ${PR_BRANCH} with state ${STATE}" - if $PUSH; then - echo "Reviewing..." - # TODO (@NickLarsenNZ): Check if the review is merged, else loop the following - # TODO (@NickLarsenNZ): Allow review if the PR author is not the current `gh` user, otherwise wait. - # gh pr review "${PR_BRANCH}" --approve -R stackabletech/"${operator}" - echo "Merging..." - gh pr merge "${PR_BRANCH}" --delete-branch --squash -R stackabletech/"${operator}" - else - echo "Dry-run: not reviewing/merging..." - echo - echo "Please checkout the release branch, and manually run git merge ${PR_BRANCH}" - fi + echo "Reviewing..." + echo "Merging..." + gh pr merge "${PR_BRANCH}" --delete-branch --squash -R stackabletech/"${operator}" else - echo "Skipping ${operator}, PR already closed" + echo "Dry-run: not reviewing/merging..." + echo + echo "Please checkout the release branch, and manually run git merge ${PR_BRANCH}" fi - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + else + echo "Skipping ${operator}, PR already closed" + fi +} + +merge_operators() { + read -p "Ask someone to approve all of the operator PRs, then press Enter" + for_each_operator merge_single_operator } merge_products() { @@ -78,7 +76,6 @@ merge_products() { if $PUSH; then STATE=$(gh pr view "${PR_BRANCH}" -R stackabletech/"${DOCKER_IMAGES_REPO}" --jq '.state' --json state) else - # It is possible to dry-run with the PR existing, but we will simply use OPEN echo "Dry-run: pretending the PR exists and is open" STATE="OPEN" fi @@ -86,10 +83,7 @@ merge_products() { echo "Processing ${DOCKER_IMAGES_REPO} in branch ${PR_BRANCH} with state ${STATE}" if $PUSH; then echo "Reviewing..." - # TODO (@NickLarsenNZ): Check if the review is merged, else loop the following - # TODO (@NickLarsenNZ): Allow review if the PR author is not the current `gh` user, otherwise wait. read -p "Ask someone to approve the PR, then press Enter" - # gh pr review "${PR_BRANCH}" --approve -R stackabletech/"${DOCKER_IMAGES_REPO}" echo "Merging..." gh pr merge "${PR_BRANCH}" --delete-branch --squash -R stackabletech/"${DOCKER_IMAGES_REPO}" else @@ -111,37 +105,18 @@ merge() { fi } -check_dependencies() { - # check for a globally configured git user - if ! git_user=$(git config --global --includes --get user.name) \ - || ! git_email=$(git config --global --includes --get user.email); then - >&2 echo "Error: global git user name/email is not set." - exit 1 - else - echo "global git user: $git_user <$git_email>" - echo "Is this correct? (y/n)" - read -r response - if [[ "$response" == "y" || "$response" == "Y" ]]; then - echo "Proceeding with $git_user <$git_email>" - else - >&2 echo "User not accepted. Exiting." - exit 1 - fi - fi - # check gh authentication: if this fails you will need to e.g. gh auth login - gh auth status -} - main() { parse_inputs "$@" - # check if tag argument provided if [ -z "${RELEASE_TAG}" ]; then - >&2 echo "Usage: create-release-merge-and-tag.sh -t [-w products|operators|all]" + >&2 echo "Usage: merge-release-candidate.sh -t [-p] [-w products|operators|all]" exit 1 fi - check_dependencies + validate_what "$WHAT" products operators all + validate_tag "$RELEASE_TAG" "$TAG_REGEX" + + check_basic_dependencies merge } diff --git a/release/post-release.sh b/release/post-release.sh index 6f7f07f..c743a48 100755 --- a/release/post-release.sh +++ b/release/post-release.sh @@ -1,19 +1,17 @@ #!/usr/bin/env bash # -# See README.adoc +# See README.md # set -euo pipefail -# set -x - -#----------------------------------------------------------- -# tags should be semver-compatible e.g. 23.1.1 not 23.01.1 -# this is needed for cargo commands to work properly -#----------------------------------------------------------- -TAG_REGEX="^[0-9][0-9]\.([1-9]|[1][0-2])\.[0-9]+$" -REMOTE="origin" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" + PR_MSG="> [!CAUTION] > ## DO NOT MERGE WITHOUT MANUAL CHECKING! > This PR contains information about commits have been cherry-picked to the release branch from the main branch, and may not reflect the correct chronology. Please check!" + parse_inputs() { RELEASE_TAG="" PUSH=false @@ -28,199 +26,140 @@ parse_inputs() { esac shift done - #----------------------------------------------------------- - # remove leading and trailing quotes - #----------------------------------------------------------- - RELEASE_TAG="${RELEASE_TAG%\"}" - RELEASE_TAG="${RELEASE_TAG#\"}" - #---------------------------------------------------------------------------------------------------- - # for a tag of e.g. 23.1.1, the release branch (already created) will be 23.1 - #---------------------------------------------------------------------------------------------------- - RELEASE="$(cut -d'.' -f1,2 <<< "$RELEASE_TAG")" - RELEASE_BRANCH="release-$RELEASE" + + RELEASE_TAG="$(strip_quotes "$RELEASE_TAG")" INITIAL_DIR="$PWD" - DOCKER_IMAGES_REPO=$(yq '... comments="" | .images-repo ' "$INITIAL_DIR"/release/config.yaml) - TEMP_RELEASE_FOLDER="/tmp/stackable-$RELEASE_BRANCH" + derive_tag_vars "$RELEASE_TAG" echo "Settings: $RELEASE_BRANCH: Push: $PUSH" } -# Check that the operator repos have been cloned locally, and that the release -# branch and tag exists. -check_operators() { - while IFS="" read -r OPERATOR || [ -n "$OPERATOR" ] - do - echo "Operator: $OPERATOR" - if [ ! -d "$TEMP_RELEASE_FOLDER/$OPERATOR" ]; then - echo "Cloning folder: $TEMP_RELEASE_FOLDER/$OPERATOR" - # $TEMP_RELEASE_FOLDER has already been created in main() - git clone "git@github.com:stackabletech/${OPERATOR}.git" "$TEMP_RELEASE_FOLDER/$OPERATOR" - fi - cd "$TEMP_RELEASE_FOLDER/$OPERATOR" +check_single_operator() { + local operator="$1" + ( # subshell to isolate cd + echo "Operator: $operator" + ensure_clone "$operator" + cd "$TEMP_RELEASE_FOLDER/$operator" - if ! git diff-index --quiet HEAD --; then - >&2 echo "Dirty git index for $OPERATOR. Check working tree or staged changes. Exiting." - exit 2 - fi + require_clean_worktree "$operator" + require_release_branch "$operator" - # Note, if this needs to check the branch exists locally, then use: - # "^[ *]*$RELEASE_BRANCH\$" - if ! git branch -a | grep "$RELEASE_BRANCH\$"; then - >&2 echo "Expected release branch is missing: $OPERATOR/$RELEASE_BRANCH" + if ! git ls-remote --tags "$REMOTE" "refs/tags/${RELEASE_TAG}" | grep -q "refs/tags/${RELEASE_TAG}"; then + >&2 echo "Expected tag $RELEASE_TAG missing for operator $operator" exit 1 fi - git fetch --tags - if ! git tag | grep "^$RELEASE_TAG\$"; then - >&2 echo "Expected tag $RELEASE_TAG missing for operator $OPERATOR" - exit 1 - fi - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + ) } -# Update the operator changelogs on main, and check they do not differ from -# the changelog in the release branch. -update_operators() { - while IFS="" read -r OPERATOR || [ -n "$OPERATOR" ] - do - cd "$TEMP_RELEASE_FOLDER/$OPERATOR" +update_single_operator() { + local operator="$1" + ( # subshell to isolate cd + cd "$TEMP_RELEASE_FOLDER/$operator" git checkout main git pull - # New branch that updates the CHANGELOG - CHANGELOG_BRANCH="chore/update-changelog-from-release-$RELEASE_TAG" - # Branch out from main - git switch -c "$CHANGELOG_BRANCH" - # Checkout CHANGELOG changes from the release tag + local changelog_branch="chore/update-changelog-from-release-$RELEASE_TAG" + git switch -c "$changelog_branch" git checkout "$RELEASE_TAG" -- CHANGELOG.md - # Ensure only the CHANGELOG has been modified and there - # are no conflicts. - CHANGELOG_MODIFIED=$(git status --short) - if [ "M CHANGELOG.md" != "$CHANGELOG_MODIFIED" ]; then - echo "Failed to update CHANGELOG.md in main for operator $OPERATOR" + + local changelog_modified + changelog_modified=$(git status --short) + if [ "M CHANGELOG.md" != "$changelog_modified" ]; then + echo "Failed to update CHANGELOG.md in main for operator $operator" exit 1 fi - # Commit the updated CHANGELOG. + git add CHANGELOG.md git commit -sm "Update CHANGELOG.md from release $RELEASE_TAG" - # Maybe push and create pull request + if "$PUSH"; then - git push -u "${REMOTE}" "${CHANGELOG_BRANCH}" - gh pr create --reviewer stackabletech/developers --base main --head "${CHANGELOG_BRANCH}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" + git push -u "${REMOTE}" "${changelog_branch}" + gh pr create --reviewer stackabletech/developers --base main --head "${changelog_branch}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" else echo "Dry-run: not pushing..." - git push --dry-run "${REMOTE}" "${CHANGELOG_BRANCH}" - gh pr create --reviewer stackabletech/developers --dry-run --base main --head "${CHANGELOG_BRANCH}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" + git push --dry-run "${REMOTE}" "${changelog_branch}" + gh pr create --reviewer stackabletech/developers --dry-run --base main --head "${changelog_branch}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" fi - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + ) } -# Check that the docker-images repo has been cloned locally, and that the release -# branch and tag exists. check_products() { - if [ ! -d "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" ]; then - echo "Cloning folder: $TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - # $TEMP_RELEASE_FOLDER has already been created in main() - git clone "git@github.com:stackabletech/${DOCKER_IMAGES_REPO}.git" "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - fi - cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + ( # subshell to isolate cd + ensure_clone "$DOCKER_IMAGES_REPO" + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - if ! git diff-index --quiet HEAD --; then - >&2 echo "Dirty git index for $DOCKER_IMAGES_REPO. Check working tree or staged changes. Exiting." - exit 2 - fi - - # Note, if this needs to check the branch exists locally, then use: - # "^[ *]*$RELEASE_BRANCH\$" - if ! git branch -a | grep "$RELEASE_BRANCH\$"; then - >&2 echo "Expected release branch is missing: $DOCKER_IMAGES_REPO/$RELEASE_BRANCH" - exit 1 - fi + require_clean_worktree "$DOCKER_IMAGES_REPO" + require_release_branch "$DOCKER_IMAGES_REPO" - git fetch --tags - # check tags: N.B. look for exact match - if ! git tag | grep "^$RELEASE_TAG\$"; then - >&2 echo "Expected tag $RELEASE_TAG missing for $DOCKER_IMAGES_REPO" - exit 1 - fi + if ! git ls-remote --tags "$REMOTE" "refs/tags/${RELEASE_TAG}" | grep -q "refs/tags/${RELEASE_TAG}"; then + >&2 echo "Expected tag $RELEASE_TAG missing for $DOCKER_IMAGES_REPO" + exit 1 + fi + ) } -# Update the docker-images changelogs on main, and check they do not differ from -# the changelog in the release branch. update_products() { - cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - - git checkout main - git pull - - # New branch that updates the CHANGELOG - CHANGELOG_BRANCH="chore/update-changelog-from-release-$RELEASE_TAG" - # Branch out from main - git switch -c "$CHANGELOG_BRANCH" - # Checkout CHANGELOG changes from the release tag - git checkout "$RELEASE_TAG" -- CHANGELOG.md - # Ensure only the CHANGELOG has been modified and there - # are no conflicts. - CHANGELOG_MODIFIED=$(git status --short) - if [ "M CHANGELOG.md" != "$CHANGELOG_MODIFIED" ]; then - echo "Failed to update CHANGELOG.md in main for $DOCKER_IMAGES_REPO" - exit 1 - fi - # Commit the updated CHANGELOG. - git add CHANGELOG.md - git commit -sm "Update CHANGELOG.md from release $RELEASE_TAG" - # Maybe push and create pull request - if "$PUSH"; then - git push -u "${REMOTE}" "${CHANGELOG_BRANCH}" - gh pr create --reviewer stackabletech/developers --base main --head "${CHANGELOG_BRANCH}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" - else - echo "Dry-run: not pushing..." - git push --dry-run "${REMOTE}" "${CHANGELOG_BRANCH}" - gh pr create --reviewer stackabletech/developers --dry-run --base main --head "${CHANGELOG_BRANCH}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" - fi -} + ( # subshell to isolate cd + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + git checkout main + git pull + + local changelog_branch="chore/update-changelog-from-release-$RELEASE_TAG" + git switch -c "$changelog_branch" + git checkout "$RELEASE_TAG" -- CHANGELOG.md + + local changelog_modified + changelog_modified=$(git status --short) + if [ "M CHANGELOG.md" != "$changelog_modified" ]; then + echo "Failed to update CHANGELOG.md in main for $DOCKER_IMAGES_REPO" + exit 1 + fi + + git add CHANGELOG.md + git commit -sm "Update CHANGELOG.md from release $RELEASE_TAG" + + if "$PUSH"; then + git push -u "${REMOTE}" "${changelog_branch}" + gh pr create --reviewer stackabletech/developers --base main --head "${changelog_branch}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" + else + echo "Dry-run: not pushing..." + git push --dry-run "${REMOTE}" "${changelog_branch}" + gh pr create --reviewer stackabletech/developers --dry-run --base main --head "${changelog_branch}" --title "chore: Update changelog from release ${RELEASE_TAG}" --body "${PR_MSG}" + fi + ) +} main() { parse_inputs "$@" - #----------------------------------------------------------- - # check if tag argument provided - #----------------------------------------------------------- + if [ -z "${RELEASE_TAG}" ]; then echo "Usage: post-release.sh [options]" echo "-t " echo "-p Push changes. Default: false" exit 1 fi - #----------------------------------------------------------- - # check if argument matches our tag regex - #----------------------------------------------------------- - if [[ ! $RELEASE_TAG =~ $TAG_REGEX ]]; then - echo "Provided tag [$RELEASE_TAG] does not match the required tag regex pattern [$TAG_REGEX]" - exit 1 - fi - if [ ! -d "$TEMP_RELEASE_FOLDER" ]; then - echo "Creating folder for cloning docker images and operators: [$TEMP_RELEASE_FOLDER]" - mkdir -p "$TEMP_RELEASE_FOLDER" + # WHAT defaults to "all" so empty is valid here + if [ -n "$WHAT" ]; then + validate_what "$WHAT" products operators all fi + validate_tag "$RELEASE_TAG" "$TAG_REGEX_FINAL" + + ensure_temp_folder if [ "products" == "$WHAT" ] || [ "all" == "$WHAT" ]; then - # sanity checks before we start: folder, branches etc. check_products - echo "Update $DOCKER_IMAGES_REPO main changelog for release $RELEASE_TAG" update_products fi if [ "operators" == "$WHAT" ] || [ "all" == "$WHAT" ]; then - # sanity checks before we start: folder, branches etc. - check_operators - + for_each_operator check_single_operator echo "Update the operator main changelog for release $RELEASE_TAG" - update_operators + for_each_operator update_single_operator fi - } main "$@" diff --git a/release/tag-release-candidate.sh b/release/tag-release-candidate.sh index 2952f6d..fad12a5 100755 --- a/release/tag-release-candidate.sh +++ b/release/tag-release-candidate.sh @@ -1,46 +1,41 @@ #!/usr/bin/env bash # -# See README.adoc +# See README.md # set -euo pipefail -# set -x -# tags should be semver-compatible e.g. 23.1.1 not 23.01.1 -# this is needed for cargo commands to work properly -# optional release-candidate suffixes are in the form: -# - rc-1, e.g. 23.1.1-rc1, 23.12.1-rc12 etc. -TAG_REGEX="^[0-9][0-9]\.([1-9]|[1][0-2])\.[0-9]+(-rc[0-9]+)?$" -REPOSITORY="origin" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib.sh +source "$SCRIPT_DIR/lib.sh" tag_products() { - # assume that the branch exists and has either been pushed or has been created locally - cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + ( # subshell to isolate cd + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - # the PR branch should already exist - git switch "$RELEASE_BRANCH" - if $PUSH; then - git pull - else - git pull || echo "Dry-run: remote branch doesn't exist yet..." - # NOTE (@NickLarsenNZ): We could add a fake commit, but that would poison the current state. - fi - git tag -sm "release $RELEASE_TAG" "$RELEASE_TAG" - push_branch + git switch "$RELEASE_BRANCH" + if $PUSH; then + git pull + else + git pull || echo "Dry-run: remote branch doesn't exist yet..." + fi + git tag -sm "release $RELEASE_TAG" "$RELEASE_TAG" + push_tag + ) } -tag_operators() { - while IFS="" read -r operator || [ -n "$operator" ]; do +tag_single_operator() { + local operator="$1" + ( # subshell to isolate cd cd "${TEMP_RELEASE_FOLDER}/${operator}" git switch "$RELEASE_BRANCH" if $PUSH; then git pull else git pull || echo "Dry-run: remote branch doesn't exist yet..." - # NOTE (@NickLarsenNZ): We could add a fake commit, but that would poison the current state. fi git tag -sm "release $RELEASE_TAG" "$RELEASE_TAG" - push_branch - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + push_tag + ) } tag_repos() { @@ -48,58 +43,30 @@ tag_repos() { tag_products fi if [ "operators" == "$WHAT" ] || [ "all" == "$WHAT" ]; then - tag_operators - fi -} - -check_tag_is_valid() { - git fetch --tags - - # check tags: N.B. look for exact match - if git tag --list | grep -E "^$RELEASE_TAG\$"; then - >&2 echo "Tag $RELEASE_TAG already exists!" - exit 1 + for_each_operator tag_single_operator fi } check_products() { - if [ ! -d "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" ]; then - echo "Cloning folder: $TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - # $TEMP_RELEASE_FOLDER has already been created in main() - git clone "git@github.com:stackabletech/${DOCKER_IMAGES_REPO}.git" "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - fi - cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + ( # subshell to isolate cd + ensure_clone "$DOCKER_IMAGES_REPO" + cd "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" - # switch to the release branch, which should exist as tagging - # is subsequent to creating the branch. - BRANCH_EXISTS=$(git branch -a | grep -E "$RELEASE_BRANCH$") - - if [ -z "${BRANCH_EXISTS}" ]; then - >&2 echo "Expected release branch is missing: $RELEASE_BRANCH" - exit 1 - fi - - check_tag_is_valid + require_release_branch "$DOCKER_IMAGES_REPO" + check_tag_is_valid "$RELEASE_TAG" "$TEMP_RELEASE_FOLDER/$DOCKER_IMAGES_REPO" + ) } -check_operators() { - while IFS="" read -r operator || [ -n "$operator" ]; do +check_single_operator() { + local operator="$1" + ( # subshell to isolate cd echo "Operator: $operator" - if [ ! -d "$TEMP_RELEASE_FOLDER/${operator}" ]; then - echo "Cloning folder: $TEMP_RELEASE_FOLDER/${operator}" - # $TEMP_RELEASE_FOLDER has already been created in main() - git clone "git@github.com:stackabletech/${operator}.git" "$TEMP_RELEASE_FOLDER/${operator}" - - fi + ensure_clone "$operator" cd "$TEMP_RELEASE_FOLDER/${operator}" - # Note, if this needs to check the branch exists locally, then use: - # "^[ *]*$RELEASE_BRANCH\$" - if ! git branch -a | grep -E "$RELEASE_BRANCH\$"; then - >&2 echo "Expected release branch is missing: ${operator}/$RELEASE_BRANCH" - exit 1 - fi - check_tag_is_valid - done < <(yq '... comments="" | .operators[] ' "$INITIAL_DIR"/release/config.yaml) + + require_release_branch "$operator" + check_tag_is_valid "$RELEASE_TAG" "$TEMP_RELEASE_FOLDER/${operator}" + ) } checks() { @@ -107,24 +74,17 @@ checks() { check_products fi if [ "operators" == "$WHAT" ] || [ "all" == "$WHAT" ]; then - check_operators + for_each_operator check_single_operator fi } -push_branch() { +push_tag() { if $PUSH; then echo "Pushing changes..." - git push "${REPOSITORY}" "${RELEASE_TAG}" + git push "$REMOTE" "${RELEASE_TAG}" else echo "Dry-run: not pushing..." - git push --dry-run "${REPOSITORY}" "${RELEASE_TAG}" - fi -} - -cleanup() { - if $CLEANUP; then - echo "Cleaning up..." - rm -rf "$TEMP_RELEASE_FOLDER" + git push --dry-run "$REMOTE" "${RELEASE_TAG}" fi } @@ -154,72 +114,28 @@ parse_inputs() { shift done - # remove leading and trailing quotes - RELEASE_TAG="${RELEASE_TAG%\"}" - RELEASE_TAG="${RELEASE_TAG#\"}" + RELEASE_TAG="$(strip_quotes "$RELEASE_TAG")" - # for a tag of e.g. 23.1.1, the release branch (already created) will be 23.1 - RELEASE="$(cut -d'.' -f1,2 <<< "$RELEASE_TAG")" - RELEASE_BRANCH="release-$RELEASE" INITIAL_DIR="$PWD" - DOCKER_IMAGES_REPO=$(yq '... comments="" | .images-repo ' "$INITIAL_DIR"/release/config.yaml) - TEMP_RELEASE_FOLDER="/tmp/stackable-$RELEASE_BRANCH" + derive_tag_vars "$RELEASE_TAG" echo "Settings: ${RELEASE_BRANCH}: Push: $PUSH: Cleanup: $CLEANUP" } -check_dependencies() { - # check for a globally configured git user - if ! git_user=$(git config --global --includes --get user.name) \ - || ! git_email=$(git config --global --includes --get user.email); then - >&2 echo "Error: global git user name/email is not set." - exit 1 - else - echo "global git user: $git_user <$git_email>" - echo "Is this correct? (y/n)" - read -r response - if [[ "$response" == "y" || "$response" == "Y" ]]; then - echo "Proceeding with $git_user <$git_email>" - else - >&2 echo "User not accepted. Exiting." - exit 1 - fi - fi - - # check gh authentication: if this fails you will need to e.g. gh auth login - gh auth status - yq --version - python --version - cargo --version - cargo set-version --version - # check for jinja2-cli including pyyaml package - jinja2 --version - python -m pip show pyyaml -} - main() { parse_inputs "$@" - # check if tag argument provided if [ -z "${RELEASE_TAG}" ]; then - >&2 echo "Usage: create-release-candidate-branch.sh -t [-p] [-c] [-w products|operators|all]" + >&2 echo "Usage: tag-release-candidate.sh -t [-p] [-c] [-w products|operators|all]" exit 1 fi - # check if argument matches our tag regex - if [[ ! $RELEASE_TAG =~ $TAG_REGEX ]]; then - >&2 echo "Provided tag [$RELEASE_TAG] does not match the required tag regex pattern [$TAG_REGEX]" - exit 1 - fi - - if [ ! -d "$TEMP_RELEASE_FOLDER" ]; then - echo "Creating folder for cloning docker images and operators: [$TEMP_RELEASE_FOLDER]" - mkdir -p "$TEMP_RELEASE_FOLDER" - fi + validate_what "$WHAT" products operators all + validate_tag "$RELEASE_TAG" "$TAG_REGEX" - check_dependencies + ensure_temp_folder + check_basic_dependencies - # sanity checks before we start: folder, branches etc. checks tag_repos