Generate Release PRs #229
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Generate Release PRs | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| chart: | |
| description: "Optional: limit to a single chart name (leave empty for all charts)" | |
| required: false | |
| default: "" | |
| type: string | |
| force_bump: | |
| description: "Force a patch release even if no releasable commits found (requires chart input)" | |
| required: false | |
| default: false | |
| type: boolean | |
| schedule: | |
| # Daily at 2:00 AM UTC | |
| - cron: '0 2 * * *' | |
| push: | |
| branches: [main] | |
| paths: ['charts/**'] | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| release-pr: | |
| runs-on: ubuntu-latest | |
| env: | |
| CHART_INPUT: ${{ github.event.inputs.chart || '' }} | |
| FORCE_BUMP: ${{ github.event.inputs.force_bump || 'false' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Load version pins | |
| uses: ./.github/actions/load-pins | |
| - name: Validate inputs | |
| if: ${{ github.event.inputs.force_bump == 'true' && github.event.inputs.chart == '' }} | |
| run: | | |
| echo "::error::force_bump=true requires a specific chart to be specified" | |
| exit 1 | |
| - name: Install generators (pinned) | |
| uses: ./.github/actions/install-generators | |
| - name: Configure git | |
| run: | | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git config user.name "github-actions[bot]" | |
| - name: Discover charts needing release | |
| id: discover | |
| shell: bash | |
| run: | | |
| # Discover which charts have unreleased changes | |
| if [ -n "${CHART_INPUT}" ]; then | |
| if [ -d "charts/${CHART_INPUT}" ]; then | |
| charts="${CHART_INPUT}" | |
| echo "Scoping to input chart: ${CHART_INPUT}" | |
| else | |
| echo "::error::Input chart '${CHART_INPUT}' not found under charts/" | |
| exit 1 | |
| fi | |
| else | |
| charts="$(find charts -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null || true)" | |
| fi | |
| # Filter charts that need a release | |
| charts_needing_release=() | |
| while IFS= read -r chart; do | |
| [ -z "$chart" ] && continue | |
| echo "::group::Checking ${chart}" | |
| # Skip if the last commit to THIS chart is a release prep commit | |
| # This prevents re-triggering immediately after a release PR is merged | |
| last_commit_for_chart=$(git log -1 --pretty=%s -- "charts/${chart}/") | |
| if [[ "$last_commit_for_chart" == "chore(${chart}): prepare release"* ]]; then | |
| echo "⏭️ ${chart}: last commit is a release prep, skipping" | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| last_tag="$(git tag --list "${chart}-*" | sort -V | tail -1 || true)" | |
| echo "Last tag for ${chart}: ${last_tag:-none}" | |
| # Classify commits since last tag | |
| chmod +x hack/release/*.sh | |
| classify_output="$(hack/release/classify_commits.sh "charts/${chart}" "$last_tag")" || { | |
| echo "::error::Failed to classify commits for ${chart}" | |
| continue | |
| } | |
| eval "$classify_output" | |
| # Check if force_bump should override BUMP_LEVEL=none | |
| if [ "${BUMP_LEVEL}" = "none" ] && [ "${FORCE_BUMP}" = "true" ] && [ -n "${CHART_INPUT}" ]; then | |
| echo "⚠️ ${chart}: forcing patch bump (no releasable commits, but force_bump=true)" | |
| BUMP_LEVEL="patch" | |
| fi | |
| if [ "${BUMP_LEVEL}" != "none" ] && [ -n "${BUMP_LEVEL}" ]; then | |
| echo "✅ ${chart} needs ${BUMP_LEVEL} bump" | |
| charts_needing_release+=("${chart}") | |
| else | |
| echo "⏭️ ${chart} has no changes requiring release" | |
| fi | |
| echo "::endgroup::" | |
| done <<< "$charts" | |
| if [ ${#charts_needing_release[@]} -eq 0 ]; then | |
| echo "::notice::No charts require a release" | |
| echo "charts_json=[]" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "::notice::Charts needing release: ${charts_needing_release[*]}" | |
| printf '%s\n' "${charts_needing_release[@]}" | jq -R -s -c 'split("\n") | map(select(length>0))' > /tmp/charts.json | |
| echo "charts_json=$(cat /tmp/charts.json)" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Process release PRs | |
| if: steps.discover.outputs.charts_json != '[]' | |
| shell: bash | |
| env: | |
| CHARTS_JSON: ${{ steps.discover.outputs.charts_json }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| FORCE_BUMP: ${{ github.event.inputs.force_bump || 'false' }} | |
| CHART_INPUT: ${{ github.event.inputs.chart || '' }} | |
| run: | | |
| charts=$(echo "$CHARTS_JSON" | jq -r '.[]') | |
| for chart in $charts; do | |
| echo "::group::Processing release for ${chart}" | |
| # Determine last tag | |
| last_tag="$(git tag --list "${chart}-*" | sort -V | tail -1 || true)" | |
| echo "Last tag: ${last_tag:-none}" | |
| # Classify commits and get bump level | |
| current_version="$(yq '.version' "charts/${chart}/Chart.yaml")" | |
| classify_output="$(hack/release/classify_commits.sh "charts/${chart}" "$last_tag")" || { | |
| echo "::error::Failed to classify commits for ${chart}" | |
| echo "::endgroup::" | |
| continue | |
| } | |
| eval "$classify_output" | |
| auto_bump="${BUMP_LEVEL}" | |
| echo "Current version: ${current_version}" | |
| echo "Automatic bump level: ${auto_bump}" | |
| # Check for label overrides on existing PRs | |
| override="" | |
| skip="false" | |
| # Query GitHub API for existing release PR (filter by prefix since --head doesn't support wildcards) | |
| existing_pr=$(gh pr list --state open --json number,labels,headRefName | jq -r --arg prefix "release/${chart}-" 'map(select(.headRefName | startswith($prefix))) | .[0] // empty') | |
| if [ -n "$existing_pr" ]; then | |
| labels=$(echo "$existing_pr" | jq -r '.labels[].name') | |
| if echo "$labels" | grep -q "skip-release"; then | |
| echo "::notice::Found 'skip-release' label, skipping ${chart}" | |
| skip="true" | |
| elif echo "$labels" | grep -q "bump:major"; then | |
| override="major" | |
| elif echo "$labels" | grep -q "bump:minor"; then | |
| override="minor" | |
| elif echo "$labels" | grep -q "bump:patch"; then | |
| override="patch" | |
| fi | |
| if [ -n "$override" ]; then | |
| echo "::notice::Label override detected: ${override}" | |
| fi | |
| fi | |
| if [ "$skip" = "true" ]; then | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| # Calculate next version | |
| # Apply force_bump if auto_bump is none and force was requested | |
| if [ "$auto_bump" = "none" ] && [ "${FORCE_BUMP}" = "true" ] && [ -n "${CHART_INPUT}" ]; then | |
| auto_bump="patch" | |
| echo "::notice::Forcing patch bump for ${chart}" | |
| fi | |
| final_bump="${override:-$auto_bump}" | |
| if [ "$final_bump" = "none" ]; then | |
| echo "::warning::No bump needed for ${chart}, skipping" | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| # For first release (no prior tag), use Chart.yaml version as-is | |
| if [ -z "$last_tag" ]; then | |
| next_version="$current_version" | |
| echo "Next version: ${next_version} (first release, using Chart.yaml version)" | |
| else | |
| next_version="$(hack/release/next_version.sh "$current_version" "" "$final_bump")" | |
| echo "Next version: ${next_version} (${final_bump} bump)" | |
| fi | |
| # Create/switch to release branch | |
| branch="release/${chart}-v${next_version}" | |
| git switch -c "$branch" 2>/dev/null || git switch "$branch" | |
| # Update Chart.yaml version | |
| hack/release/update_chart_version.sh "charts/${chart}" "$next_version" | |
| # Generate CHANGELOG | |
| printf '{"name":"%s-chart","version":"%s"}' "${chart}" "$next_version" > package.json | |
| npx conventional-changelog -p conventionalcommits --tag-prefix "${chart}-" --commit-path "charts/${chart}" -i "charts/${chart}/CHANGELOG.md" -s || true | |
| rm package.json | |
| # Detect and generate README with appropriate tool | |
| echo "🔍 Detecting documentation format for ${chart}..." | |
| doc_tool="none" | |
| values_file="charts/${chart}/values.yaml" | |
| if grep -q "^## @param" "${values_file}"; then | |
| doc_tool="readme-generator" | |
| echo "✅ Detected readme-generator format (## @param annotations)" | |
| elif grep -q "^# --" "${values_file}"; then | |
| doc_tool="helm-docs" | |
| echo "✅ Detected helm-docs format (# -- annotations)" | |
| else | |
| echo "⚠️ No documentation annotations found in values.yaml" | |
| fi | |
| # Generate README based on detected format | |
| if [ "${doc_tool}" = "helm-docs" ]; then | |
| if command -v helm-docs > /dev/null 2>&1; then | |
| echo "📝 Generating README with helm-docs" | |
| helm-docs -c "charts/${chart}" | |
| else | |
| echo "::warning::helm-docs not found, skipping README generation" | |
| fi | |
| elif [ "${doc_tool}" = "readme-generator" ]; then | |
| echo "📝 Generating README with readme-generator" | |
| npx @bitnami/readme-generator-for-helm --readme "charts/${chart}/README.md" --values "charts/${chart}/values.yaml" | |
| else | |
| echo "⚠️ Skipping README generation (no documentation annotations found)" | |
| fi | |
| # Commit changes | |
| git add "charts/${chart}/Chart.yaml" "charts/${chart}/CHANGELOG.md" "charts/${chart}/README.md" | |
| if git diff --cached --quiet; then | |
| echo "::warning::No changes to commit for ${chart}" | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| git commit -m "chore(${chart}): prepare release v${next_version}" \ | |
| -m "Automated Release PR preparation" \ | |
| -m "" \ | |
| -m "Bump: ${final_bump}" \ | |
| -m "Previous version: ${current_version}" \ | |
| -m "Next version: ${next_version}" | |
| # Push branch | |
| git push origin "${branch}" --force | |
| echo "✅ Pushed branch: ${branch}" | |
| # Open or update PR | |
| existing_pr_number=$(gh pr list --state open --head "${branch}" --json number --jq '.[0].number' || echo "") | |
| compare_url="https://github.com/${{ github.repository }}/compare/${last_tag:-main}...${branch}" | |
| # Create PR body using heredoc | |
| pr_body=$(cat <<EOF | |
| Automated Release PR for **${chart}** v${next_version} | |
| ## Release Information | |
| - **Chart:** ${chart} | |
| - **Current version:** ${current_version} | |
| - **Next version:** ${next_version} | |
| - **Bump type:** ${final_bump} | |
| - **Compare:** ${compare_url} | |
| ## Label Overrides | |
| You can override the version bump by adding labels: | |
| - \`bump:major\` - Force major version bump | |
| - \`bump:minor\` - Force minor version bump | |
| - \`bump:patch\` - Force patch version bump | |
| - \`skip-release\` - Defer this release (accumulate more changes) | |
| Merge this PR to publish the chart. | |
| EOF | |
| ) | |
| if [ -n "$existing_pr_number" ]; then | |
| gh pr edit "$existing_pr_number" \ | |
| --title "release(${chart}): v${next_version}" \ | |
| --body "$pr_body" | |
| echo "✅ Updated PR #${existing_pr_number}" | |
| else | |
| gh pr create \ | |
| --head "${branch}" \ | |
| --base main \ | |
| --title "release(${chart}): v${next_version}" \ | |
| --body "$pr_body" \ | |
| --label "release-pr" | |
| echo "✅ Created new PR" | |
| fi | |
| # Return to main branch for next chart | |
| git switch main | |
| echo "::endgroup::" | |
| done |