Automate release bumps from the current branch (#831)

* Automate dependent chart release bumps

Signed-off-by: Faisal Memon <[email protected]>

* Use ruamel for chart dependency parsing

Signed-off-by: Faisal Memon <[email protected]>

* Simplify dependent chart graph helper

Signed-off-by: Faisal Memon <[email protected]>

* Add current-branch mode to release automation

Signed-off-by: Faisal Memon <[email protected]>

* Polish chart dependency helper

Signed-off-by: Faisal Memon <[email protected]>

---------

Signed-off-by: Faisal Memon <[email protected]>
This commit is contained in:
Faisal Memon
2026-05-31 05:29:55 -07:00
committed by GitHub
parent ab3164d48e
commit a40409ec53
2 changed files with 440 additions and 32 deletions
+255 -32
View File
@@ -6,23 +6,27 @@
##
## Usage example(s):
##
## ./__PROG__ --chart spire --new-version 0.16.0
## ./__PROG__ --chart spire-crds --new-version 0.3.0
## ./__PROG__ --chart spire --bump patch
## ./__PROG__ --chart spire-crds --bump minor
##
## Options:
## --help Show this help message
## --chart The chart to release
## --new-version The new version number
## --bump The semantic version bump type: major, minor, or patch
## --from-current-branch Apply the release bump on the current branch instead of recreating a bump branch from main
## --dry-run Will not actually submit the PR
##
## Prerequisites:
## - gsed (MacOS)
## - git
## - helm
## - GitHub CLI (gh)
## - npm (if readme-generator is not already installed)
## - yq
##
## Commands
##
## ./__PROG__ --chart «chart» --current-version «current-version» --new-version «new-version» [--dry-run]
## ./__PROG__ --chart «chart» --bump «major|minor|patch» [--from-current-branch] [--dry-run]
me=$(basename "$0")
function usage {
@@ -39,10 +43,22 @@ function print_error_and_exit {
exit 1
}
function require_command {
command -v "$1" >/dev/null 2>&1 || {
print_error_and_exit "$2"
}
}
function unreleased_changes_other_charts {
local chart latest_tag changes
for chart in "$@" ; do
latest_tag="$(git --no-pager tag --list "${chart}-[0-9]*.[0-9]*.[0-9]*" | sort -V | tail -n 1)"
changes="$(git --no-pager log "${latest_tag}..HEAD" --pretty=format:'* %h %s' "charts/${chart}")"
latest_tag="$(latest_chart_tag "${chart}")"
if [ -n "${latest_tag}" ] ; then
changes="$(git --no-pager log "${latest_tag}..HEAD" --pretty=format:'* %h %s' -- "charts/${chart}")"
else
changes="$(git --no-pager log --pretty=format:'* %h %s' -- "charts/${chart}")"
fi
if [ -n "${changes}" ] ; then
echo "### Unreleased changes ${chart}"
echo
@@ -51,12 +67,125 @@ function unreleased_changes_other_charts {
echo Please ensure you bump above charts as well before merging main into the release branch.
echo
echo '```shell'
echo ./release-chart.sh --chart "${chart}" --new-version ………
echo ./release-chart.sh --chart "${chart}" --bump patch
echo '```'
fi
done
}
function latest_chart_tag {
local chart_name=$1
git --no-pager tag --list "${chart_name}-[0-9]*.[0-9]*.[0-9]*" | sort -V | tail -n 1
}
function bump_version {
local current_version=$1
local bump_type=$2
local major minor patch
IFS=. read -r major minor patch <<< "${current_version}"
if [[ -z "${major}" || -z "${minor}" || -z "${patch}" ]]; then
print_error_and_exit "invalid semantic version '${current_version}'"
fi
case "${bump_type}" in
major)
major=$((major + 1))
minor=0
patch=0
;;
minor)
minor=$((minor + 1))
patch=0
;;
patch)
patch=$((patch + 1))
;;
*)
print_error_and_exit "invalid bump type '${bump_type}'"
;;
esac
echo "${major}.${minor}.${patch}"
}
function update_dependency_version {
local chart_yaml=$1
local dependency_chart=$2
local dependency_version=$3
DEPENDENCY_CHART="${dependency_chart}" DEPENDENCY_VERSION="${dependency_version}" \
yq e 'with(.dependencies[]? | select(.name == strenv(DEPENDENCY_CHART)); .version = strenv(DEPENDENCY_VERSION))' -i "${chart_yaml}"
}
function update_chart_version {
local chart_name=$1
local dependency_version=$2
local chart_yaml="charts/${chart_name}/Chart.yaml"
TARGET_VERSION="${dependency_version}" \
yq e '.version = strenv(TARGET_VERSION)' -i "${chart_yaml}"
}
function ensure_readme_generator {
local readme_generator_version="2.6.0"
local readme_generator_exe="readme-generator"
if ! hash "${readme_generator_exe}" 2>/dev/null; then
echo >&2 "${readme_generator_exe} not installed. Installing..."
require_command npm "npm is required to install ${readme_generator_exe}. Please install npm and rerun the script."
npm install -g "@bitnami/readme-generator-for-helm@${readme_generator_version}"
fi
}
function refresh_chart_docs {
local chart_dir
ensure_readme_generator
for chart_dir in "$@" ; do
if [ -f "${chart_dir}/values.yaml" ] && [ -f "${chart_dir}/README.md" ] ; then
echo >&2 "Generating Chart documentation for ${chart_dir}…"
readme-generator --values="${chart_dir}/values.yaml" --readme="${chart_dir}/README.md"
fi
done
}
function chart_has_remote_dependencies {
local chart_yaml=$1
local remote_count
remote_count="$(yq e '[.dependencies[]? | select(((.repository // "") | test("^file://")) | not)] | length' "${chart_yaml}")"
[ "${remote_count}" -gt 0 ]
}
function refresh_chart_dependencies {
local chart_dir
local refreshed_repos=''
for chart_dir in "$@" ; do
if chart_has_remote_dependencies "${chart_dir}/Chart.yaml" && [ -z "${refreshed_repos}" ] ; then
helm repo update
refreshed_repos='true'
fi
helm dependency update --skip-refresh "${chart_dir}"
done
}
function collect_dependent_charts {
local root_chart=$1
python3 scripts/chart-graph.py --chart "${root_chart}" --output names
}
function get_chart_version {
local chart_name=$1
yq e '.version' "charts/${chart_name}/Chart.yaml"
}
while (("$#")); do
case "$1" in
--help)
@@ -67,10 +196,14 @@ while (("$#")); do
chart=$2
shift 2
;;
--new-version)
new_version=$2
--bump)
bump_type=$2
shift 2
;;
--from-current-branch)
from_current_branch='true'
shift 1
;;
--dry-run)
dry_run='-w'
shift 1
@@ -82,14 +215,13 @@ while (("$#")); do
esac
done
command -v gh >/dev/null 2>&1 || {
print_error_and_exit 'the GitHub cli (gh) is required to run this script'
}
require_command gh 'the GitHub cli (gh) is required to run this script'
require_command helm 'helm is required to run this script'
require_command yq 'yq is required to run this script'
require_command python3 'python3 is required to run this script'
if [[ $OSTYPE == "darwin"* ]]; then
command -v gsed >/dev/null 2>&1 || {
print_error_and_exit 'gsed is required to run this script'
}
require_command gsed 'gsed is required to run this script'
SED='gsed'
else
SED='sed'
@@ -100,28 +232,105 @@ if [ -z "$chart" ]; then
print_error_and_exit 'chart option is missing'
fi
if [ -z "$new_version" ]; then
if [ -z "$bump_type" ]; then
usage
print_error_and_exit 'new-version option is missing'
print_error_and_exit 'bump option is missing'
fi
if [ ! -f "charts/${chart}/Chart.yaml" ] ; then
print_error_and_exit "no chart named '${chart}' in charts folder"
fi
branch_name="bump-${chart}-version"
if [ -n "${from_current_branch}" ] ; then
branch_name="$(git branch --show-current)"
if [ -z "${branch_name}" ] ; then
print_error_and_exit 'unable to determine current branch; please checkout a branch before using --from-current-branch'
fi
else
branch_name="bump-${chart}-version"
git fetch --tags
git checkout main
git pull
git checkout --track -B "${branch_name}" main
git fetch --tags
git checkout main
git pull
git checkout --track -B "${branch_name}" main
fi
current_version="$(grep '^version:' "charts/${chart}/Chart.yaml" | awk '{print $2}')"
commits_since_previous_release="$(git log "${chart}-${current_version}..HEAD" --pretty=format:'* %h %s' "charts/${chart}")"
"${SED}" -i "s/version: ${current_version}/version: ${new_version}/" "charts/${chart}/Chart.yaml"
new_version="$(bump_version "${current_version}" "${bump_type}")"
release_base_tag="$(latest_chart_tag "${chart}")"
if [ -n "${release_base_tag}" ] ; then
commits_since_previous_release="$(git log "${release_base_tag}..HEAD" --pretty=format:'* %h %s' -- "charts/${chart}")"
else
commits_since_previous_release="$(git log --pretty=format:'* %h %s' -- "charts/${chart}")"
fi
update_chart_version "${chart}" "${new_version}"
"${SED}" -i "s/${current_version}/${new_version}/g" "charts/${chart}/README.md"
git add "charts/${chart}/"{Chart.yaml,README.md}
git commit -m "Bump ${chart} Helm Chart version from ${current_version} to ${new_version}" \
updated_dependency_charts=()
updated_chart_versions=()
release_charts=("${chart}")
while IFS= read -r dependent_chart ; do
if [ -z "${dependent_chart}" ] ; then
continue
fi
chart_yaml="charts/${dependent_chart}/Chart.yaml"
if [ ! -f "${chart_yaml}" ] ; then
print_error_and_exit "dependent chart '${dependent_chart}' does not have a Chart.yaml at ${chart_yaml}"
fi
dependent_current_version="$(grep '^version:' "${chart_yaml}" | awk '{print $2}')"
dependent_new_version="$(bump_version "${dependent_current_version}" "${bump_type}")"
update_chart_version "${dependent_chart}" "${dependent_new_version}"
update_dependency_version "${chart_yaml}" "${chart}" "${new_version}"
updated_dependency_charts+=("charts/${dependent_chart}")
updated_chart_versions+=("${dependent_chart}:${dependent_current_version}:${dependent_new_version}")
release_charts+=("${dependent_chart}")
done < <(collect_dependent_charts "${chart}")
unique_dependency_charts=()
while IFS= read -r chart_dir ; do
if [ -n "${chart_dir}" ] ; then
unique_dependency_charts+=("${chart_dir}")
fi
done < <(printf '%s\n' "${updated_dependency_charts[@]}" | sort -u)
unique_release_charts=()
while IFS= read -r chart_name ; do
if [ -n "${chart_name}" ] ; then
unique_release_charts+=("${chart_name}")
fi
done < <(printf '%s\n' "${release_charts[@]}" | sort -u)
for chart_name in "${unique_release_charts[@]}" ; do
chart_version="$(get_chart_version "${chart_name}")"
for chart_dir in "${unique_dependency_charts[@]}" ; do
update_dependency_version "${chart_dir}/Chart.yaml" "${chart_name}" "${chart_version}"
done
done
refresh_chart_dependencies "${unique_dependency_charts[@]}"
refresh_chart_docs "charts/${chart}" "${unique_dependency_charts[@]}"
if [ -n "${dry_run}" ] && [ -n "${from_current_branch}" ] ; then
echo >&2
echo >&2 "Dry run completed on the current branch (${branch_name})."
echo >&2 "Inspect the working tree diff before deciding what to keep."
exit 0
fi
git add release-chart.sh "charts/${chart}/"{Chart.yaml,README.md}
for chart_dir in "${unique_dependency_charts[@]}" ; do
git add "${chart_dir}/Chart.yaml"
if [ -f "${chart_dir}/Chart.lock" ] ; then
git add "${chart_dir}/Chart.lock"
fi
if [ -f "${chart_dir}/README.md" ] ; then
git add "${chart_dir}/README.md"
fi
done
git commit -m "Bump ${chart} and dependent Helm Chart versions (${bump_type})" \
-m "${commits_since_previous_release}" \
-s
git push -u origin --force-with-lease
@@ -154,6 +363,14 @@ $(unreleased_changes_other_charts "${other_charts[@]}")
> git push
> \`\`\`
## Release set
- ${chart}: ${current_version} -> ${new_version}
$(for version_update in "${updated_chart_versions[@]}" ; do
IFS=: read -r dependent_chart dependent_current_version dependent_new_version <<< "${version_update}"
echo "- ${dependent_chart}: ${dependent_current_version} -> ${dependent_new_version}"
done)
## Changes in this release
${commits_since_previous_release}
@@ -161,11 +378,15 @@ EOF
if [ -n "${dry_run}" ] ; then
echo >&2
echo >&2 "If you choose not to submit the PR please run following commands to cleanup the branch:"
echo >&2
echo >&2 " git checkout main"
echo >&2 " git push origin :${branch_name}"
echo >&2 " git branch -D ${branch_name}"
if [ -n "${from_current_branch}" ] ; then
echo >&2 "Dry run completed on the current branch (${branch_name}). Inspect the branch diff before deciding what to keep."
else
echo >&2 "If you choose not to submit the PR please run following commands to cleanup the branch:"
echo >&2
echo >&2 " git checkout main"
echo >&2 " git push origin :${branch_name}"
echo >&2 " git branch -D ${branch_name}"
fi
echo >&2
echo >&2 'If you choose to submit the PR, please run following:'
echo >&2
@@ -174,4 +395,6 @@ if [ -n "${dry_run}" ] ; then
fi
gh pr merge --auto -r -d
git checkout main
if [ -z "${from_current_branch}" ] ; then
git checkout main
fi