Controller Release from releases/v0.13 (dry-run) #34
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: Controller Release | |
| run-name: Controller Release from ${{ github.event.inputs.branch }}${{ github.event.inputs.dry_run == 'true' && ' (dry-run)' || '' }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| branch: | |
| description: "Branch to release from (e.g., releases/v0.21)" | |
| required: true | |
| type: string | |
| dry_run: | |
| description: "Dry-run RC phase without pushing tags or creating releases." | |
| required: false | |
| default: true | |
| type: boolean | |
| env: | |
| REGISTRY: ghcr.io | |
| COMPONENT_PATH: kubernetes/controller | |
| COMPONENT_NAME: OCM Controller | |
| concurrency: | |
| cancel-in-progress: false | |
| group: controller-release-${{ github.event.inputs.branch }} | |
| jobs: | |
| # =========================================================================== | |
| # PHASE 1: RELEASE CANDIDATE | |
| # =========================================================================== | |
| # --------------------------------------------------------------------------- | |
| # PREPARE - Computes next RC version and generates changelog | |
| # --------------------------------------------------------------------------- | |
| prepare: | |
| name: Prepare Release Metadata | |
| uses: ./.github/workflows/release-candidate-version.yml | |
| with: | |
| branch: ${{ github.event.inputs.branch }} | |
| component_path: kubernetes/controller | |
| # --------------------------------------------------------------------------- | |
| # TAG RC - Creates annotated RC git tag. | |
| # --------------------------------------------------------------------------- | |
| tag_rc: | |
| name: Create and Push RC Tag | |
| runs-on: ubuntu-latest | |
| environment: release | |
| needs: [prepare] | |
| if: ${{ github.event.inputs.dry_run == 'false' }} | |
| permissions: | |
| contents: write | |
| outputs: | |
| pushed: ${{ steps.tag.outputs.pushed }} | |
| steps: | |
| - name: Generate App Token | |
| id: get_token | |
| uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 | |
| with: | |
| app-id: ${{ secrets.OCMBOT_APP_ID }} | |
| private-key: ${{ secrets.OCMBOT_PRIV_KEY }} | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| sparse-checkout: | | |
| ${{ env.COMPONENT_PATH }} | |
| .github/scripts | |
| ref: ${{ github.event.inputs.branch }} | |
| token: ${{ steps.get_token.outputs.token }} | |
| fetch-tags: true | |
| - name: Setup git config | |
| run: | | |
| git config --global user.name "${{ github.actor }}" | |
| git config --global user.email "${{ github.actor }}@users.noreply.github.com" | |
| - name: Create RC tag | |
| id: tag | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| TAG: ${{ needs.prepare.outputs.new_tag }} | |
| with: | |
| github-token: ${{ steps.get_token.outputs.token }} | |
| script: | | |
| const script = await import('${{ github.workspace }}/.github/scripts/create-tag.js'); | |
| await script.createRcTag({ core }); | |
| # --------------------------------------------------------------------------- | |
| # BUILD - Calls main controller build workflow | |
| # --------------------------------------------------------------------------- | |
| build: | |
| name: Build Controller | |
| if: ${{ github.event.inputs.dry_run == 'false' && needs.tag_rc.outputs.pushed == 'true' }} | |
| needs: [prepare, tag_rc] | |
| uses: ./.github/workflows/kubernetes-controller.yml | |
| with: | |
| ref: ${{ needs.prepare.outputs.new_tag }} | |
| # --------------------------------------------------------------------------- | |
| # RELEASE RC - Creates GitHub pre-release with Helm chart as asset. | |
| # --------------------------------------------------------------------------- | |
| release_rc: | |
| name: Create RC Pre-Release | |
| if: ${{ github.event.inputs.dry_run == 'false' && needs.tag_rc.outputs.pushed == 'true' }} | |
| needs: [prepare, tag_rc, build] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| actions: read | |
| steps: | |
| - name: Download changelog artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: ${{ needs.prepare.outputs.changelog_artifact }} | |
| path: ${{ runner.temp }} | |
| - name: Download chart artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: ${{ needs.build.outputs.chart_artifact_name }} | |
| path: ${{ runner.temp }}/assets | |
| - name: Create RC Release | |
| uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 | |
| with: | |
| name: ${{ env.COMPONENT_NAME }} ${{ needs.prepare.outputs.new_version }} | |
| tag_name: ${{ needs.prepare.outputs.new_tag }} | |
| body_path: ${{ runner.temp }}/CHANGELOG.md | |
| fail_on_unmatched_files: true | |
| prerelease: true | |
| files: ${{ runner.temp }}/assets/chart-*.tgz | |
| - name: Summarize RC release | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| RC_TAG: ${{ needs.prepare.outputs.new_tag }} | |
| RC_VERSION: ${{ needs.prepare.outputs.new_version }} | |
| IMAGE_DIGEST: ${{ needs.build.outputs.image_digest }} | |
| CHART_DIGEST: ${{ needs.build.outputs.chart_digest }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const release = await github.rest.repos.getReleaseByTag({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| tag: process.env.RC_TAG, | |
| }); | |
| const releaseUrl = release.data.html_url; | |
| const assetsCount = Array.isArray(release.data.assets) ? release.data.assets.length : 0; | |
| const chartName = release.data.assets.find(a => a.name.endsWith('.tgz'))?.name || 'N/A'; | |
| await core.summary | |
| .addHeading('RC Release Published') | |
| .addTable([ | |
| [{ data: 'Field', header: true }, { data: 'Value', header: true }], | |
| ['RC Tag', process.env.RC_TAG], | |
| ['RC Version', process.env.RC_VERSION], | |
| ['Helm Chart', chartName], | |
| ['Image Digest', process.env.IMAGE_DIGEST ? process.env.IMAGE_DIGEST.substring(0, 19) + '...' : 'N/A'], | |
| ['Chart Digest', process.env.CHART_DIGEST ? process.env.CHART_DIGEST.substring(0, 19) + '...' : 'N/A'], | |
| ['Uploaded Assets', String(assetsCount)], | |
| ]) | |
| .addEOL() | |
| .addLink('View Release', releaseUrl) | |
| .addEOL() | |
| .write(); | |
| # =========================================================================== | |
| # PHASE 2: FINAL RELEASE (after environment gate) | |
| # =========================================================================== | |
| # Environment Gate: | |
| # The "release" environment must be configured in GitHub Settings → Environments: | |
| # - Required reviewers: At least 1 reviewer must approve | |
| # --------------------------------------------------------------------------- | |
| # VERIFY ATTESTATIONS - Ensure RC artifacts are attestable | |
| # --------------------------------------------------------------------------- | |
| verify_attestations: | |
| name: Verify RC Attestations | |
| needs: [prepare, build, release_rc] | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: release | |
| permissions: | |
| contents: read | |
| packages: read | |
| steps: | |
| - name: Verify image attestation | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| IMAGE_DIGEST: ${{ needs.build.outputs.image_digest }} | |
| run: | | |
| gh attestation verify \ | |
| "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller@${IMAGE_DIGEST}" \ | |
| --repo "${{ github.repository }}" | |
| - name: Verify chart attestation | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| CHART_DIGEST: ${{ needs.build.outputs.chart_digest }} | |
| run: | | |
| CHART_REPO="${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart" | |
| gh attestation verify "oci://${CHART_REPO}@${CHART_DIGEST}" --repo "${{ github.repository }}" | |
| # --------------------------------------------------------------------------- | |
| # PROMOTE AND RELEASE FINAL - Promotes RC artifacts and creates final release | |
| # --------------------------------------------------------------------------- | |
| # Combined job: Avoids duplicate tool setup and registry pull. | |
| # The chart must be re-packaged with the final version (Chart.yaml, values.yaml) | |
| # and re-attested, since the Helm chart embeds version strings. | |
| promote_and_release_final: | |
| name: Promote and Release Final | |
| needs: [prepare, build, verify_attestations] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Generate App Token | |
| id: get_token | |
| uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 | |
| with: | |
| app-id: ${{ secrets.OCMBOT_APP_ID }} | |
| private-key: ${{ secrets.OCMBOT_PRIV_KEY }} | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| with: | |
| sparse-checkout: | | |
| ${{ env.COMPONENT_PATH }} | |
| .github/scripts | |
| fetch-depth: 0 | |
| ref: ${{ github.event.inputs.branch }} | |
| token: ${{ steps.get_token.outputs.token }} | |
| fetch-tags: true | |
| - name: Setup git config | |
| run: | | |
| git config --global user.name "${{ github.actor }}" | |
| git config --global user.email "${{ github.actor }}@users.noreply.github.com" | |
| - name: Create final tag | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| RC_TAG: ${{ needs.prepare.outputs.new_tag }} | |
| FINAL_TAG: ${{ needs.prepare.outputs.promotion_tag }} | |
| with: | |
| github-token: ${{ steps.get_token.outputs.token }} | |
| script: | | |
| const script = await import('${{ github.workspace }}/.github/scripts/create-tag.js'); | |
| await script.createFinalTag({ core }); | |
| - name: Setup ORAS | |
| uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Promote image tags | |
| env: | |
| RC_VERSION: ${{ needs.prepare.outputs.new_version }} | |
| FINAL_VERSION: ${{ needs.prepare.outputs.base_version }} | |
| IMAGE_REPO: ${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller | |
| SET_LATEST: ${{ needs.prepare.outputs.set_latest }} | |
| run: | | |
| set -euo pipefail | |
| oras tag "${IMAGE_REPO}:${RC_VERSION}" "${FINAL_VERSION}" | |
| echo "✅ Tagged image: ${FINAL_VERSION}" | |
| if [ "${SET_LATEST}" = "true" ]; then | |
| oras tag "${IMAGE_REPO}:${FINAL_VERSION}" "latest" | |
| echo "✅ Tagged image: latest" | |
| fi | |
| - name: Install Task | |
| uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2 | |
| with: | |
| version: 3.x | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Install Helm | |
| uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Download RC chart artifact | |
| uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7 | |
| with: | |
| name: ${{ needs.build.outputs.chart_artifact_name }} | |
| path: ${{ runner.temp }}/rc-chart | |
| - name: Package final chart | |
| working-directory: ${{ env.COMPONENT_PATH }} | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.base_version }} | |
| IMAGE_DIGEST: ${{ needs.build.outputs.image_digest }} | |
| IMAGE_REPOSITORY: ${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller | |
| run: | | |
| set -euo pipefail | |
| task helm/package VERSION="${VERSION}" APP_VERSION="${VERSION}" IMAGE_DIGEST="${IMAGE_DIGEST}" IMAGE_REPOSITORY="${IMAGE_REPOSITORY}" | |
| - name: Verify promote | |
| working-directory: ${{ env.COMPONENT_PATH }} | |
| env: | |
| RC_VERSION: ${{ needs.prepare.outputs.new_version }} | |
| VERSION: ${{ needs.prepare.outputs.base_version }} | |
| run: | | |
| set -euo pipefail | |
| work=$(mktemp -d) | |
| mkdir -p "$work/rc" "$work/final" | |
| trap 'rm -rf "$work"' EXIT | |
| tar -xzf "${{ runner.temp }}/rc-chart/chart-${RC_VERSION}.tgz" -C "$work/rc" --strip-components=1 | |
| tar -xzf "dist/chart-${VERSION}.tgz" -C "$work/final" --strip-components=1 | |
| # Normalize version-dependent fields so both charts become comparable | |
| for dir in "$work/rc" "$work/final"; do | |
| yq -i 'del(.version, .appVersion)' "$dir/Chart.yaml" | |
| yq -i '.manager.image.tag |= sub("^[^@]*", "")' "$dir/values.yaml" | |
| done | |
| # After normalization both trees must be identical | |
| diff -r "$work/rc" "$work/final" | |
| echo "✅ Promote verification passed" | |
| - name: Push final chart | |
| working-directory: ${{ env.COMPONENT_PATH }} | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.base_version }} | |
| run: | | |
| set -euo pipefail | |
| task helm/push VERSION="${VERSION}" REGISTRY="${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller" | |
| - name: Resolve chart digest | |
| id: chart-digest | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.base_version }} | |
| run: | | |
| set -euo pipefail | |
| CHART_REPO="${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart" | |
| echo "digest=$(oras resolve ${CHART_REPO}:${VERSION})" >> "$GITHUB_OUTPUT" | |
| - name: Attest final chart | |
| uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| subject-digest: ${{ steps.chart-digest.outputs.digest }} | |
| subject-name: ${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart | |
| push-to-registry: true | |
| - name: Get RC release notes | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| gh release view "${{ needs.prepare.outputs.new_tag }}" \ | |
| --repo "${{ github.repository }}" --json body --jq '.body' > "${{ runner.temp }}/CHANGELOG.md" | |
| - name: Prepare final chart assets | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.base_version }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "${{ runner.temp }}/final-assets" | |
| cp "${{ env.COMPONENT_PATH }}/dist/chart-${VERSION}.tgz" "${{ runner.temp }}/final-assets/" | |
| - name: Publish final release | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| RC_TAG: ${{ needs.prepare.outputs.new_tag }} | |
| FINAL_TAG: ${{ needs.prepare.outputs.promotion_tag }} | |
| FINAL_VERSION: ${{ needs.prepare.outputs.base_version }} | |
| COMPONENT_NAME: ${{ env.COMPONENT_NAME }} | |
| ASSETS_DIR: ${{ runner.temp }}/final-assets | |
| NOTES_FILE: ${{ runner.temp }}/CHANGELOG.md | |
| IMAGE_REPO: ${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller | |
| IMAGE_DIGEST: ${{ needs.build.outputs.image_digest }} | |
| CHART_REPO: ${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart | |
| SET_LATEST: ${{ needs.prepare.outputs.set_latest }} | |
| HIGHEST_FINAL_VERSION: ${{ needs.prepare.outputs.highest_final_version }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const script = await import('${{ github.workspace }}/.github/scripts/publish-final-release.js'); | |
| await script.default({ github, context, core }); |