Skip to content

Create Commercial Release Branch #23

Create Commercial Release Branch

Create Commercial Release Branch #23

name: Create Commercial Release Branch
on:
workflow_dispatch:
inputs:
project:
description: 'Spring Cloud GitHub project name (e.g. spring-cloud-config-commercial)'
required: true
type: string
branch:
description: 'Source branch to create the release branch from (e.g. main, 4.2.x)'
required: true
type: string
release-train:
description: 'Spring release train to join (e.g. 2026.09). Supplying it joins that train once the branch is prepared; leave it empty to prepare the branch without joining.'
required: false
type: string
default: ''
token:
description: 'GitHub token with access to the project repos. Falls back to GH_ACTIONS_REPO_TOKEN.'
required: false
type: string
default: ''
workflow_call:
inputs:
project:
description: 'Spring Cloud GitHub project name (e.g. spring-cloud-config-commercial)'
required: true
type: string
branch:
description: 'Source branch to create the release branch from (e.g. main, 4.2.x)'
required: true
type: string
release-train:
description: 'Spring release train to join (e.g. 2025.09). Supplying it joins that train once the branch is prepared; leave it empty to prepare the branch without joining.'
required: false
type: string
default: ''
secrets:
token:
description: 'GitHub token with access to the project repos. Falls back to GH_ACTIONS_REPO_TOKEN.'
required: false
permissions:
contents: read
jobs:
join-release-train:
name: Join Release Train - ${{ inputs.project }} (${{ inputs.branch }})
runs-on: ubuntu-latest
steps:
- name: Checkout spring-cloud-github-actions
uses: actions/checkout@v4
with:
token: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Check if commercial repository
id: is-commercial
uses: ./.github/actions/is-commercial-repo
with:
repository: spring-cloud/${{ inputs.project }}
# Every release - commercial, OSS and hotfix alike - is cut from the commercial repo,
# so this workflow only ever operates on a -commercial project. Reject anything else
# up front: an OSS name would otherwise be taken for a commercial repo and the release
# branch cut in the OSS repository itself.
- name: Reject non-commercial project
if: steps.is-commercial.outputs.commercial != 'true'
env:
PROJECT: ${{ inputs.project }}
run: |
echo "ERROR: '${PROJECT}' is not a commercial repository."
echo
echo "This workflow releases from spring-cloud/<project>-commercial, where every"
echo "release - commercial, OSS and hotfix alike - is cut."
echo
echo " - For a commercial release, pass '${PROJECT}-commercial'."
echo " - For an OSS release, use create-oss-release-branch.yml instead, which"
echo " creates the -internal branch and stamps -INTERNAL-SNAPSHOT versions"
echo " before cutting the release branch. This workflow does neither."
exit 1
- name: Determine release version and commercial project
id: setup
env:
GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
echo "Reading pom.xml from spring-cloud/${{ inputs.project }} at ${{ inputs.branch }}..."
pom=$(gh api "repos/spring-cloud/${{ inputs.project }}/contents/pom.xml?ref=${{ inputs.branch }}" \
--jq '.content' | tr -d '\n' | base64 -d)
echo "$pom" > /tmp/pom.xml
version=$(node -e "
const fs = require('fs');
const data = fs.readFileSync('/tmp/pom.xml', 'utf8');
const noParent = data.replace(/<parent>[\s\S]*?<\/parent>/g, '');
const m = noParent.match(/<version>([^<]+)<\/version>/);
if (!m) { process.stderr.write('No <version> found in pom.xml\n'); process.exit(1); }
process.stdout.write(m[1].replace(/-SNAPSHOT\$/, '') + '\n');
")
echo "release-version=$version" >> $GITHUB_OUTPUT
echo "Release version: $version"
# The source is a -commercial project, verified above, so the destination is
# always Spring Enterprise.
commercial_project="${{ inputs.project }}"
echo "commercial-project=$commercial_project" >> $GITHUB_OUTPUT
echo "Commercial project: $commercial_project"
- name: Check tag and release do not already exist
env:
GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
version="${{ steps.setup.outputs.release-version }}"
tag="v${version}"
commercial_project="${{ steps.setup.outputs.commercial-project }}"
# The OSS counterpart, checked alongside the commercial repo so a version already
# released on either side is caught. Commercial-only projects have no OSS repo of
# this name; the lookups simply come back empty, which reads as "not released".
oss_project="${{ steps.is-commercial.outputs.base-repo-name }}"
failed=false
for repo in "spring-cloud/${oss_project}" "spring-cloud/${commercial_project}"; do
echo "Checking ${repo} for existing tag/release ${tag}..."
if gh api "repos/${repo}/git/refs/tags/${tag}" --silent 2>/dev/null; then
echo "ERROR: Tag '${tag}' already exists in ${repo}"
failed=true
else
echo " Tag '${tag}' not found in ${repo} ✓"
fi
if gh api "repos/${repo}/releases/tags/${tag}" --silent 2>/dev/null; then
echo "ERROR: Release '${tag}' already exists in ${repo}"
failed=true
else
echo " Release '${tag}' not found in ${repo} ✓"
fi
done
if [[ "$failed" == "true" ]]; then
echo "Aborting: ${tag} already exists — version ${version} has already been released."
exit 1
fi
echo "Check passed: ${tag} does not exist in any repo."
# The release branch inherits its workflows from the source branch, and the
# final step of this job dispatches release-train-join.yml on it. Verify the
# release train workflows are on the source branch now, before anything is
# created, so a missing file fails fast instead of leaving a half-created
# release branch and a projects.json entry to clean up by hand.
- name: Verify release train workflows exist on source branch
uses: ./.github/actions/check-release-train-workflows
with:
repo: spring-cloud/${{ inputs.project }}
branch: ${{ inputs.branch }}
token: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
fail-on-missing: 'true'
- name: Create milestone in source repo if missing
uses: ./.github/actions/create-milestone
with:
repo: spring-cloud/${{ inputs.project }}
version: ${{ steps.setup.outputs.release-version }}
token: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Create release branch in commercial repo
env:
GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
commercial_project="${{ steps.setup.outputs.commercial-project }}"
release_branch="release/${{ steps.setup.outputs.release-version }}"
# Source and destination are the same repo, so the branch is cut from the source
# branch's SHA over the API - no clone needed.
echo "Creating ${release_branch} in spring-cloud/${commercial_project} from ${{ inputs.branch }}..."
source_sha=$(gh api "repos/spring-cloud/${commercial_project}/git/ref/heads/${{ inputs.branch }}" \
--jq '.object.sha')
gh api "repos/spring-cloud/${commercial_project}/git/refs" \
--method POST \
--field ref="refs/heads/${release_branch}" \
--field sha="${source_sha}"
echo "Branch ${release_branch} created successfully."
- name: Update CI and PR workflows for release branch
uses: ./.github/actions/update-oss-workflows-to-commercial
with:
repository: spring-cloud/${{ steps.setup.outputs.commercial-project }}
branch: release/${{ steps.setup.outputs.release-version }}
token: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Update projects.json for release branch
uses: ./.github/actions/update-projects-json
with:
oss-repo: spring-cloud/${{ inputs.project }}
oss-branch: ${{ inputs.branch }}
commercial-branch: release/${{ steps.setup.outputs.release-version }}
token: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
# Dispatch release-train-join.yml in the commercial repo and wait for it, when a
# release-train is supplied - leaving that input empty prepares the branch and stops
# short of joining.
#
# inputs['release-train'] rather than inputs.release-train: a hyphen in a dot-path is
# ambiguous to the expression parser, and getting it wrong here would silently
# evaluate falsy and skip the join rather than fail visibly.
- name: Trigger release-train-join workflow and wait
if: ${{ inputs['release-train'] != '' }}
env:
GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
commercial_project="${{ steps.setup.outputs.commercial-project }}"
release_branch="release/${{ steps.setup.outputs.release-version }}"
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.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