Skip to content

Join Release Train

Join Release Train #5

name: Join Release Train
on:
workflow_dispatch:
inputs:
project:
description: 'Spring Cloud GitHub project name (e.g. spring-cloud-config or 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)'
required: true
type: string
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 or 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)'
required: true
type: string
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: 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)
version=$(echo "$pom" | python3 -c "
import sys, xml.etree.ElementTree as ET
root = ET.fromstring(sys.stdin.read())
ns = root.tag.split('}')[0].strip('{') if '}' in root.tag else ''
tag = ('{' + ns + '}version') if ns else 'version'
print(root.find(tag).text.replace('-SNAPSHOT', ''))
")
echo "release-version=$version" >> $GITHUB_OUTPUT
echo "Release version: $version"
project="${{ inputs.project }}"
if [[ "$project" == *-commercial ]]; then
commercial_project="$project"
deployment_destination="Spring Enterprise"
else
commercial_project="${project}-commercial"
deployment_destination="Maven Central"
fi
echo "commercial-project=$commercial_project" >> $GITHUB_OUTPUT
echo "deployment-destination=$deployment_destination" >> $GITHUB_OUTPUT
echo "Commercial project: $commercial_project"
echo "Deployment destination: $deployment_destination"
- name: Create milestone in source repo if missing
env:
GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
run: |
version="${{ steps.setup.outputs.release-version }}"
repo="spring-cloud/${{ inputs.project }}"
echo "Checking for milestone '${version}' in ${repo}..."
milestone_number=$(gh api "repos/${repo}/milestones?per_page=100" | \
jq -r --arg title "$version" '.[] | select(.title == $title) | .number')
if [[ -n "$milestone_number" ]]; then
echo "Milestone '${version}' already exists in ${repo} (#${milestone_number})."
else
echo "Milestone '${version}' not found — creating..."
gh api "repos/${repo}/milestones" \
--method POST \
--field title="$version"
echo "Milestone '${version}' created in ${repo}."
fi
- 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 }}"
token="${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}"
project="${{ inputs.project }}"
if [[ "$project" == *-commercial ]]; then
# Source is already a commercial repo — use its branch SHA via the API
echo "Creating ${release_branch} in spring-cloud/${commercial_project} from ${project}@${{ 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}"
else
# Source is an OSS repo — clone it and push to the commercial repo
echo "Creating ${release_branch} in spring-cloud/${commercial_project} from OSS ${project}@${{ inputs.branch }}..."
git clone --depth 1 --branch "${{ inputs.branch }}" \
"https://x-access-token:${token}@github.com/spring-cloud/${project}.git" source-repo
cd source-repo
git remote add commercial \
"https://x-access-token:${token}@github.com/spring-cloud/${commercial_project}.git"
git push commercial "HEAD:refs/heads/${release_branch}"
cd ..
rm -rf source-repo
fi
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
env:
GH_TOKEN: ${{ inputs.token || secrets.token || secrets.GH_ACTIONS_REPO_TOKEN }}
PROJECT: ${{ inputs.project }}
SOURCE_BRANCH: ${{ inputs.branch }}
RELEASE_BRANCH: release/${{ steps.setup.outputs.release-version }}
RELEASE_TRAIN: ${{ inputs.release-train }}
run: |
cat > /tmp/update_projects.py << 'PYEOF'
import json, os, re, sys
project = os.environ['PROJECT']
source_branch = os.environ['SOURCE_BRANCH']
release_branch = os.environ['RELEASE_BRANCH']
with open('/tmp/projects_original.json') as f:
config = json.load(f)
is_commercial = project.endswith('-commercial')
base_project = project[:-len('-commercial')] if is_commercial else project
src_type = 'commercial' if is_commercial else 'oss'
# Mirror the JDK lookup chain used by determine-matrix:
# [project].[type].jdkVersions.[branch]
# [project].[type].jdkVersions.default
# defaults.[type].jdkVersions.default
jdkmap = config.get(base_project, {}).get(src_type, {}).get('jdkVersions', {})
if source_branch in jdkmap:
jdks = jdkmap[source_branch]
elif 'default' in jdkmap:
jdks = jdkmap['default']
else:
default_jdkmap = config.get('defaults', {}).get(src_type, {}).get('jdkVersions', {})
jdks = default_jdkmap.get(source_branch) or default_jdkmap.get('default', ['17', '21'])
print(f"JDKs for {source_branch} in {base_project}/{src_type}: {jdks}")
if base_project not in config:
config[base_project] = {}
if 'commercial' not in config[base_project]:
config[base_project]['commercial'] = {
'branches': {'scheduled': [], 'default': []},
'jdkVersions': {}
}
commercial = config[base_project]['commercial']
changed = False
scheduled = commercial.setdefault('branches', {}).setdefault('scheduled', [])
if release_branch not in scheduled:
scheduled.insert(0, release_branch)
changed = True
print(f"Added {release_branch} to {base_project}.commercial.branches.scheduled")
else:
print(f"{release_branch} already in branches.scheduled — skipping")
version_map = commercial.setdefault('jdkVersions', {})
if release_branch not in version_map:
version_map[release_branch] = jdks
changed = True
print(f"Set {base_project}.commercial.jdkVersions[{release_branch}] = {jdks}")
else:
print(f"jdkVersions[{release_branch}] already exists — skipping")
if not changed:
print("No changes needed to projects.json.")
sys.exit(0)
raw = json.dumps(config, indent=2)
def collapse_str_arrays(m):
try:
arr = json.loads(m.group(0))
if isinstance(arr, list) and all(isinstance(x, str) for x in arr):
return '[' + ', '.join(json.dumps(x) for x in arr) + ']'
except Exception:
pass
return m.group(0)
raw = re.sub(r'\[[\s\S]*?\]', collapse_str_arrays, raw)
with open('/tmp/projects_updated.json', 'w') as f:
f.write(raw + '\n')
print("projects.json written.")
PYEOF
# Fetch current projects.json and its blob SHA from main
file_response=$(gh api "repos/spring-cloud/spring-cloud-github-actions/contents/config/projects.json?ref=main")
current_sha=$(echo "$file_response" | jq -r '.sha')
echo "$file_response" | jq -r '.content' | tr -d '\n' | base64 -d > /tmp/projects_original.json
cp /tmp/projects_original.json /tmp/projects_updated.json
python3 /tmp/update_projects.py
if diff -q /tmp/projects_original.json /tmp/projects_updated.json > /dev/null 2>&1; then
echo "No changes to projects.json — nothing to push."
else
new_content_b64=$(base64 -w 0 /tmp/projects_updated.json)
jq -n \
--arg message "Adding ${RELEASE_BRANCH} for ${PROJECT} as part of joining ${RELEASE_TRAIN}" \
--arg content "$new_content_b64" \
--arg sha "$current_sha" \
'{message: $message, content: $content, sha: $sha, branch: "main"}' | \
gh api "repos/spring-cloud/spring-cloud-github-actions/contents/config/projects.json" \
--method PUT \
--input -
echo "projects.json updated on main."
fi
- name: Trigger release-train-join workflow and wait
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 }}"
deployment_destination="${{ steps.setup.outputs.deployment-destination }}"
run_url=$(gh workflow run release-train-join.yml \
--repo "spring-cloud/${commercial_project}" \
--ref "${release_branch}" \
--field "deployment-destination=${deployment_destination}" \
--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