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
180 changes: 180 additions & 0 deletions .github/workflows/stack-pin-bump.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# When a chart release is published, open a pull request moving the
# self-managed stack's pin to that version.
#
# The released tag carries the version, so there is no "newest version" lookup
# and none of the ordering questions that come with one. A tag of
# deploy/helm/nats/v0.8.0 states the answer.
#
# The failure this is built to avoid is silence. A chart releases, nothing in
# the stack resolves to it, no pin moves, and the run goes green. The resolver
# therefore enumerates every release in the stack and treats one it cannot
# resolve as an error, so a gap shows up as a red run rather than as nothing.

name: stack pin 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: Chart release tag, for example deploy/helm/nats/v0.8.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 file.
group: stack-pin-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. Pinning against the tag's tree
# would carry whatever the stack 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 resolver
# The resolver decides which line in the shipped stack gets rewritten,
# 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/stack-pin-resolver ./...

- name: Select the tag
id: tag
run: |
set -euo pipefail
tag="${{ github.event.inputs.tag || github.event.release.tag_name }}"
echo "tag=${tag}" >> "${GITHUB_OUTPUT}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Only chart releases move stack pins. Everything else is a normal
# release and is not this job's business.
case "${tag}" in
deploy/helm/*/v*) echo "applies=true" >> "${GITHUB_OUTPUT}" ;;
*) echo "applies=false" >> "${GITHUB_OUTPUT}"
echo "${tag} is not a chart release; nothing to do" ;;
esac

- name: Audit the stack
if: steps.tag.outputs.applies == 'true'
# Runs before the edit so an unresolvable release fails the job with a
# name attached, rather than being quietly skipped over.
run: tools/ci/stack-pin-resolver --audit

- name: Check out the bump branch
if: steps.tag.outputs.applies == 'true'
env:
BRANCH: chore/stack-pin-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 pin, and a swallowed stash conflict either drops
# that earlier bump or commits conflict markers. Starting here also
# makes the tool idempotent: it sees the existing 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 -euo pipefail
tools/ci/stack-pin-resolver --tag "${TAG}" --write
# 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/stacks/self-managed/helmfile.d; then
echo "changed=false" >> "${GITHUB_OUTPUT}"
echo "stack already pins this version"
else
echo "changed=true" >> "${GITHUB_OUTPUT}"
git --no-pager diff --stat -- deploy/stacks/self-managed/helmfile.d
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 }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
branch="chore/stack-pin-bumps"

git add deploy/stacks/self-managed/helmfile.d
# 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. deploy/stacks/self-managed is itself a release
# subproject, and tools/ci/github-release feeds RELEASE_RULES to
# semantic-release, where chore carries "release": false. A chore
# commit would move the pin on main without ever cutting a stack
# release, so nothing downstream would see the new pin.
git commit \
-m "fix(stack): pin ${TAG#deploy/helm/}" \
-m "Opened by the stack pin bump workflow on release of ${TAG}."
git push --force-with-lease origin "${branch}"

body="$(printf '%s\n' \
"Opened by \`.github/workflows/stack-pin-bump.yml\` when \`${TAG}\` was published." \
"" \
"The released tag carries the version, so this is a direct pin update rather than a lookup of the newest published chart." \
"" \
"Release notes: ${SERVER_URL}/${REPO}/releases/tag/${TAG}" \
"" \
"If this pull request sits unmerged, later chart releases add their bumps to the same branch, so merging it applies all of them." \
"" \
"Github commit:" \
"fix(stack): pin ${TAG#deploy/helm/}" \
"")"

# 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(stack): bump self-managed stack chart pins" \
--body "${body}"
fi
34 changes: 34 additions & 0 deletions tools/ci/stack-pin-resolver
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Stable CI entrypoint for the Go tool in tools/stack-pin-resolver.
#
# The wrapper exists for two reasons.
#
# The repository root. `go run -C <dir>` leaves the process running with that
# directory as its working directory, so the tool cannot find the helmfiles or
# the release metadata on its own. Resolving the root from this script's own
# location means callers do not have to pass it.
#
# The exit code. `go run` does NOT propagate the program's status: it prints
# "exit status N" and exits 1, collapsing every non-zero code into one. This
# tool only uses 0 and 1 today, so nothing is lost yet, but building the binary
# keeps that from becoming a trap the first time a distinct code is added.
#
# Run the tests with: go test -C tools/stack-pin-resolver ./...
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
bin_dir="$(mktemp -d)"
trap 'rm -rf "${bin_dir}"' EXIT

go build -C "${repo_root}/tools/stack-pin-resolver" -o "${bin_dir}/stack-pin-resolver" .

# Not exec, so the trap above still runs, and not under errexit, so the exit
# code reaches the caller rather than aborting the shell first.
set +e
"${bin_dir}/stack-pin-resolver" --root "${repo_root}" "$@"
status=$?
set -e
exit "${status}"
2 changes: 2 additions & 0 deletions tools/stack-pin-resolver/.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.
/stack-pin-resolver
3 changes: 3 additions & 0 deletions tools/stack-pin-resolver/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module stack-pin-resolver

go 1.26
Loading
Loading