Skip to content

Merge pull request #256 from NVIDIA/feat/cherry-pick #17

Merge pull request #256 from NVIDIA/feat/cherry-pick

Merge pull request #256 from NVIDIA/feat/cherry-pick #17

Workflow file for this run

# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Create GitHub Release
# Triggered on tag pushes for operator, agent, and chart components.
# CLI releases are handled separately by cli-release.yaml (which also builds binaries).
#
# Tags follow the pattern <component>/v<version> (e.g. operator/v0.14.0).
# Release notes are generated by git-cliff scoped to the tagged component.
# Preview locally with: make changelog-preview COMPONENT=<name>
on:
push:
tags:
- 'operator/**'
- 'agent/**'
- 'chart/**'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract component name from tag
id: extract
run: |
COMPONENT=$(echo "${{ github.ref_name }}" | cut -f 1 -d /)
echo "component=${COMPONENT}" >> $GITHUB_OUTPUT
- name: Install git-cliff
run: |
GC_VERSION=$(curl -sSf https://api.github.com/repos/orhun/git-cliff/releases/latest | grep '"tag_name"' | cut -d '"' -f 4)
curl -sSL "https://github.com/orhun/git-cliff/releases/download/${GC_VERSION}/git-cliff-${GC_VERSION#v}-x86_64-unknown-linux-gnu.tar.gz" \
| tar xz --wildcards "*/git-cliff" --strip-components=1
sudo mv git-cliff /usr/local/bin/
- name: Generate release notes
id: release_notes
run: |
COMPONENT="${{ steps.extract.outputs.component }}"
INCLUDE_PATH="${COMPONENT}/**"
# CLI code lives under operator/cmd/cli/ but is handled by cli-release.yaml
NOTES=$(git cliff \
--include-path "${INCLUDE_PATH}" \
--tag-pattern "${COMPONENT}/.*" \
--latest \
--strip all)
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "${NOTES}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: operator/go.mod
cache: false
- name: Set up Python
uses: actions/setup-python@v6
with:
# Pinned to match agent-ci.yaml PYTHON_VERSION so notices generation
# uses the same interpreter as the agent build.
python-version: '3.13'
- name: Install go-licenses
run: make -C operator go-licenses
- name: Regenerate third-party notices
run: make notices
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Idempotent: skip create if the release already exists so workflow
# re-runs on the same tag can still reach the upload step below.
run: |
PRERELEASE_FLAG=""
# Release candidate tags (e.g. operator/v0.16.0-rc1) → mark as
# prerelease so they don't become "Latest" on the Releases page.
# Only -rc<N> is accepted; any other suffix after the version is
# rejected to keep the tag format predictable.
VERSION="${GITHUB_REF_NAME#*/}"
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
: # plain release
elif [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+$ ]]; then
PRERELEASE_FLAG="--prerelease"
else
echo "ERROR: tag '${GITHUB_REF_NAME}' does not match v<MAJOR>.<MINOR>.<PATCH> or v<MAJOR>.<MINOR>.<PATCH>-rc<N>" >&2
exit 1
fi
if ! gh release view "${{ github.ref_name }}" >/dev/null 2>&1; then
gh release create "${{ github.ref_name }}" \
--title "${{ github.ref_name }}" \
--notes "${{ steps.release_notes.outputs.notes }}" \
${PRERELEASE_FLAG}
fi
- name: Upload third-party notices to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
COMPONENT="${{ steps.extract.outputs.component }}"
case "${COMPONENT}" in
operator) NOTICES_FILE="operator/THIRD_PARTY_NOTICES.md" ;;
agent) NOTICES_FILE="agent/THIRD_PARTY_NOTICES.md" ;;
chart) NOTICES_FILE="THIRD_PARTY_NOTICES.md" ;;
*)
echo "ERROR: unknown component '${COMPONENT}' — no notices file mapping." >&2
exit 1
;;
esac
gh release upload "${{ github.ref_name }}" "${NOTICES_FILE}" --clobber
publish-chart:
if: startsWith(github.ref_name, 'chart/')
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: v3.16.2
- name: Verify chart version matches tag
id: chart-metadata
run: |
CHART_NAME=$(yq '.name' chart/Chart.yaml)
TAG_VERSION="${GITHUB_REF_NAME#chart/}"
CHART_VERSION=$(yq '.version' chart/Chart.yaml)
if [ "${TAG_VERSION}" != "${CHART_VERSION}" ]; then
echo "Tag version ${TAG_VERSION} does not match chart/Chart.yaml version ${CHART_VERSION}" >&2
exit 1
fi
echo "name=${CHART_NAME}" >> "${GITHUB_OUTPUT}"
echo "version=${CHART_VERSION}" >> "${GITHUB_OUTPUT}"
- name: Package chart
run: |
mkdir -p dist
helm package chart/ --destination dist/
- name: Log in to ghcr.io
run: |
echo "${{ secrets.GITHUB_TOKEN }}" \
| helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin
# cosign reads ~/.docker/config.json; helm registry login writes its own
# config so cosign can't see those creds. Authenticate docker too so the
# downstream sign/attest steps can upload the .sig and SBOM layers.
- name: Log in to ghcr.io (docker, for cosign)
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push chart to ghcr.io
id: push-chart
env:
CHART_NAME: ${{ steps.chart-metadata.outputs.name }}
CHART_VERSION: ${{ steps.chart-metadata.outputs.version }}
run: |
set -euo pipefail
chart_repo="ghcr.io/nvidia/nodewright/charts"
chart_subject="${chart_repo}/${CHART_NAME}"
# `helm push` writes "Pushed:" and "Digest:" to stderr (helm 3.16+),
# so capture both streams or awk sees an empty string.
push_output="$(helm push "dist/${CHART_NAME}-${CHART_VERSION}.tgz" "oci://${chart_repo}" 2>&1)"
printf '%s\n' "${push_output}"
chart_digest="$(awk '/^Digest:/ {print $2}' <<< "${push_output}")"
if ! [[ "${chart_digest}" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "::error::failed to parse Helm chart digest from helm push output"
exit 1
fi
echo "subject-name=${chart_subject}" >> "${GITHUB_OUTPUT}"
echo "digest=${chart_digest}" >> "${GITHUB_OUTPUT}"
- name: Sign GHCR Helm chart and attach SBOM
uses: ./.github/actions/cosign-sign-sbom
with:
subject-name: ${{ steps.push-chart.outputs.subject-name }}
subject-digest: ${{ steps.push-chart.outputs.digest }}
sbom-source: chart
- name: Attest GHCR Helm chart provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ${{ steps.push-chart.outputs.subject-name }}
subject-digest: ${{ steps.push-chart.outputs.digest }}
push-to-registry: true
- name: Verify GHCR Helm chart signature and attestations
uses: ./.github/actions/cosign-verify-release
with:
subject-name: ${{ steps.push-chart.outputs.subject-name }}
subject-digest: ${{ steps.push-chart.outputs.digest }}
certificate-identity-regexp: ^https://github.com/${{ github.repository }}/\.github/workflows/release\.yml@refs/tags/chart/.*$