Controller Release from releases/v0.21 #12
Workflow file for this run
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 | |
| concurrency: | |
| cancel-in-progress: true | |
| group: controller-release-${{ github.event.inputs.branch }} | |
| jobs: | |
| # ============================================================ | |
| # PHASE 1: RELEASE CANDIDATE | |
| # ============================================================ | |
| prepare: | |
| name: Prepare Release Metadata | |
| uses: ./.github/workflows/release-candidate-version.yml | |
| with: | |
| branch: ${{ github.event.inputs.branch }} | |
| component_path: kubernetes/controller | |
| secrets: inherit | |
| tag_rc: | |
| name: Create RC Tag | |
| runs-on: ubuntu-latest | |
| 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 }} | |
| ref: ${{ github.event.inputs.branch }} | |
| token: ${{ steps.get_token.outputs.token }} | |
| - 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 tag | |
| id: tag | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| TAG: ${{ needs.prepare.outputs.new_tag }} | |
| CHANGELOG_B64: ${{ needs.prepare.outputs.changelog_b64 }} | |
| with: | |
| github-token: ${{ steps.get_token.outputs.token }} | |
| script: | | |
| const { execSync } = require("child_process"); | |
| const tag = process.env.TAG; | |
| const msg = Buffer.from(process.env.CHANGELOG_B64, "base64").toString("utf8"); | |
| try { execSync(`git rev-parse "refs/tags/${tag}"`); core.setOutput("pushed","false"); return; } catch {} | |
| require("fs").writeFileSync(".tagmsg", msg); | |
| execSync(`git tag -a "${tag}" -F .tagmsg`); | |
| execSync(`git push origin "refs/tags/${tag}"`); | |
| core.setOutput("pushed","true"); | |
| 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-new.yml | |
| secrets: inherit | |
| with: | |
| ref: ${{ needs.prepare.outputs.new_tag }} | |
| release_rc: | |
| name: Create RC 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 | |
| steps: | |
| - name: Decode changelog | |
| run: echo "${{ needs.prepare.outputs.changelog_b64 }}" | base64 --decode > "${{ runner.temp }}/CHANGELOG.md" | |
| - name: Install Helm | |
| uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Login to GHCR | |
| uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download Helm chart | |
| run: | | |
| helm pull "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart" \ | |
| --version "${{ needs.prepare.outputs.new_version }}" \ | |
| --destination "${{ runner.temp }}/assets" | |
| - name: Create RC Release | |
| uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2 | |
| with: | |
| name: Controller ${{ 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/ocm-k8s-toolkit-*.tgz | |
| # ============================================================ | |
| # PHASE 2: FINAL RELEASE (after environment gate) | |
| # ============================================================ | |
| verify_attestations: | |
| name: Verify RC Attestations | |
| needs: [prepare, build, release_rc] | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: controller/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: Setup ORAS | |
| uses: oras-project/setup-oras@22ce207df3b08e061f537244349aac6ae1d214f6 # v1 | |
| - name: Install Helm | |
| uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Login to GHCR | |
| uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Verify chart attestation | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ needs.prepare.outputs.new_version }} | |
| run: | | |
| CHART_REPO="${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart" | |
| DIGEST=$(oras resolve "${CHART_REPO}:${VERSION}") | |
| gh attestation verify "oci://${CHART_REPO}@${DIGEST}" --repo "${{ github.repository }}" | |
| promote_final: | |
| name: Promote to Final | |
| needs: [prepare, build, verify_attestations] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| outputs: | |
| set_latest: ${{ steps.promote_oci.outputs.set_latest }} | |
| 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 }} | |
| fetch-depth: 0 | |
| ref: ${{ github.event.inputs.branch }} | |
| token: ${{ steps.get_token.outputs.token }} | |
| - 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 { execSync } = require("child_process"); | |
| const { RC_TAG: rcTag, FINAL_TAG: finalTag } = process.env; | |
| try { execSync(`git rev-parse "refs/tags/${finalTag}"`, { stdio: "pipe" }); core.setFailed(`Tag ${finalTag} exists`); return; } catch {} | |
| const rcSha = execSync(`git rev-parse "refs/tags/${rcTag}^{commit}"`, { stdio: "pipe" }).toString().trim(); | |
| execSync(`git tag -a "${finalTag}" "${rcSha}" -m "Promote ${rcTag} to ${finalTag}"`); | |
| execSync(`git push origin "refs/tags/${finalTag}"`); | |
| - 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 | |
| id: promote_oci | |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 | |
| env: | |
| RC_VERSION: ${{ needs.prepare.outputs.new_version }} | |
| FINAL_VERSION: ${{ needs.prepare.outputs.promotion_version }} | |
| IMAGE_REPO: ${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller | |
| with: | |
| script: | | |
| const { execSync } = require('child_process'); | |
| const { RC_VERSION: rc, FINAL_VERSION: final, IMAGE_REPO: repo } = process.env; | |
| let highest = ''; | |
| try { | |
| const releases = await github.rest.repos.listReleases({ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }); | |
| highest = releases.data | |
| .filter(r => !r.prerelease && r.tag_name.startsWith('kubernetes/controller/v')) | |
| .map(r => r.tag_name.replace('kubernetes/controller/', '')) | |
| .filter(t => /^v\d+\.\d+\.\d+$/.test(t)) | |
| .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })) | |
| .pop() || ''; | |
| } catch {} | |
| const setLatest = !highest || final.localeCompare(highest, undefined, { numeric: true }) >= 0; | |
| execSync(`oras tag "${repo}:${rc}" ${setLatest ? `"${final}" "latest"` : `"${final}"`}`, { stdio: 'inherit' }); | |
| core.setOutput('set_latest', setLatest ? 'true' : 'false'); | |
| - 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: Package and push final chart | |
| working-directory: ${{ env.COMPONENT_PATH }} | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.promotion_version }} | |
| IMAGE_DIGEST: ${{ needs.build.outputs.image_digest }} | |
| run: | | |
| task helm/package VERSION="${VERSION}" APP_VERSION="${VERSION}" IMAGE_DIGEST="${IMAGE_DIGEST}" | |
| helm push dist/ocm-k8s-toolkit-${VERSION}.tgz oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart | |
| - name: Resolve chart digest | |
| id: chart-digest | |
| env: | |
| VERSION: ${{ needs.prepare.outputs.promotion_version }} | |
| run: | | |
| 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 | |
| release_final: | |
| name: Create Final Release | |
| needs: [prepare, promote_final] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Get RC release notes | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release view "${{ needs.prepare.outputs.new_tag }}" \ | |
| --repo "${{ github.repository }}" --json body --jq '.body' > "${{ runner.temp }}/CHANGELOG.md" | |
| - name: Install Helm | |
| uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 | |
| with: | |
| version: v3.14.0 | |
| - name: Login to GHCR | |
| uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download final chart | |
| run: | | |
| helm pull "oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart" \ | |
| --version "${{ needs.prepare.outputs.promotion_version }}" \ | |
| --destination "${{ runner.temp }}/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.promotion_version }} | |
| ASSETS_DIR: ${{ runner.temp }}/assets | |
| NOTES_FILE: ${{ runner.temp }}/CHANGELOG.md | |
| SET_LATEST: ${{ needs.promote_final.outputs.set_latest }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { FINAL_TAG: finalTag, FINAL_VERSION: finalVersion, RC_TAG: rcTag, ASSETS_DIR: assetsDir, NOTES_FILE: notesFile, SET_LATEST: setLatest } = process.env; | |
| let notes = fs.existsSync(notesFile) ? fs.readFileSync(notesFile, 'utf8').trim() : `Promoted from ${rcTag}`; | |
| const today = new Date().toISOString().split('T')[0]; | |
| notes = notes.replace(/^\[([^\]]+)\]\s*-\s*[\d-]+/, `[${finalTag}] - promoted from [${rcTag}] on ${today}`); | |
| const created = await github.rest.repos.createRelease({ | |
| owner: context.repo.owner, repo: context.repo.repo, | |
| tag_name: finalTag, name: `Controller ${finalVersion}`, | |
| body: notes, prerelease: false, make_latest: setLatest === 'true' ? 'true' : 'false', | |
| }); | |
| for (const file of fs.readdirSync(assetsDir).filter(f => f.endsWith('.tgz'))) { | |
| const data = fs.readFileSync(path.join(assetsDir, file)); | |
| await github.rest.repos.uploadReleaseAsset({ | |
| owner: context.repo.owner, repo: context.repo.repo, release_id: created.data.id, | |
| name: file, data, headers: { 'content-type': 'application/gzip', 'content-length': data.length }, | |
| }); | |
| } |