Skip to content

Controller Release from releases/v0.13 (dry-run) #34

Controller Release from releases/v0.13 (dry-run)

Controller Release from releases/v0.13 (dry-run) #34

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 });