Repository navigation
Create Commercial Release Branch #23
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: 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 |