refactor: deduplicate validation and recommendation init code (#341) #218
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: 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: | |
| 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@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 | |
| 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 | |
| pull-requests: write | |
| id-token: write | |
| outputs: | |
| 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| fetch-depth: 0 | |
| ref: ${{ steps.tag.outputs.name }} | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| cache: true | |
| - name: Check if Docker build is possible | |
| id: docker-check | |
| run: | | |
| if [ -n "${{ inputs.tag }}" ]; then | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::Skipping Docker build on re-release to preserve OLM bundle digest" | |
| elif [ -f Dockerfile.release ]; then | |
| echo "enabled=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "enabled=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::Dockerfile.release not found at this tag; skipping Docker image build" | |
| fi | |
| - name: Login to GHCR | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Login to Docker Hub | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Set up Docker Buildx | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4 | |
| - name: Capture build date | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| 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 | |
| if [ -n "${{ inputs.tag }}" ]; then | |
| # Re-release: only delete GoReleaser-produced assets so install.yaml, | |
| # crds.yaml, SBOM, and provenance are preserved. | |
| case "$asset" in | |
| kubectl-attune_*.tar.gz|checksums.txt|checksums.txt.sig|checksums.txt.pem) | |
| echo "Deleting GoReleaser asset: $asset" | |
| gh release delete-asset "$TAG" "$asset" -y || true | |
| ;; | |
| *) | |
| echo "Preserving: $asset" | |
| ;; | |
| esac | |
| else | |
| echo "Deleting existing asset: $asset" | |
| gh release delete-asset "$TAG" "$asset" -y || true | |
| fi | |
| done | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Run GoReleaser | |
| uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7.2.2 | |
| with: | |
| version: "~> v2" | |
| args: release --clean | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Apply custom release notes | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.tag.outputs.name }} | |
| run: | | |
| if [ -f RELEASE_NOTES.md ]; then | |
| echo "Custom release notes found, updating release body..." | |
| gh release edit "$TAG" --notes-file RELEASE_NOTES.md | |
| else | |
| echo "No custom release notes, using auto-generated notes" | |
| fi | |
| - name: Extract legacy .sig and .pem from Sigstore bundle | |
| shell: bash -Eeuo pipefail {0} | |
| run: | | |
| # GoReleaser now signs with --bundle, producing a .sigstore.json. | |
| # Extract separate .sig and .pem files for backward compatibility | |
| # with downstream verification scripts. | |
| BUNDLE="goreleaser-dist/checksums.txt.sigstore.json" | |
| if [ -f "$BUNDLE" ] && [ ! -f "goreleaser-dist/checksums.txt.sig" ]; then | |
| echo "Extracting .sig and .pem from Sigstore bundle" | |
| jq -r '.messageSignature.signature' "$BUNDLE" \ | |
| > goreleaser-dist/checksums.txt.sig | |
| jq -r '.verificationMaterial.certificate.rawBytes' "$BUNDLE" \ | |
| | base64 -d | openssl x509 -inform DER -outform PEM \ | |
| > goreleaser-dist/checksums.txt.pem | |
| gh release upload "${{ steps.tag.outputs.name }}" \ | |
| goreleaser-dist/checksums.txt.sig \ | |
| goreleaser-dist/checksums.txt.pem \ | |
| --repo "${{ github.repository }}" --clobber | |
| elif [ -f "goreleaser-dist/checksums.txt.sig" ]; then | |
| echo "Legacy .sig already exists (produced by GoReleaser directly)" | |
| else | |
| echo "No bundle or signatures found; fallback step will handle this" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Sign checksums (fallback for older tags without signs config) | |
| if: inputs.tag != '' | |
| shell: bash | |
| run: | | |
| # If GoReleaser's .goreleaser.yaml at this tag lacks any signs | |
| # config, neither .sigstore.json nor .sig will exist. Sign manually. | |
| if [ ! -f goreleaser-dist/checksums.txt.sig ] && [ ! -f goreleaser-dist/checksums.txt.sigstore.json ]; then | |
| echo "GoReleaser did not produce signatures; signing checksums manually" | |
| cosign sign-blob --yes \ | |
| --bundle goreleaser-dist/checksums.txt.bundle \ | |
| goreleaser-dist/checksums.txt | |
| jq -r '.messageSignature.signature' goreleaser-dist/checksums.txt.bundle \ | |
| > goreleaser-dist/checksums.txt.sig | |
| jq -r '.verificationMaterial.certificate.rawBytes' goreleaser-dist/checksums.txt.bundle \ | |
| | base64 -d | openssl x509 -inform DER -outform PEM \ | |
| > goreleaser-dist/checksums.txt.pem | |
| gh release upload "${{ steps.tag.outputs.name }}" \ | |
| goreleaser-dist/checksums.txt.sig \ | |
| goreleaser-dist/checksums.txt.pem \ | |
| --repo "${{ github.repository }}" --clobber | |
| else | |
| echo "GoReleaser already produced signatures" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Attest binary archives | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-path: goreleaser-dist/*.tar.gz | |
| - name: Attest checksums | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-path: goreleaser-dist/checksums.txt | |
| - name: Upload attestation bundles to release | |
| shell: bash -Eeuo pipefail {0} | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ steps.tag.outputs.name }} | |
| run: | | |
| mkdir -p attestation-bundles | |
| for asset in goreleaser-dist/*.tar.gz goreleaser-dist/checksums.txt; do | |
| [ -f "$asset" ] || continue | |
| name=$(basename "$asset") | |
| tmpdir=$(mktemp -d) | |
| pushd "$tmpdir" > /dev/null | |
| if gh attestation download "$OLDPWD/$asset" \ | |
| --repo "${{ github.repository }}" 2>/dev/null; then | |
| for bundle in *.jsonl; do | |
| [ -f "$bundle" ] || continue | |
| cp "$bundle" "$OLDPWD/attestation-bundles/${name}.intoto.jsonl" | |
| break | |
| done | |
| fi | |
| popd > /dev/null | |
| rm -rf "$tmpdir" | |
| done | |
| if ls attestation-bundles/*.intoto.jsonl 1>/dev/null 2>&1; then | |
| gh release upload "$TAG" attestation-bundles/*.intoto.jsonl --clobber | |
| fi | |
| - name: Prepare release image context | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| shell: bash -Eeuo pipefail {0} | |
| run: | | |
| # Map GoReleaser output dirs to Docker TARGETARCH layout. | |
| # GoReleaser names dirs like manager_linux_{arch}_{variant}, | |
| # e.g. manager_linux_amd64_v1, manager_linux_arm_7, manager_linux_s390x. | |
| for dir in goreleaser-dist/manager_linux_*/; do | |
| arch=$(basename "$dir" | sed 's/^manager_linux_//' | sed 's/_.*//') | |
| mkdir -p ".release-image/${arch}" | |
| cp "${dir}manager" ".release-image/${arch}/manager" | |
| done | |
| # Verify all target platforms have a binary | |
| for arch in amd64 arm64 arm ppc64le s390x; do | |
| if [ ! -f ".release-image/${arch}/manager" ]; then | |
| echo "ERROR: missing manager binary for ${arch}" >&2 | |
| exit 1 | |
| fi | |
| done | |
| - name: Compute image tags | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| id: image-tags | |
| shell: bash | |
| run: | | |
| TAG="${{ steps.tag.outputs.name }}" | |
| NL=$'\n' | |
| TAGS="${REGISTRY}/${IMAGE_NAME}:${TAG}${NL}docker.io/attuneio/attune:${TAG}" | |
| # Skip :latest on re-releases to avoid overwriting a newer release | |
| if [ -z "${{ inputs.tag }}" ]; then | |
| TAGS="${TAGS}${NL}${REGISTRY}/${IMAGE_NAME}:latest${NL}docker.io/attuneio/attune:latest" | |
| fi | |
| { | |
| echo "tags<<EOF" | |
| echo "$TAGS" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Build and push multi-arch image | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 | |
| id: build | |
| with: | |
| context: .release-image | |
| file: Dockerfile.release | |
| platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/ppc64le,linux/s390x | |
| push: true | |
| tags: ${{ steps.image-tags.outputs.tags }} | |
| 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: Sign GHCR image | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| shell: bash -Eeuo pipefail -x {0} | |
| run: | | |
| cosign sign --yes \ | |
| ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }} | |
| - name: Sign Docker Hub image | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| shell: bash -Eeuo pipefail -x {0} | |
| run: | | |
| cosign sign --yes \ | |
| docker.io/attuneio/attune@${{ steps.build.outputs.digest }} | |
| - name: Attest container image | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 | |
| with: | |
| subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | |
| subject-digest: ${{ steps.build.outputs.digest }} | |
| push-to-registry: true | |
| - name: Generate SBOM | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| 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 | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| 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 | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| 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 | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| 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 | |
| if: steps.docker-check.outputs.enabled == 'true' | |
| 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 | |
| if: inputs.tag == '' | |
| continue-on-error: true | |
| uses: rajatjindal/krew-release-bot@c970b8a8f6dbc2f2285a26e3ae160903b87002c3 # v0.0.51 | |
| with: | |
| krew_plugin_release_tag: ${{ steps.tag.outputs.name }} | |
| # Clean up RELEASE_NOTES.md from main after the release is published. | |
| # Branch protection blocks direct pushes, so we create a short-lived PR. | |
| # Uses the GitHub App token so the PR triggers CI and auto-approve. | |
| - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| id: cleanup-token | |
| with: | |
| client-id: ${{ vars.APP_CLIENT_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| - name: Clean up release notes file | |
| env: | |
| GH_TOKEN: ${{ steps.cleanup-token.outputs.token }} | |
| TAG: ${{ steps.tag.outputs.name }} | |
| run: | | |
| if git ls-tree HEAD --name-only | grep -q '^RELEASE_NOTES.md$'; then | |
| BRANCH="chore/cleanup-release-notes-${TAG}" | |
| gh api "repos/${{ github.repository }}/git/refs" \ | |
| -f ref="refs/heads/$BRANCH" \ | |
| -f sha="$(git rev-parse HEAD)" | |
| FILE_SHA=$(gh api \ | |
| "repos/${{ github.repository }}/contents/RELEASE_NOTES.md?ref=$BRANCH" \ | |
| --jq '.sha') | |
| gh api --method DELETE \ | |
| "repos/${{ github.repository }}/contents/RELEASE_NOTES.md" \ | |
| -f message="chore: remove release notes override" \ | |
| -f sha="$FILE_SHA" \ | |
| -f branch="$BRANCH" | |
| PR_URL=$(gh pr create --base main --head "$BRANCH" \ | |
| --title "chore: remove release notes override" \ | |
| --body "Auto-cleanup after ${TAG} release.") | |
| gh pr merge "$PR_URL" --auto --squash | |
| fi | |
| 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. | |
| # Skip on re-releases (inputs.tag set) since re-releases only fix release | |
| # artifacts; Helm chart versions are immutable once published. | |
| if: ${{ !cancelled() && needs.release.result == 'success' && inputs.tag == '' }} | |
| 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # 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 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 | |
| - uses: oras-project/setup-oras@f8710a5536385a773dd14a148cf030021e293aef # v2 (includes 1.3.2) | |
| with: | |
| version: "1.3.2" | |
| - name: Copy Helm chart to Docker Hub | |
| shell: bash -Eeuo pipefail -x {0} | |
| run: | | |
| # helm push appends the chart name, creating a 3-level path | |
| # (attuneio/attune-chart/attune) that Docker Hub rejects. | |
| # Use oras cp to copy from GHCR to the exact 2-level path. | |
| VERSION="${{ needs.release.outputs.tag }}" | |
| VERSION="${VERSION#v}" | |
| echo "${{ secrets.DOCKERHUB_TOKEN }}" | oras login registry-1.docker.io \ | |
| --username "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin | |
| oras cp \ | |
| ${{ env.REGISTRY }}/${{ github.repository_owner }}/charts/attune:${VERSION} \ | |
| registry-1.docker.io/attuneio/attune-chart:${VERSION} | |
| - 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} | |
| - 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 | |
| # 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] | |
| # Skip on re-releases; OLM bundles were already submitted for this version. | |
| if: ${{ !cancelled() && needs.release.result == 'success' && inputs.tag == '' }} | |
| 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 | |
| with: | |
| ref: ${{ needs.release.outputs.tag }} | |
| - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 | |
| with: | |
| go-version-file: go.mod | |
| 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 }} | |
| IMAGE_DIGEST: ${{ needs.release.outputs.digest }} | |
| 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] | |
| # Skip on re-releases; the README content at the re-released tag may be | |
| # older than what is currently on Docker Hub. | |
| if: ${{ !cancelled() && needs.release.result == 'success' && inputs.tag == '' }} | |
| steps: | |
| - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 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 |