Join Release Train #5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |