-
Notifications
You must be signed in to change notification settings - Fork 1
[KCM-6031] FinOps Agent release automation #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+310
−0
Merged
Changes from 9 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
3b51835
First pass at release automation.
thomasvn c9c0c7f
Update token name. Add comments about token.
thomasvn e74f2f3
Merge branch 'develop' into thomasn/auto-release
thomasvn 1038ed0
Merge branch 'develop' into thomasn/auto-release
peatey 759180c
Merge branch 'develop' into thomasn/auto-release
thomasvn e15b051
Use Github App Token as opposed to PAT.
thomasvn de7da3c
Add dry-run capability
thomasvn b4f3827
Simplify logic.
thomasvn e2eafc2
Simplify.
thomasvn f432bef
Claude's recommended fixes
thomasvn 5c6e59f
Fixes from automated review.
thomasvn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,277 @@ | ||
| name: Automated FinOps Agent Release | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| version: | ||
| description: "Version number (e.g., 1.0.22)" | ||
| required: true | ||
| type: string | ||
| # Every irreversible action here is a `git push`, so a dry run passes | ||
| # --dry-run to each one: git still contacts the remote and validates auth | ||
| # and the ref update, but nothing lands. Steps that only observe a push's | ||
| # side effects are skipped with `if: !inputs.dry_run`. Not exercised: the | ||
| # image build, the chart publish, and Chart.lock (which needs the new | ||
| # chart version to actually be published). | ||
| dry_run: | ||
| description: "Dry run (validate and edit locally, push nothing)" | ||
| required: false | ||
| default: false | ||
| type: boolean | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: finops-agent-release | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| TAG: v${{ inputs.version }} | ||
|
|
||
| # The only difference between a real release and a dry run. | ||
| PUSH_ARGS: ${{ inputs.dry_run && '--dry-run' || '' }} | ||
|
|
||
| # The cross-repo contract. The two workflow filenames name files in repos | ||
| # this one cannot see, so a rename there surfaces here only as a "did not | ||
| # start" timeout — keep them visible in one place. | ||
| BUILD_REPO: kubecost/integration-ci-cd | ||
| BUILD_WORKFLOW: finops-agent-build-release.yaml | ||
| CHART_REPO: kubecost/finops-agent-chart | ||
| CHART_WORKFLOW: publish-chart.yaml | ||
| KUBECOST_REPO: kubecost/kubecost | ||
| KUBECOST_BRANCH: develop | ||
|
|
||
| # Applies to every commit and annotated tag this workflow creates. | ||
| GIT_AUTHOR_NAME: kc-actions-bot[bot] | ||
| GIT_AUTHOR_EMAIL: kc-actions-bot[bot]@users.noreply.github.com | ||
| GIT_COMMITTER_NAME: kc-actions-bot[bot] | ||
| GIT_COMMITTER_EMAIL: kc-actions-bot[bot]@users.noreply.github.com | ||
|
|
||
| jobs: | ||
| tag-agent: | ||
| name: Validate and tag ibm-finops-agent | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Validate version format | ||
| run: | | ||
| if ! [[ "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then | ||
| echo "❌ Invalid version format. Expected: X.Y.Z (e.g., 1.0.22)" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Version format valid: ${{ inputs.version }}" | ||
|
|
||
| - name: Generate app token to tag ibm-finops-agent and watch the build | ||
| id: generate-app-token | ||
| uses: actions/create-github-app-token@v3.2.0 | ||
| with: | ||
| client-id: ${{ vars.KC_ACTIONS_BOT_APP_CLIENT_ID }} | ||
| private-key: ${{ secrets.KC_ACTIONS_BOT_APP_PRIVATE_KEY }} | ||
| repositories: | | ||
| ibm-finops-agent | ||
| integration-ci-cd | ||
|
|
||
| - name: Checkout ibm-finops-agent | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: develop | ||
| token: ${{ steps.generate-app-token.outputs.token }} | ||
|
|
||
| - name: Check tag doesn't exist | ||
| run: | | ||
| if git ls-remote --exit-code --tags origin "$TAG" >/dev/null 2>&1; then | ||
| echo "❌ Tag $TAG already exists" | ||
| exit 1 | ||
| fi | ||
| echo "✅ Tag $TAG is available" | ||
|
|
||
| - name: Create and push tag | ||
| run: | | ||
| git tag -a "$TAG" -m "Release $TAG" | ||
| git push $PUSH_ARGS origin "$TAG" | ||
|
|
||
| # The tag push triggers build-release.yaml, which dispatches to | ||
| # $BUILD_REPO. Poll for the run that appears there, then watch it. | ||
| - name: Wait for image build | ||
| if: ${{ !inputs.dry_run }} | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-app-token.outputs.token }} | ||
| timeout-minutes: 30 | ||
| run: | | ||
| for i in {1..24}; do | ||
| RUN_ID=$(gh run list --repo "$BUILD_REPO" --workflow "$BUILD_WORKFLOW" \ | ||
| --event repository_dispatch --limit 1 \ | ||
| --json databaseId --jq '.[0].databaseId // empty') | ||
| if [ -n "$RUN_ID" ]; then break; fi | ||
| echo "⏳ Waiting for build to start (attempt $i/24)..." | ||
| sleep 5 | ||
| done | ||
| : "${RUN_ID:?build workflow did not start within 2 minutes}" | ||
|
|
||
| gh run watch "$RUN_ID" --repo "$BUILD_REPO" --exit-status | ||
|
|
||
| update-chart: | ||
| name: Update finops-agent-chart | ||
| needs: tag-agent | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Generate app token to update finops-agent-chart | ||
| id: generate-app-token | ||
| uses: actions/create-github-app-token@v3.2.0 | ||
| with: | ||
| client-id: ${{ vars.KC_ACTIONS_BOT_APP_CLIENT_ID }} | ||
| private-key: ${{ secrets.KC_ACTIONS_BOT_APP_PRIVATE_KEY }} | ||
| repositories: finops-agent-chart | ||
|
|
||
| - name: Checkout finops-agent-chart | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| repository: ${{ env.CHART_REPO }} | ||
| ref: main | ||
| token: ${{ steps.generate-app-token.outputs.token }} | ||
|
|
||
| - name: Bump image tag and chart version | ||
| run: | | ||
| sed -i -E "/^image:/,/^[^ ]/ s|^ tag: .*| tag: $TAG|" charts/finops-agent/values.yaml | ||
| sed -i -E "s|^version: .*|version: ${{ inputs.version }}|; | ||
| s|^appVersion: .*|appVersion: $TAG|" charts/finops-agent/Chart.yaml | ||
|
|
||
| git --no-pager diff | ||
|
|
||
| verify() { | ||
| actual=$(yq "$2" "$1") | ||
| [ "$actual" = "$3" ] || { echo "❌ $1: $2 is '$actual', expected '$3'"; exit 1; } | ||
| } | ||
| verify charts/finops-agent/values.yaml .image.tag "$TAG" | ||
| verify charts/finops-agent/Chart.yaml .version "${{ inputs.version }}" | ||
| verify charts/finops-agent/Chart.yaml .appVersion "$TAG" | ||
|
|
||
| - name: Commit, tag, and push | ||
| run: | | ||
| git add charts/finops-agent/values.yaml charts/finops-agent/Chart.yaml | ||
| git commit -m "Release $TAG" | ||
| git tag -a "$TAG" -m "Release $TAG" | ||
| git push $PUSH_ARGS origin main "$TAG" | ||
|
|
||
| # --branch matches the tag ref, so this correlates to the tag just | ||
| # pushed rather than to whatever happened to run most recently. | ||
| - name: Wait for chart publish | ||
| if: ${{ !inputs.dry_run }} | ||
| env: | ||
| GH_TOKEN: ${{ steps.generate-app-token.outputs.token }} | ||
| timeout-minutes: 20 | ||
| run: | | ||
| for i in {1..24}; do | ||
| RUN_ID=$(gh run list --repo "$CHART_REPO" --workflow "$CHART_WORKFLOW" \ | ||
| --event push --branch "$TAG" --limit 1 \ | ||
| --json databaseId --jq '.[0].databaseId // empty') | ||
| if [ -n "$RUN_ID" ]; then break; fi | ||
| echo "⏳ Waiting for chart publish to start (attempt $i/24)..." | ||
| sleep 5 | ||
| done | ||
| : "${RUN_ID:?chart publish workflow did not start within 2 minutes}" | ||
|
|
||
| gh run watch "$RUN_ID" --repo "$CHART_REPO" --exit-status | ||
|
|
||
| update-kubecost: | ||
| name: Update kubecost chart dependency | ||
| needs: update-chart | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Generate app token to update kubecost | ||
| id: generate-app-token | ||
| uses: actions/create-github-app-token@v3.2.0 | ||
| with: | ||
| client-id: ${{ vars.KC_ACTIONS_BOT_APP_CLIENT_ID }} | ||
| private-key: ${{ secrets.KC_ACTIONS_BOT_APP_PRIVATE_KEY }} | ||
| repositories: kubecost | ||
|
|
||
| - name: Checkout kubecost | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| repository: ${{ env.KUBECOST_REPO }} | ||
| ref: ${{ env.KUBECOST_BRANCH }} | ||
| token: ${{ steps.generate-app-token.outputs.token }} | ||
|
|
||
| - name: Install Helm | ||
| uses: azure/setup-helm@v3 | ||
| with: | ||
| version: v3.12.0 | ||
|
|
||
| - name: Bump finops-agent dependency | ||
| run: | | ||
| sed -i -E "/^ - name: finops-agent$/,/^ - name:/ s|^ version: .*| version: \"$TAG\"|" kubecost/Chart.yaml | ||
|
|
||
| git --no-pager diff | ||
|
|
||
| actual=$(yq '.dependencies[] | select(.name == "finops-agent") | .version' kubecost/Chart.yaml) | ||
|
thomasvn marked this conversation as resolved.
|
||
| [ "$actual" = "$TAG" ] || { echo "❌ dependency is '$actual', expected '$TAG'"; exit 1; } | ||
|
|
||
| # Needs the new chart version to actually be published, so it can only | ||
| # run for real. Chart.lock is left untouched in a dry run. | ||
| - name: Update Chart.lock | ||
| if: ${{ !inputs.dry_run }} | ||
| run: | | ||
| helm repo add finops-agent https://kubecost.github.io/finops-agent-chart/ || true | ||
| helm repo update finops-agent | ||
| helm dependency update ./kubecost | ||
|
|
||
| - name: Commit and push | ||
| run: | | ||
| git add kubecost/Chart.yaml kubecost/Chart.lock | ||
| git commit -m "Update finops-agent to $TAG" | ||
| git push $PUSH_ARGS origin "$KUBECOST_BRANCH" | ||
|
|
||
| summary: | ||
| name: Release Summary | ||
| needs: [tag-agent, update-chart, update-kubecost] | ||
| runs-on: ubuntu-latest | ||
| if: always() | ||
| steps: | ||
| - name: Report status | ||
| env: | ||
| TAG_RESULT: ${{ needs.tag-agent.result }} | ||
| CHART_RESULT: ${{ needs.update-chart.result }} | ||
| KUBECOST_RESULT: ${{ needs.update-kubecost.result }} | ||
| run: | | ||
| mark() { | ||
| case "$1" in | ||
| success) echo "✅" ;; | ||
| skipped) echo "⏭️" ;; | ||
| *) echo "❌" ;; | ||
| esac | ||
| } | ||
|
|
||
| if [ "${{ inputs.dry_run }}" = "true" ]; then | ||
| TITLE="🔍 Dry Run — FinOps Agent Release $TAG (nothing was pushed)" | ||
| else | ||
| TITLE="FinOps Agent Release $TAG" | ||
| fi | ||
|
|
||
| cat >> "$GITHUB_STEP_SUMMARY" <<EOF | ||
| # $TITLE | ||
|
|
||
| - $(mark "$TAG_RESULT") ibm-finops-agent tagged and image built | ||
| - $(mark "$CHART_RESULT") finops-agent-chart updated and published | ||
| - $(mark "$KUBECOST_RESULT") kubecost chart dependency updated | ||
|
|
||
| [agent tag](https://github.com/${{ github.repository }}/releases/tag/$TAG) · | ||
| [chart release](https://github.com/$CHART_REPO/releases/tag/$TAG) · | ||
| [kubecost commits](https://github.com/$KUBECOST_REPO/commits/$KUBECOST_BRANCH) | ||
| EOF | ||
|
|
||
| # A dry run pushes no tags, so there is never anything to clean up. | ||
| - name: Add cleanup instructions | ||
| if: ${{ !inputs.dry_run && contains(needs.*.result, 'failure') }} | ||
| run: | | ||
| cat >> "$GITHUB_STEP_SUMMARY" <<EOF | ||
|
|
||
| ## Manual cleanup | ||
|
|
||
| The release stopped partway through. Delete whichever tags were pushed: | ||
|
|
||
| \`\`\`bash | ||
| gh api -X DELETE repos/${{ github.repository }}/git/refs/tags/$TAG | ||
| gh api -X DELETE repos/$CHART_REPO/git/refs/tags/$TAG | ||
| \`\`\` | ||
| EOF | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.