diff --git a/.github/workflows/publish-helm-oci.yaml b/.github/workflows/publish-helm-oci.yaml new file mode 100644 index 000000000..9f2a7ba9c --- /dev/null +++ b/.github/workflows/publish-helm-oci.yaml @@ -0,0 +1,297 @@ +# Publish csi-driver-nfs container images and Helm chart to GHCR. +# +# Triggers: +# - Automatically on GitHub Release publish (tag must match v*) +# - Manually via workflow_dispatch with a version input +# +# Publishes: +# - Multi-arch container image: ghcr.io/kubernetes-csi/nfsplugin: +# (linux/amd64, linux/arm64, linux/arm/v7, linux/ppc64le) +# - Helm chart (OCI): oci://ghcr.io/kubernetes-csi/csi-driver-nfs +# +name: Publish to GHCR + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., v4.13.2)' + required: true + type: string + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io/kubernetes-csi + IMAGE_NAME: nfsplugin + CHART_NAME: csi-driver-nfs + # GO_VERSION sourced from go.mod via go-version-file + +jobs: + # ── Validate version format early ────────────────────────────────────────── + validate: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + chart_version: ${{ steps.version.outputs.chart_version }} + steps: + - name: Determine and validate version + id: version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION="${{ github.event.release.tag_name }}" + fi + + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid version format: '${VERSION}'. Expected: v.. (e.g., v4.13.2)" + exit 1 + fi + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "chart_version=${VERSION#v}" >> "$GITHUB_OUTPUT" + echo "Version: ${VERSION}" + + # ── Build & push container images ────────────────────────────────────────── + build-linux: + runs-on: ubuntu-latest + needs: [validate] + strategy: + matrix: + include: + - goarch: amd64 + platform: linux/amd64 + suffix: linux-amd64 + - goarch: arm64 + platform: linux/arm64 + suffix: linux-arm64 + - goarch: ppc64le + platform: linux/ppc64le + suffix: linux-ppc64le + - goarch: arm + goarm: '7' + platform: linux/arm/v7 + suffix: linux-arm-v7 + steps: + - name: Checkout at version tag + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ needs.validate.outputs.version }} + + - name: Set up Go + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6 + with: + go-version-file: go.mod + + - name: Build binary + run: | + VERSION="${{ needs.validate.outputs.version }}" + if [[ "${{ matrix.goarch }}" == "arm" ]]; then + make nfs-armv7 IMAGE_VERSION="${VERSION}" PUBLISH=true + else + make nfs ARCH=${{ matrix.goarch }} IMAGE_VERSION="${VERSION}" PUBLISH=true + fi + + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3 + + - name: Login to GHCR + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push ${{ matrix.platform }} + run: | + VERSION="${{ needs.validate.outputs.version }}" + # Match Makefile convention: --build-arg ARCH=arm/v7 for armv7 + if [[ "${{ matrix.goarch }}" == "arm" ]]; then + DOCKER_ARCH="arm/v7" + else + DOCKER_ARCH="${{ matrix.goarch }}" + fi + GIT_COMMIT=$(git rev-parse HEAD) + docker buildx build --pull --push \ + --platform=${{ matrix.platform }} \ + --build-arg ARCH=${DOCKER_ARCH} \ + --provenance=false --sbom=false \ + --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}-${{ matrix.suffix }} \ + --label org.opencontainers.image.revision=${GIT_COMMIT} \ + --label org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} \ + . + + # ── Create multi-arch manifest ───────────────────────────────────────────── + manifest: + runs-on: ubuntu-latest + needs: [validate, build-linux] + steps: + - name: Login to GHCR + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create and push multi-arch manifest + env: + DOCKER_CLI_EXPERIMENTAL: enabled + run: | + VERSION="${{ needs.validate.outputs.version }}" + IMAGE="${REGISTRY}/${IMAGE_NAME}" + + docker manifest create --amend "${IMAGE}:${VERSION}" \ + "${IMAGE}:${VERSION}-linux-amd64" \ + "${IMAGE}:${VERSION}-linux-arm64" \ + "${IMAGE}:${VERSION}-linux-arm-v7" \ + "${IMAGE}:${VERSION}-linux-ppc64le" + + docker manifest push -p "${IMAGE}:${VERSION}" + echo "Pushed manifest: ${IMAGE}:${VERSION}" + + # ── Publish Helm chart as OCI (independent of image builds) ──────────────── + publish-helm: + runs-on: ubuntu-latest + needs: [validate] + outputs: + helm_chart_version: ${{ steps.verify_version.outputs.helm_chart_version }} + steps: + - name: Checkout at version tag + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + ref: ${{ needs.validate.outputs.version }} + + - name: Validate chart directory + id: chart + run: | + VERSION="${{ needs.validate.outputs.version }}" + CHART_DIR="charts/${VERSION}/${CHART_NAME}" + # Fallback: some repos store the chart under charts/latest/ + if [[ ! -f "${CHART_DIR}/Chart.yaml" ]]; then + CHART_DIR="charts/latest/${CHART_NAME}" + fi + if [[ ! -f "${CHART_DIR}/Chart.yaml" ]]; then + echo "::error::Chart not found for ${VERSION}" + echo "Searched: charts/${VERSION}/${CHART_NAME} and charts/latest/${CHART_NAME}" + echo "Available chart directories:" + ls -1 charts/ || echo " (none)" + exit 1 + fi + echo "chart_dir=${CHART_DIR}" >> "$GITHUB_OUTPUT" + + - name: Setup Helm + uses: azure/setup-helm@b9e51907a09c216f16ebe8536097933489208112 # v4 + with: + version: v3.17.0 + + - name: Lint chart + run: helm lint "${{ steps.chart.outputs.chart_dir }}" + + - name: Verify chart version matches + id: verify_version + run: | + YAML_VERSION=$(helm show chart "${{ steps.chart.outputs.chart_dir }}" | grep '^version:' | awk '{print $2}') + EXPECTED="${{ needs.validate.outputs.chart_version }}" + # Handle older charts that may have leading 'v' in Chart.yaml version + YAML_VERSION_STRIPPED="${YAML_VERSION#v}" + if [[ "$YAML_VERSION_STRIPPED" != "$EXPECTED" ]]; then + echo "::error::Chart.yaml version ($YAML_VERSION) != expected ($EXPECTED)" + exit 1 + fi + # Use the actual Chart.yaml version for helm commands + echo "helm_chart_version=${YAML_VERSION}" >> "$GITHUB_ENV" + echo "helm_chart_version=${YAML_VERSION}" >> "$GITHUB_OUTPUT" + + - name: Login to GHCR (Helm) + run: | + echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \ + --username "${{ github.actor }}" --password-stdin + + - name: Package and push chart + run: | + helm package "${{ steps.chart.outputs.chart_dir }}" --destination /tmp/helm-pkg + PACKAGE=$(ls /tmp/helm-pkg/${CHART_NAME}-*.tgz) + helm push "${PACKAGE}" "oci://${REGISTRY}" + + - name: Verify published chart + run: | + helm show chart "oci://${REGISTRY}/${CHART_NAME}" \ + --version "${{ steps.verify_version.outputs.helm_chart_version }}" + + # ── Ensure GHCR packages are public ───────────────────────────────────────── + set-visibility: + runs-on: ubuntu-latest + needs: [manifest, publish-helm] + if: needs.manifest.result == 'success' || needs.publish-helm.result == 'success' + steps: + - name: Set container package visibility to public + if: needs.manifest.result == 'success' + run: | + curl -s -X PATCH \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/orgs/${{ github.repository_owner }}/packages/container/${IMAGE_NAME}" \ + -d '{"visibility":"public"}' || echo "::warning::Failed to set container package visibility (may need manual setup)" + + - name: Set Helm chart package visibility to public + if: needs.publish-helm.result == 'success' + run: | + curl -s -X PATCH \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/orgs/${{ github.repository_owner }}/packages/container/${CHART_NAME}" \ + -d '{"visibility":"public"}' || echo "::warning::Failed to set Helm chart package visibility (may need manual setup)" + + # ── Summary ──────────────────────────────────────────────────────────────── + summary: + runs-on: ubuntu-latest + needs: [validate, manifest, publish-helm, set-visibility] + if: always() + steps: + - name: Job summary + env: + VERSION: ${{ needs.validate.outputs.version }} + CHART_VERSION: ${{ needs.publish-helm.outputs.helm_chart_version || needs.validate.outputs.chart_version }} + MANIFEST_RESULT: ${{ needs.manifest.result }} + HELM_RESULT: ${{ needs.publish-helm.result }} + shell: bash + run: | + FENCE='```' + echo "## Publish to GHCR — Results" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + if [[ "$MANIFEST_RESULT" == "success" ]]; then + { + echo "### ✅ Container Image" + echo "| Image | Tag |" + echo "|-------|-----|" + echo "| ${REGISTRY}/${IMAGE_NAME} | ${VERSION} (linux/amd64, arm64, arm/v7, ppc64le) |" + echo "" + echo "${FENCE}bash" + echo "docker pull ${REGISTRY}/${IMAGE_NAME}:${VERSION}" + echo "${FENCE}" + } >> "$GITHUB_STEP_SUMMARY" + else + echo "### ❌ Container Image — ${MANIFEST_RESULT}" >> "$GITHUB_STEP_SUMMARY" + fi + + echo "" >> "$GITHUB_STEP_SUMMARY" + + if [[ "$HELM_RESULT" == "success" ]]; then + { + echo "### ✅ Helm Chart (OCI)" + echo "${FENCE}bash" + echo "helm install csi-driver-nfs oci://${REGISTRY}/${CHART_NAME} --version ${CHART_VERSION} -n kube-system" + echo "${FENCE}" + } >> "$GITHUB_STEP_SUMMARY" + else + echo "### ❌ Helm Chart — ${HELM_RESULT}" >> "$GITHUB_STEP_SUMMARY" + fi