Skip to content

Controller Release from releases/v0.21 #16

Controller Release from releases/v0.21

Controller Release from releases/v0.21 #16

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
env:
CHART_REPO: oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/kubernetes/controller/chart
VERSION: ${{ needs.prepare.outputs.new_version }}
run: |
mkdir -p "${{ runner.temp }}/assets"
for i in 1 2 3 4 5; do
if helm pull "${CHART_REPO}" --version "${VERSION}" --destination "${{ runner.temp }}/assets"; then
echo "✓ Chart downloaded successfully"
exit 0
fi
echo "Attempt $i: Chart not yet available in registry, waiting..."
sleep 5
done
echo "::error::Failed to download chart after 5 attempts"
exit 1
- 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
id: chart-push
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
echo "chart_path=dist/ocm-k8s-toolkit-${VERSION}.tgz" >> "$GITHUB_OUTPUT"
- name: Attest final chart
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
subject-path: ${{ env.COMPONENT_PATH }}/${{ steps.chart-push.outputs.chart_path }}
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 },
});
}