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:
+244
-21
@@ -6,23 +6,27 @@
|
|||||||
##
|
##
|
||||||
## Usage example(s):
|
## Usage example(s):
|
||||||
##
|
##
|
||||||
## ./__PROG__ --chart spire --new-version 0.16.0
|
## ./__PROG__ --chart spire --bump patch
|
||||||
## ./__PROG__ --chart spire-crds --new-version 0.3.0
|
## ./__PROG__ --chart spire-crds --bump minor
|
||||||
##
|
##
|
||||||
## Options:
|
## Options:
|
||||||
## --help Show this help message
|
## --help Show this help message
|
||||||
## --chart The chart to release
|
## --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
|
## --dry-run Will not actually submit the PR
|
||||||
##
|
##
|
||||||
## Prerequisites:
|
## Prerequisites:
|
||||||
## - gsed (MacOS)
|
## - gsed (MacOS)
|
||||||
## - git
|
## - git
|
||||||
|
## - helm
|
||||||
## - GitHub CLI (gh)
|
## - GitHub CLI (gh)
|
||||||
|
## - npm (if readme-generator is not already installed)
|
||||||
|
## - yq
|
||||||
##
|
##
|
||||||
## Commands
|
## 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")
|
me=$(basename "$0")
|
||||||
|
|
||||||
function usage {
|
function usage {
|
||||||
@@ -39,10 +43,22 @@ function print_error_and_exit {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function require_command {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
print_error_and_exit "$2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function unreleased_changes_other_charts {
|
function unreleased_changes_other_charts {
|
||||||
|
local chart latest_tag changes
|
||||||
|
|
||||||
for chart in "$@" ; do
|
for chart in "$@" ; do
|
||||||
latest_tag="$(git --no-pager tag --list "${chart}-[0-9]*.[0-9]*.[0-9]*" | sort -V | tail -n 1)"
|
latest_tag="$(latest_chart_tag "${chart}")"
|
||||||
changes="$(git --no-pager log "${latest_tag}..HEAD" --pretty=format:'* %h %s' "charts/${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
|
if [ -n "${changes}" ] ; then
|
||||||
echo "### Unreleased changes ${chart}"
|
echo "### Unreleased changes ${chart}"
|
||||||
echo
|
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 Please ensure you bump above charts as well before merging main into the release branch.
|
||||||
echo
|
echo
|
||||||
echo '```shell'
|
echo '```shell'
|
||||||
echo ./release-chart.sh --chart "${chart}" --new-version ………
|
echo ./release-chart.sh --chart "${chart}" --bump patch
|
||||||
echo '```'
|
echo '```'
|
||||||
fi
|
fi
|
||||||
done
|
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
|
while (("$#")); do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--help)
|
--help)
|
||||||
@@ -67,10 +196,14 @@ while (("$#")); do
|
|||||||
chart=$2
|
chart=$2
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--new-version)
|
--bump)
|
||||||
new_version=$2
|
bump_type=$2
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--from-current-branch)
|
||||||
|
from_current_branch='true'
|
||||||
|
shift 1
|
||||||
|
;;
|
||||||
--dry-run)
|
--dry-run)
|
||||||
dry_run='-w'
|
dry_run='-w'
|
||||||
shift 1
|
shift 1
|
||||||
@@ -82,14 +215,13 @@ while (("$#")); do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
command -v gh >/dev/null 2>&1 || {
|
require_command gh 'the GitHub cli (gh) is required to run this script'
|
||||||
print_error_and_exit '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
|
if [[ $OSTYPE == "darwin"* ]]; then
|
||||||
command -v gsed >/dev/null 2>&1 || {
|
require_command gsed 'gsed is required to run this script'
|
||||||
print_error_and_exit 'gsed is required to run this script'
|
|
||||||
}
|
|
||||||
SED='gsed'
|
SED='gsed'
|
||||||
else
|
else
|
||||||
SED='sed'
|
SED='sed'
|
||||||
@@ -100,28 +232,105 @@ if [ -z "$chart" ]; then
|
|||||||
print_error_and_exit 'chart option is missing'
|
print_error_and_exit 'chart option is missing'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -z "$new_version" ]; then
|
if [ -z "$bump_type" ]; then
|
||||||
usage
|
usage
|
||||||
print_error_and_exit 'new-version option is missing'
|
print_error_and_exit 'bump option is missing'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "charts/${chart}/Chart.yaml" ] ; then
|
if [ ! -f "charts/${chart}/Chart.yaml" ] ; then
|
||||||
print_error_and_exit "no chart named '${chart}' in charts folder"
|
print_error_and_exit "no chart named '${chart}' in charts folder"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
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"
|
branch_name="bump-${chart}-version"
|
||||||
|
|
||||||
git fetch --tags
|
git fetch --tags
|
||||||
git checkout main
|
git checkout main
|
||||||
git pull
|
git pull
|
||||||
git checkout --track -B "${branch_name}" main
|
git checkout --track -B "${branch_name}" main
|
||||||
|
fi
|
||||||
|
|
||||||
current_version="$(grep '^version:' "charts/${chart}/Chart.yaml" | awk '{print $2}')"
|
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}")"
|
new_version="$(bump_version "${current_version}" "${bump_type}")"
|
||||||
"${SED}" -i "s/version: ${current_version}/version: ${new_version}/" "charts/${chart}/Chart.yaml"
|
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"
|
"${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}" \
|
-m "${commits_since_previous_release}" \
|
||||||
-s
|
-s
|
||||||
git push -u origin --force-with-lease
|
git push -u origin --force-with-lease
|
||||||
@@ -154,6 +363,14 @@ $(unreleased_changes_other_charts "${other_charts[@]}")
|
|||||||
> git push
|
> 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
|
## Changes in this release
|
||||||
|
|
||||||
${commits_since_previous_release}
|
${commits_since_previous_release}
|
||||||
@@ -161,11 +378,15 @@ EOF
|
|||||||
|
|
||||||
if [ -n "${dry_run}" ] ; then
|
if [ -n "${dry_run}" ] ; then
|
||||||
echo >&2
|
echo >&2
|
||||||
|
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 "If you choose not to submit the PR please run following commands to cleanup the branch:"
|
||||||
echo >&2
|
echo >&2
|
||||||
echo >&2 " git checkout main"
|
echo >&2 " git checkout main"
|
||||||
echo >&2 " git push origin :${branch_name}"
|
echo >&2 " git push origin :${branch_name}"
|
||||||
echo >&2 " git branch -D ${branch_name}"
|
echo >&2 " git branch -D ${branch_name}"
|
||||||
|
fi
|
||||||
echo >&2
|
echo >&2
|
||||||
echo >&2 'If you choose to submit the PR, please run following:'
|
echo >&2 'If you choose to submit the PR, please run following:'
|
||||||
echo >&2
|
echo >&2
|
||||||
@@ -174,4 +395,6 @@ if [ -n "${dry_run}" ] ; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
gh pr merge --auto -r -d
|
gh pr merge --auto -r -d
|
||||||
|
if [ -z "${from_current_branch}" ] ; then
|
||||||
git checkout main
|
git checkout main
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
|
|
||||||
|
yaml = YAML(typ="safe")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Dependency:
|
||||||
|
name: str
|
||||||
|
repository: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Chart:
|
||||||
|
name: str
|
||||||
|
path: Path
|
||||||
|
dependencies: tuple[Dependency, ...]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
"""Parse CLI arguments for the root chart lookup."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Print charts that depend on a given root chart."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--chart",
|
||||||
|
required=True,
|
||||||
|
help="Chart name whose dependent chart closure should be printed.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--charts-root",
|
||||||
|
default="charts",
|
||||||
|
help="Path to the charts root directory (default: charts)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
choices=("names", "print-graph"),
|
||||||
|
default="names",
|
||||||
|
help="Output format (default: names).",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Execute the dependent-chart lookup and print the selected output format."""
|
||||||
|
args = parse_args()
|
||||||
|
charts_root = Path(args.charts_root).resolve()
|
||||||
|
charts = discover_charts(charts_root)
|
||||||
|
|
||||||
|
if args.chart not in charts:
|
||||||
|
print(f"Unknown chart: {args.chart}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
reverse_dependencies = build_reverse_dependencies(charts)
|
||||||
|
dependents = find_dependents(args.chart, reverse_dependencies)
|
||||||
|
|
||||||
|
if args.output == "print-graph":
|
||||||
|
print(f"Dependents of {args.chart}:")
|
||||||
|
if not dependents:
|
||||||
|
print(" (none)")
|
||||||
|
else:
|
||||||
|
for dependent in dependents:
|
||||||
|
relpath = charts[dependent].path.relative_to(charts_root.parent)
|
||||||
|
print(f" {dependent} [{relpath}]")
|
||||||
|
else:
|
||||||
|
for dependent in dependents:
|
||||||
|
print(dependent)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def parse_chart_yaml(chart_yaml: Path) -> Chart:
|
||||||
|
"""Read one Chart.yaml file into the lightweight Chart structure."""
|
||||||
|
with chart_yaml.open() as fp:
|
||||||
|
data = yaml.load(fp)
|
||||||
|
|
||||||
|
if not isinstance(data, dict) or "name" not in data:
|
||||||
|
raise ValueError(f"Could not find chart name in {chart_yaml}")
|
||||||
|
|
||||||
|
dependencies: list[Dependency] = []
|
||||||
|
for dependency in data.get("dependencies", []) or []:
|
||||||
|
if not isinstance(dependency, dict) or "name" not in dependency:
|
||||||
|
continue
|
||||||
|
dependencies.append(
|
||||||
|
Dependency(
|
||||||
|
name=str(dependency["name"]),
|
||||||
|
repository=str(dependency.get("repository", "")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Chart(
|
||||||
|
name=str(data["name"]),
|
||||||
|
path=chart_yaml.parent.resolve(),
|
||||||
|
dependencies=tuple(dependencies),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def discover_charts(charts_root: Path) -> dict[str, Chart]:
|
||||||
|
"""Discover every chart under the charts root and index them by chart name."""
|
||||||
|
charts: dict[str, Chart] = {}
|
||||||
|
for chart_yaml in sorted(charts_root.rglob("Chart.yaml")):
|
||||||
|
chart = parse_chart_yaml(chart_yaml)
|
||||||
|
if chart.name in charts:
|
||||||
|
raise ValueError(f"Duplicate chart name detected: {chart.name}")
|
||||||
|
charts[chart.name] = chart
|
||||||
|
return charts
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_local_dependency(
|
||||||
|
source_chart: Chart, dependency: Dependency, charts: dict[str, Chart]
|
||||||
|
) -> str | None:
|
||||||
|
"""Resolve a file:// dependency reference back to a known local chart name."""
|
||||||
|
if not dependency.repository.startswith("file://"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
dependency_path = (
|
||||||
|
source_chart.path / dependency.repository.removeprefix("file://")
|
||||||
|
).resolve()
|
||||||
|
chart_yaml = dependency_path / "Chart.yaml"
|
||||||
|
if not chart_yaml.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
for chart_name, chart in charts.items():
|
||||||
|
if chart.path == dependency_path:
|
||||||
|
return chart_name
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_reverse_dependencies(charts: dict[str, Chart]) -> dict[str, set[str]]:
|
||||||
|
"""Build a reverse dependency index for walking from a chart to its dependents."""
|
||||||
|
reverse_dependencies: dict[str, set[str]] = {
|
||||||
|
chart_name: set() for chart_name in charts
|
||||||
|
}
|
||||||
|
|
||||||
|
for chart_name, chart in charts.items():
|
||||||
|
for dependency in chart.dependencies:
|
||||||
|
dependency_name = resolve_local_dependency(chart, dependency, charts)
|
||||||
|
if dependency_name is not None:
|
||||||
|
reverse_dependencies[dependency_name].add(chart_name)
|
||||||
|
|
||||||
|
return reverse_dependencies
|
||||||
|
|
||||||
|
|
||||||
|
def find_dependents(root_chart: str, reverse_dependencies: dict[str, set[str]]) -> list[str]:
|
||||||
|
"""Traverse the reverse dependency graph and fail fast on reachable cycles."""
|
||||||
|
dependents: list[str] = []
|
||||||
|
visited: set[str] = set()
|
||||||
|
on_stack: set[str] = {root_chart}
|
||||||
|
stack: list[tuple[str, list[str]]] = [
|
||||||
|
(root_chart, sorted(reverse_dependencies[root_chart]))
|
||||||
|
]
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
current, children = stack[-1]
|
||||||
|
if not children:
|
||||||
|
on_stack.remove(current)
|
||||||
|
stack.pop()
|
||||||
|
continue
|
||||||
|
|
||||||
|
child = children.pop(0)
|
||||||
|
if child in on_stack:
|
||||||
|
cycle = " -> ".join([item[0] for item in stack] + [child])
|
||||||
|
raise ValueError(f"Dependency cycle detected: {cycle}")
|
||||||
|
if child in visited:
|
||||||
|
continue
|
||||||
|
|
||||||
|
visited.add(child)
|
||||||
|
dependents.append(child)
|
||||||
|
on_stack.add(child)
|
||||||
|
stack.append((child, sorted(reverse_dependencies[child])))
|
||||||
|
|
||||||
|
return dependents
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user