Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions .github/workflows/chart-version-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# When a service release is published, open a pull request moving the charts
# that deploy it to that version.
#
# This is the first hop of the service to chart to stack cascade. The second is
# stack-pin-bump.yml: merging this pull request does not by itself move the
# stack, because a chart version only reaches the stack once the chart is
# released. Cutting that chart release stays a human decision, and publishing
# it is what triggers the stack bump.
#
# Which charts deploy the released service is declared, not derived; see
# tools/ci/chart-service-edge for why deriving it is wrong. A service whose
# charts have not declared the edge yet bumps nothing and says so.

name: chart version bump

on:
release:
types: [published]
# Manual entry point for re-running a release whose bump did not land, and
# for exercising the job without cutting a tag.
workflow_dispatch:
inputs:
tag:
description: >-
Service release tag, for example
src/control-plane-services/notary/v1.9.0
required: true

permissions:
contents: read

concurrency:
# One bump at a time. Several releases landing together refresh the same
# pull request rather than racing on the same files.
group: chart-version-bump
cancel-in-progress: false

jobs:
bump:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
# The default for a release event is the tagged commit, but the pull
# request targets the default branch. Bumping the tag's tree would
# carry whatever the chart looked like then onto a branch cut from
# today's main.
ref: ${{ github.event.repository.default_branch }}

- uses: actions/setup-go@v5
with:
# Derived from the anchor, never a literal: tools/ci/check-go-version
# fails any workflow that pins one.
go-version-file: tools/go-toolchain/go.mod

- name: Test the bumper
# The bumper rewrites version fields in shipped charts, so its tests run
# here rather than somewhere that might not be reached. A test that
# gates nothing is not a test.
run: go test -C tools/chart-version-bumper ./...

- name: Select the tag
id: tag
env:
# Through env like every other step here. A release tag is chosen by
# whoever pushes it, so expanding it into the script body is the
# standard Actions injection shape.
INPUT_TAG: ${{ github.event.inputs.tag }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
set -euo pipefail
tag="${INPUT_TAG:-${RELEASE_TAG}}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
# Chart releases move stack pins, which is stack-pin-bump.yml's job.
# Everything else is a service release and is this job's business:
# a tag that names no known service fails below rather than here, so
# a service missing from the release metadata is visible.
case "${tag}" in
deploy/helm/*/v*) echo "applies=false" >> "${GITHUB_OUTPUT}"
echo "${tag} is a chart release; stack-pin-bump.yml handles it" ;;
*/v*) echo "applies=true" >> "${GITHUB_OUTPUT}" ;;
*) echo "applies=false" >> "${GITHUB_OUTPUT}"
echo "${tag} is not a subtree release tag; nothing to do" ;;
esac
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Check the chart to service edges
if: steps.tag.outputs.applies == 'true'
# Report-only. Runs here so the undeclared charts are listed in the same
# log as a bump that reached fewer charts than someone expected.
run: tools/ci/chart-service-edge --audit

- name: Check out the bump branch
if: steps.tag.outputs.applies == 'true'
env:
BRANCH: chore/chart-version-bumps
run: |
set -euo pipefail
# The bump is applied ON the pull request branch, not on the default
# branch and moved across afterwards. Bumping first and stashing the
# result over a checkout collides whenever the branch already carries
# a bump for the same chart, and a swallowed stash conflict either
# drops that earlier bump or commits conflict markers. Starting here
# also makes the run idempotent: the bumper sees the current value and
# reports "already <version>".
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch origin "${BRANCH}" || true
if git rev-parse --verify -q "origin/${BRANCH}" >/dev/null; then
git checkout -B "${BRANCH}" "origin/${BRANCH}"
else
git checkout -B "${BRANCH}"
fi

- name: Apply the bump
id: bump
if: steps.tag.outputs.applies == 'true'
env:
# Through env, never expanded into the script body: a tag is chosen by
# whoever pushes it, and ${{ }} interpolation into a run: block is the
# standard Actions injection shape.
TAG: ${{ steps.tag.outputs.tag }}
run: |
set +e
set -uo pipefail
# +e, deliberately. GitHub invokes this as `bash -e`, and the bumper
# exits 3 when a chart's appVersion and image tag disagree even though
# it still applied every chart it could move safely. Aborting here
# would throw those away and leave the refusal as the only outcome.
tools/ci/chart-version-bumper \
--tag "${TAG}" --write 2>/tmp/refusals
status=$?
cat /tmp/refusals >&2
# The exit code, not whether stderr is empty. SystemExit writes its
# message to stderr too, so an unresolvable tag looks exactly like a
# refused chart there. 3 means the charts that could move did; any
# other non-zero means nothing moved and the run should stop.
if [ "${status}" -ne 0 ] && [ "${status}" -ne 3 ]; then
echo "bumper failed (exit ${status}); no chart was changed" >&2
exit "${status}"
fi
if [ "${status}" -eq 3 ]; then
{
echo "refused<<CHART_BUMP_REFUSALS"
cat /tmp/refusals
echo "CHART_BUMP_REFUSALS"
} >> "${GITHUB_OUTPUT}"
fi
# Scoped to the same paths the commit below stages. Repo-wide, any
# unrelated modification in the workspace would set changed=true and
# the commit would then abort with nothing staged.
if git diff --quiet -- deploy/helm; then
echo "changed=false" >> "${GITHUB_OUTPUT}"
echo "no chart moved"
else
echo "changed=true" >> "${GITHUB_OUTPUT}"
git --no-pager diff --stat -- deploy/helm
fi

- name: Open or refresh the pull request
if: steps.bump.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.NV_GITHUB_TOKEN || github.token }}
TAG: ${{ steps.tag.outputs.tag }}
REFUSED: ${{ steps.bump.outputs.refused }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
branch="chore/chart-version-bumps"

git add deploy/helm
# Separate -m flags rather than an embedded multi-line string: the
# continuation lines of one would have to sit at column zero, which
# ends the YAML block scalar this script lives in.
# fix, not chore. tools/ci/github-release feeds RELEASE_RULES to
# semantic-release, where chore carries "release": false. A chore
# commit therefore cuts no chart release, and since publishing a chart
# release is exactly what triggers stack-pin-bump.yml, the cascade
# would stop here: the chart would carry the new appVersion on main
# and the stack would never learn about it.
#
# A patch bump of the chart is the right size. The chart's own
# templates and values schema have not changed, only the application
# version it defaults to, which is the conventional reading of chart
# version against appVersion.
git commit \
-m "fix(charts): bump for ${TAG}" \
-m "Opened by the chart version bump workflow on release of ${TAG}."
git push --force-with-lease origin "${branch}"

notes=""
if [ -n "${REFUSED}" ]; then
notes="$(printf '%s\n' \
"" \
"Some charts were not bumped:" \
"" \
'```' \
"${REFUSED}" \
'```' \
"" \
"A chart is refused when its \`appVersion\` and image tag disagree, or when the tag is floating. Reconciling those two fields is a decision, so it is left to a person rather than resolved during an automated bump.")"
fi

body="$(printf '%s\n' \
"Opened by \`.github/workflows/chart-version-bump.yml\` when \`${TAG}\` was published." \
"" \
"The released tag carries the version, so this is a direct update rather than a lookup of the newest published image." \
"" \
"Merging this does not move the self-managed stack. A chart version reaches the stack only once the chart itself is released, and publishing that chart release is what triggers \`stack-pin-bump.yml\`." \
"" \
"Release notes: ${SERVER_URL}/${REPO}/releases/tag/${TAG}" \
"${notes}" \
"" \
"If this pull request sits unmerged, later service releases add their bumps to the same branch, so merging it applies all of them." \
"" \
"Github commit:" \
"fix(charts): bump chart versions for released services" \
"")"

# gh api, not `gh pr edit`. Against this repository `gh pr edit` fails
# with "Projects (classic) is being deprecated ...
# (repository.pullRequest.projectCards)", because it queries project
# cards it does not need. The REST endpoint has no such dependency.
number="$(gh api "repos/${REPO}/pulls?head=${REPO%%/*}:${branch}&state=open" -q '.[0].number')"
if [ -n "${number}" ] && [ "${number}" != "null" ]; then
jq -n --arg b "${body}" '{body: $b}' \
| gh api -X PATCH "repos/${REPO}/pulls/${number}" --input - >/dev/null
echo "refreshed pull request #${number}"
else
gh pr create --base main --head "${branch}" \
--title "fix(charts): bump chart versions for released services" \
--body "${body}"
fi

- name: Surface refusals
if: steps.bump.outputs.refused != ''
# Last, so it does not stop the safe bumps from being opened. A chart
# that wanted a bump and could not take one is a finding, and a green
# run would bury it.
env:
# Via env, not ${{ }} interpolation: expanding it into the script body
# is the standard Actions injection shape, even for text this
# repository produced.
REFUSED: ${{ steps.bump.outputs.refused }}
run: |
echo "::error::charts refused the bump:"
printf '%s\n' "${REFUSED}"
exit 1
2 changes: 2 additions & 0 deletions tools/chart-version-bumper/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# go build ./... drops the binary here; it must never be committed.
/chart-version-bumper
Loading
Loading