Skip to content

Create Hotfix Branch #290

Create Hotfix Branch

Create Hotfix Branch #290

name: Create Hotfix Branch
# Creates a commercial hotfix release branch directly from an OSS tag.
#
# The commercial repo is always the OSS repo with -commercial appended.
# The release branch is named release/<version>.1, derived from the tag by
# stripping the leading 'v' and appending '.1' (e.g. v5.0.1 -> release/5.0.1.1).
#
# After the branch is initialised, the project version is stamped to
# <current-version>.1-SNAPSHOT (e.g. 5.0.1 -> 5.0.1.1-SNAPSHOT).
# Optionally, dependency versions can be updated from a release train or an
# explicit project-version override can be supplied.
# Finally, release-train-join.yml is triggered in the commercial repo.
on:
workflow_dispatch:
inputs:
oss_repo:
description: 'Open source repository name in the spring-cloud org (e.g. spring-cloud-stream)'
required: true
type: string
oss_tag:
description: 'OSS tag to create the hotfix branch from (e.g. v5.0.1)'
required: true
type: string
spring_release_train:
description: 'Spring release train this hotfix is part of (e.g. 2026.1)'
required: true
type: string
project_version:
description: 'Override the auto-computed hotfix project version (default: <current-version>.1-SNAPSHOT)'
required: false
type: string
default: ''
release_train_version:
description: 'Release train version (e.g. 2025.0.1) — when supplied, dependency versions are updated from the Spring Cloud release train'
required: false
type: string
default: ''
versions:
description: 'JSON map of dependency versions to apply (e.g. {"spring-boot":"3.3.0","spring-cloud-commons":"4.1.1"}). Mutually exclusive with release_train_version.'
required: false
type: string
default: ''
sha:
description: 'Commit SHA of this repo to copy release-train action files from. Defaults to the commit that triggered this workflow.'
required: false
type: string
default: ''
workflow_call:
inputs:
oss_repo:
description: 'Open source repository name in the spring-cloud org (e.g. spring-cloud-stream)'
required: true
type: string
oss_tag:
description: 'OSS tag to create the hotfix branch from (e.g. v5.0.1)'
required: true
type: string
spring_release_train:
description: 'Spring release train this hotfix is part of (e.g. 2026.1)'
required: true
type: string
project_version:
description: 'Override the auto-computed hotfix project version (default: <current-version>.1-SNAPSHOT)'
required: false
type: string
default: ''
release_train_version:
description: 'Release train version (e.g. 2025.0.1) — when supplied, dependency versions are updated from the Spring Cloud release train'
required: false
type: string
default: ''
versions:
description: 'JSON map of dependency versions to apply (e.g. {"spring-boot":"3.3.0"}). Mutually exclusive with release_train_version.'
required: false
type: string
default: ''
sha:
description: 'Commit SHA of this repo to copy release-train action files from. Defaults to the commit that triggered this workflow.'
required: false
type: string
default: ''
secrets:
token:
description: 'GitHub token with contents: write on the commercial repo. Falls back to the GH_ACTIONS_REPO_TOKEN organization secret.'
required: false
permissions:
contents: read
jobs:
# Strip the leading 'v' from the tag, append '.1', and prefix with 'release/'
# to form the commercial release branch name (e.g. v5.0.1 -> release/5.0.1.1).
# GitHub Actions expressions have no replace/substring function so this needs
# a shell step.
derive:
runs-on: ubuntu-latest
outputs:
commercial_repo: ${{ steps.derive.outputs.commercial_repo }}
commercial_branch: ${{ steps.derive.outputs.commercial_branch }}
commercial_project: ${{ steps.derive.outputs.commercial_project }}
commercial_version: ${{ steps.derive.outputs.commercial_version }}
steps:
- name: Derive commercial repo and branch from tag
id: derive
shell: bash
run: |
OSS_REPO="spring-cloud/${{ inputs.oss_repo }}"
OSS_TAG="${{ inputs.oss_tag }}"
COMMERCIAL_REPO="${OSS_REPO}-commercial"
COMMERCIAL_PROJECT="${COMMERCIAL_REPO#spring-cloud/}"
# strip leading 'v', append '.1', prefix with 'release/' (v5.0.1 -> release/5.0.1.1)
COMMERCIAL_BRANCH="release/${OSS_TAG#v}.1"
COMMERCIAL_VERSION="${COMMERCIAL_BRANCH#release/}"
echo "commercial_repo=${COMMERCIAL_REPO}" >> "$GITHUB_OUTPUT"
echo "commercial_branch=${COMMERCIAL_BRANCH}" >> "$GITHUB_OUTPUT"
echo "commercial_project=${COMMERCIAL_PROJECT}" >> "$GITHUB_OUTPUT"
echo "commercial_version=${COMMERCIAL_VERSION}" >> "$GITHUB_OUTPUT"
echo "OSS repo: $OSS_REPO"
echo "OSS tag: $OSS_TAG"
echo "Commercial repo: $COMMERCIAL_REPO"
echo "Commercial project: $COMMERCIAL_PROJECT"
echo "Commercial branch: $COMMERCIAL_BRANCH"
initialize:
needs: derive
uses: ./.github/workflows/initialize-commercial-branch.yml
with:
oss_repo: ${{ format('spring-cloud/{0}', inputs.oss_repo) }}
oss_tag: ${{ inputs.oss_tag }}
commercial_repo: ${{ needs.derive.outputs.commercial_repo }}
commercial_branch: ${{ needs.derive.outputs.commercial_branch }}
set_default_branch: false
secrets: inherit
update-versions:
needs: [derive, initialize]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Clone commercial branch and compute hotfix version
id: setup
shell: bash
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
COMMERCIAL_REPO="${{ needs.derive.outputs.commercial_repo }}"
COMMERCIAL_BRANCH="${{ needs.derive.outputs.commercial_branch }}"
git clone --single-branch --branch "$COMMERCIAL_BRANCH" \
"https://x-access-token:${GH_TOKEN}@github.com/${COMMERCIAL_REPO}.git" _version_repo
git -C _version_repo config user.name "Spring Builds"
git -C _version_repo config user.email "svc.spring-builds@broadcom.com"
# Read the project version from the root pom.xml using Python for
# reliable XML parsing (handles optional namespace prefixes).
CURRENT_VERSION=$(python3 - <<'PYEOF'
import xml.etree.ElementTree as ET, sys
tree = ET.parse('_version_repo/pom.xml')
root = tree.getroot()
ns = (root.tag.split('}')[0] + '}') if '}' in root.tag else ''
v = root.find(f'{ns}version')
print(v.text.strip() if v is not None else '', end='')
PYEOF
)
if [[ -z "$CURRENT_VERSION" ]]; then
echo "ERROR: could not read project version from _version_repo/pom.xml"
exit 1
fi
AUTO_VERSION="${CURRENT_VERSION}.1-SNAPSHOT"
EFFECTIVE_VERSION="${{ inputs.project_version }}"
if [[ -z "$EFFECTIVE_VERSION" ]]; then
EFFECTIVE_VERSION="$AUTO_VERSION"
fi
echo "current_version=${CURRENT_VERSION}" >> "$GITHUB_OUTPUT"
echo "effective_version=${EFFECTIVE_VERSION}" >> "$GITHUB_OUTPUT"
echo "Current version: $CURRENT_VERSION"
echo "Auto version: $AUTO_VERSION"
echo "Effective version: $EFFECTIVE_VERSION"
# When release-train-version is provided, update dependency version
# properties from the release train first. This also stamps the project
# version to the release train value; the next step overwrites that with
# the correct hotfix snapshot.
- name: Update dependency versions from release train
if: ${{ inputs.release_train_version != '' }}
uses: ./.github/actions/update-project-versions
with:
release-train-version: ${{ inputs.release_train_version }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
directory: _version_repo
commercial: 'true' # use release config from commercial repo
# Always stamp the project version to the hotfix snapshot (or the
# explicit override). When versions is supplied it also updates
# dependency properties in the same pass; otherwise versions:'{}' ensures
# only the project version is touched.
- name: Stamp hotfix project version
uses: ./.github/actions/update-project-versions
with:
project-version: ${{ steps.setup.outputs.effective_version }}
versions: ${{ inputs.versions || '{}' }}
directory: _version_repo
- name: Commit and push version updates
shell: bash
run: |
cd _version_repo
git add .
if git diff --cached --quiet; then
echo "No version changes to commit."
exit 0
fi
git commit -m "Updating project version to ${{ steps.setup.outputs.effective_version }} [skip actions]"
git push origin "${{ needs.derive.outputs.commercial_branch }}"
echo "Version updates committed."
create-milestone:
needs: derive
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create milestone in commercial repo if missing
uses: ./.github/actions/create-milestone
with:
repo: ${{ needs.derive.outputs.commercial_repo }}
version: ${{ needs.derive.outputs.commercial_version }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
ensure-workflows:
needs: [derive, update-versions]
runs-on: ubuntu-latest
steps:
- name: Check for required release train workflows
id: check
shell: bash
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
commercial_project="${{ needs.derive.outputs.commercial_project }}"
release_branch="${{ needs.derive.outputs.commercial_branch }}"
missing=false
for workflow in "release-train-join.yml" "release-train-ready.yml"; do
if gh api "repos/spring-cloud/${commercial_project}/contents/.github/workflows/${workflow}?ref=${release_branch}" --silent 2>/dev/null; then
echo " ✓ ${workflow} present"
else
echo " ✗ ${workflow} missing"
missing=true
fi
done
echo "needs-generation=${missing}" >> "$GITHUB_OUTPUT"
# Look up primary JDK from projects.json.
# initialize-commercial-branch already ran update-projects-json, which added
# commercial.jdkVersions[release_branch] sourced from oss.jdkVersions['X.Y.x'].
# We look up by the commercial branch name directly, matching how the generator
# queries scheduled branches.
project_key="${commercial_project%-commercial}"
echo "JDK lookup: commercial.jdkVersions['${release_branch}'] in project '${project_key}'"
gh api repos/spring-cloud/spring-cloud-github-actions/contents/config/projects.json \
-X GET -f ref=main --jq '.content' | base64 -d > /tmp/projects.json
export PROJECT_KEY="${project_key}"
export COMMERCIAL_BRANCH="${release_branch}"
primary_jdk=$(node - << 'JSEOF'
const fs = require('fs');
const projectKey = process.env.PROJECT_KEY;
const commercialBranch = process.env.COMMERCIAL_BRANCH;
const projects = JSON.parse(fs.readFileSync('/tmp/projects.json', 'utf8'));
const defaults = projects.defaults || {};
function getJdks(pk, type, branch) {
const cfg = (projects[pk] || {})[type] || {};
const jdkmap = cfg.jdkVersions || {};
if (jdkmap[branch]) return jdkmap[branch];
if (jdkmap['default']) {
process.stderr.write(`WARNING: '${branch}' not found in projects.json commercial.jdkVersions for '${pk}' — falling back to commercial default: ${JSON.stringify(jdkmap['default'])}\n`);
return jdkmap['default'];
}
const defMap = (defaults[type] || {}).jdkVersions || {};
if (defMap[branch]) return defMap[branch];
const globalFallback = defMap['default'] || ['17', '21', '25'];
process.stderr.write(`WARNING: '${branch}' not found in projects.json for '${pk}' — falling back to global default: ${JSON.stringify(globalFallback)}\n`);
return globalFallback;
}
const jdks = getJdks(projectKey, 'commercial', commercialBranch);
process.stdout.write(jdks.includes('8') ? '8' : '17');
JSEOF
)
echo "primary-jdk=${primary_jdk}" >> "$GITHUB_OUTPUT"
if [[ "$missing" == "false" ]]; then
echo "All required workflows are present — skipping generator."
else
echo "One or more required workflows are missing — generator will run (primary JDK: ${primary_jdk})."
fi
- name: Checkout
if: steps.check.outputs.needs-generation == 'true'
uses: actions/checkout@v4
with:
ref: ${{ inputs.sha || github.sha }}
- name: Run workflow generator for hotfix branch
if: steps.check.outputs.needs-generation == 'true'
uses: ./.github/actions/generate-workflows-for-branch
with:
repo: ${{ needs.derive.outputs.commercial_repo }}
branch: ${{ needs.derive.outputs.commercial_branch }}
primary-jdk: ${{ steps.check.outputs.primary-jdk }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
trigger-release-train-join:
needs: [derive, ensure-workflows, create-milestone]
runs-on: ubuntu-latest
steps:
- name: Trigger release-train-join workflow and wait
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
shell: bash
run: |
commercial_project="${{ needs.derive.outputs.commercial_project }}"
release_branch="${{ needs.derive.outputs.commercial_branch }}"
echo "Triggering release-train-join.yml in spring-cloud/${commercial_project} on ${release_branch}..."
run_url=$(gh workflow run release-train-join.yml \
--repo "spring-cloud/${commercial_project}" \
--ref "${release_branch}" \
--field "deployment-destination=Spring Enterprise" \
--field "release-train=${{ inputs.spring_release_train }}" \
--field "release-train-repository=spring-io/release-train")
echo "Dispatched workflow run. Waiting for $run_url to complete."
run_id=${run_url##*/}
watch_exit_code=0
gh run watch $run_id --repo "spring-cloud/${commercial_project}" --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
if [[ $watch_exit_code -eq 0 ]]; then
echo "Workflow run succeeded."
else
echo "Workflow run failed."
fi
exit $watch_exit_code
trigger-ci:
needs: [derive, trigger-release-train-join]
runs-on: ubuntu-latest
steps:
- name: Push empty commit to trigger CI
shell: bash
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
COMMERCIAL_REPO="${{ needs.derive.outputs.commercial_repo }}"
BRANCH="${{ needs.derive.outputs.commercial_branch }}"
git clone --single-branch --branch "$BRANCH" \
"https://x-access-token:${GH_TOKEN}@github.com/${COMMERCIAL_REPO}.git" _trigger_repo
cd _trigger_repo
git config user.name "Spring Builds"
git config user.email "svc.spring-builds@broadcom.com"
git commit --allow-empty -m "Initialize hotfix branch"
git push origin "$BRANCH"
echo "Empty commit pushed to trigger CI on ${COMMERCIAL_REPO}@${BRANCH}."