Skip to content

Release

Release #126

Workflow file for this run

name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
tag:
description: "Re-release an existing tag (e.g., v0.1.12). Leave empty for normal release-please flow."
required: false
type: string
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
env:
GO_VERSION: "1.26"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# Release Please runs on every push to main. When a release PR is merged,
# it creates a GitHub Release + tag and sets release_created=true, which
# triggers the downstream release jobs in this same workflow run.
# This avoids the GITHUB_TOKEN limitation where tags created by Actions
# do not trigger separate tag-push workflows.
release-please:
name: Release Please
# Skip release-please when manually re-releasing an existing tag.
if: inputs.tag == '' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
# Use the GitHub App token so that release-please PR pushes
# trigger pull_request events (GITHUB_TOKEN suppresses them,
# which prevents the CI Gate status check from appearing).
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
token: ${{ steps.app-token.outputs.token }}
release:
name: Release
needs: [release-please]
# Run when release-please created a release, on manual workflow_dispatch
# with a tag ref, or when re-releasing via the tag input.
# always() is needed because release-please is skipped on
# workflow_dispatch, which would otherwise skip this job too.
if: |-
always() &&
!failure() &&
!cancelled() &&
(needs.release-please.outputs.release_created == 'true' ||
(github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/')) ||
(github.event_name == 'workflow_dispatch' && inputs.tag != ''))
runs-on: ubuntu-latest
timeout-minutes: 30
environment: release
permissions:
attestations: write
contents: write
packages: write
id-token: write
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
digest: ${{ steps.build.outputs.digest }}
tag: ${{ steps.tag.outputs.name }}
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Resolve release tag
id: tag
shell: bash
run: |
# On release-please: use its output. On re-release: use the input tag.
# On workflow_dispatch with tag ref: use ref_name.
TAG="${{ needs.release-please.outputs.tag_name || inputs.tag || github.ref_name }}"
echo "name=${TAG}" >> "$GITHUB_OUTPUT"
echo "Releasing: ${TAG}"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
ref: ${{ steps.tag.outputs.name }}
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Login to GHCR
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
- name: Capture build date
id: build_date
run: echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
- name: Clean existing release assets (idempotent re-runs)
shell: bash
run: |
TAG="${{ steps.tag.outputs.name }}"
for asset in $(gh release view "$TAG" --json assets --jq '.assets[].name' 2>/dev/null || true); do
echo "Deleting existing asset: $asset"
gh release delete-asset "$TAG" "$asset" -y || true
done
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2
with:
version: "~> v2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate provenance subjects
id: hash
shell: bash -Eeuo pipefail {0}
run: |
echo "hashes=$(cat goreleaser-dist/checksums.txt | base64 -w0)" >> "$GITHUB_OUTPUT"
- name: Build and push multi-arch image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
id: build
with:
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.name }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker.io/attuneio/attune:${{ steps.tag.outputs.name }}
docker.io/attuneio/attune:latest
build-args: |
VERSION=${{ steps.tag.outputs.name }}
COMMIT=${{ github.sha }}
DATE=${{ steps.build_date.outputs.date }}
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.version=${{ steps.tag.outputs.name }}
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Sign GHCR image
shell: bash -Eeuo pipefail -x {0}
run: |
cosign sign --yes \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
- name: Sign Docker Hub image
shell: bash -Eeuo pipefail -x {0}
run: |
cosign sign --yes \
docker.io/attuneio/attune@${{ steps.build.outputs.digest }}
- name: Attest container image
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Generate SBOM
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- name: Trivy scan released image
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
severity: HIGH,CRITICAL
exit-code: 1
- name: Sign SBOM
shell: bash -Eeuo pipefail -x {0}
run: |
cosign sign-blob --yes \
--bundle sbom.spdx.json.bundle \
sbom.spdx.json
- name: Generate install manifest and CRDs bundle
shell: bash -Eeuo pipefail -x {0}
run: |
make build-installer IMG=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.name }}
make build-crds
- name: Attach install manifest, CRDs, and SBOM to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
tag_name: ${{ steps.tag.outputs.name }}
fail_on_unmatched_files: true
files: |
dist/install.yaml
dist/crds.yaml
sbom.spdx.json
sbom.spdx.json.bundle
# Krew manifest update runs last with continue-on-error so a Krew
# failure never blocks Docker images, signatures, or provenance.
- name: Update Krew plugin manifest
continue-on-error: true
uses: rajatjindal/krew-release-bot@c970b8a8f6dbc2f2285a26e3ae160903b87002c3 # v0.0.51
with:
krew_plugin_release_tag: ${{ steps.tag.outputs.name }}
helm-release:
name: Helm Chart Release
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [release]
# Explicit condition required: release-please is skipped on workflow_dispatch,
# and GitHub Actions propagates skips transitively through the needs chain.
# Without this, downstream jobs are skipped even when release succeeded.
if: ${{ !cancelled() && needs.release.result == 'success' }}
permissions:
contents: read
packages: write
id-token: write
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release.outputs.tag }}
- uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5
with:
version: v4.1.4
- name: Login to GHCR (Helm)
shell: bash -Eeuo pipefail -x {0}
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ${{ env.REGISTRY }} \
--username ${{ github.actor }} --password-stdin
- name: Login to Docker Hub (Helm)
shell: bash -Eeuo pipefail -x {0}
run: |
echo "${{ secrets.DOCKERHUB_TOKEN }}" | helm registry login registry-1.docker.io \
--username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
- name: Login to GHCR (Docker/cosign)
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub (Docker/cosign)
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Package and push Helm chart
shell: bash -Eeuo pipefail -x {0}
run: |
# Update chart version to match tag
VERSION="${{ needs.release.outputs.tag }}"
VERSION="${VERSION#v}" # Strip 'v' prefix
sed -i "s/^version:.*/version: ${VERSION}/" charts/attune/Chart.yaml
sed -i "s/^appVersion:.*/appVersion: ${VERSION}/" charts/attune/Chart.yaml
helm package charts/attune
helm push attune-${VERSION}.tgz oci://${{ env.REGISTRY }}/${{ github.repository_owner }}/charts
helm push attune-${VERSION}.tgz oci://registry-1.docker.io/attuneio/attune-chart
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Sign Helm chart (GHCR)
shell: bash -Eeuo pipefail -x {0}
run: |
VERSION="${{ needs.release.outputs.tag }}"
VERSION="${VERSION#v}"
cosign sign --yes \
${{ env.REGISTRY }}/${{ github.repository_owner }}/charts/attune:${VERSION}
- name: Sign Helm chart (Docker Hub)
shell: bash -Eeuo pipefail -x {0}
run: |
VERSION="${{ needs.release.outputs.tag }}"
VERSION="${VERSION#v}"
cosign sign --yes \
docker.io/attuneio/attune-chart:${VERSION}
- uses: oras-project/setup-oras@v1
with:
version: "1.3.2"
- name: Push Artifact Hub metadata
shell: bash -Eeuo pipefail -x {0}
run: |
# Push artifacthub-repo.yml as OCI artifact for Verified Publisher status
echo "${{ secrets.GITHUB_TOKEN }}" | oras login ${{ env.REGISTRY }} \
--username ${{ github.actor }} --password-stdin
oras push \
${{ env.REGISTRY }}/${{ github.repository_owner }}/charts/attune:artifacthub.io \
--config /dev/null:application/vnd.cncf.artifacthub.config.v1+yaml \
artifacthub-repo.yml:application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml
# SLSA Level 3 provenance for GoReleaser binary artifacts.
# Runs as a reusable workflow (required for non-forgeable provenance).
provenance:
name: Binary Provenance
needs: [release]
if: ${{ !cancelled() && needs.release.result == 'success' }}
permissions:
actions: read
id-token: write
contents: write
# NOTE: SLSA reusable workflows require tag refs (not SHA pins) for
# self-verification of the builder binary.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: "${{ needs.release.outputs.hashes }}"
upload-assets: true
upload-tag-name: ${{ needs.release.outputs.tag }}
# SLSA Level 3 provenance for the container image.
container-provenance:
name: Container Provenance
needs: [release]
if: ${{ !cancelled() && needs.release.result == 'success' }}
permissions:
actions: read
id-token: write
packages: write
# NOTE: SLSA reusable workflows require tag refs (not SHA pins) for
# self-verification of the builder binary.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.release.outputs.digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
# Submit OLM bundle PRs to OperatorHub repos.
# Step-level continue-on-error keeps the job green while exposing the
# actual outcome via outputs for the release-notify job.
operatorhub-pr:
name: OperatorHub PR
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [release]
if: ${{ !cancelled() && needs.release.result == 'success' }}
permissions:
contents: read
outputs:
outcome: ${{ steps.create-pr.outcome }}
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ needs.release.outputs.tag }}
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
owner: attune-io
repositories: community-operators,community-operators-prod
- name: Generate OLM bundle and create PRs
id: create-pr
continue-on-error: true
env:
VERSION: ${{ needs.release.outputs.tag }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
UPSTREAM_GH_TOKEN: ${{ secrets.OPERATORHUB_PAT }}
FORK_OWNER: attune-io
run: |
# Strip 'v' prefix from tag
export VERSION="${VERSION#v}"
hack/operatorhub-pr.sh
# Sync docker/README.md to the Docker Hub repository description.
dockerhub-readme:
name: Docker Hub README
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [release]
if: ${{ !cancelled() && needs.release.result == 'success' }}
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Update Docker Hub description
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: attuneio/attune
readme-filepath: docker/README.md
short-description: "Safe, in-place Kubernetes pod resource right-sizing. VPA done right."
# Create a GitHub Issue if any optional post-release job failed.
# This prevents silent failures from going unnoticed.
release-notify:
name: Release Health Check
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [release, operatorhub-pr, helm-release]
if: always() && needs.release.result == 'success'
permissions:
issues: write
steps:
- name: Check for failures and create issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPERATORHUB_OUTCOME: ${{ needs.operatorhub-pr.outputs.outcome }}
HELM_RESULT: ${{ needs.helm-release.result }}
TAG: ${{ needs.release.outputs.tag }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
FAILURES=""
if [ "${OPERATORHUB_OUTCOME}" = "failure" ]; then
FAILURES="${FAILURES}- OperatorHub PR creation failed\n"
fi
if [ "${HELM_RESULT}" = "failure" ]; then
FAILURES="${FAILURES}- Helm chart release failed\n"
fi
if [ -n "${FAILURES}" ]; then
BODY="The release ${TAG} succeeded, but some post-release jobs failed:\n\n${FAILURES}\n[View workflow run](${RUN_URL})\n\nThese failures do not affect the core release (container images, binaries, signatures) but may leave the OperatorHub listing or Helm chart out of date."
gh issue create \
--repo "${{ github.repository }}" \
--title "Release ${TAG}: post-release job failures" \
--label "bug" \
--body "$(echo -e "${BODY}")"
else
echo "All post-release jobs succeeded."
fi