Repository navigation
Create Hotfix Release Branch #1
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 Hotfix Release 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, unless | |
| # the trigger_release_train_join input is set to false to opt out. | |
| 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: '' | |
| trigger_release_train_join: | |
| description: 'Join the Spring release train after the branch is prepared. Uncheck to opt out.' | |
| required: false | |
| type: boolean | |
| default: true | |
| 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: '' | |
| trigger_release_train_join: | |
| description: 'Dispatch release-train-join.yml in the commercial repo after the branch is prepared. Set to false to opt out.' | |
| required: false | |
| type: boolean | |
| default: true | |
| 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 | |
| if: ${{ inputs.trigger_release_train_join }} | |
| 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 | |
| # Collapse all [skip actions] initialization commits into a single commit and | |
| # force push it (without [skip actions]) so the hotfix branch starts from one | |
| # clean snapshot and CI is triggered now that the branch is fully initialised. | |
| # The branch is an orphan (no OSS history to preserve), so the entire history | |
| # is rewritten to a single root commit. | |
| trigger-ci: | |
| needs: [derive, trigger-release-train-join] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Squash initialization commits and push 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" | |
| # Capture the current remote tip so the force push is guarded against a | |
| # concurrent update to the branch. | |
| REMOTE_SHA="$(git rev-parse HEAD)" | |
| # Create a fresh single-commit history from the current tree. The orphan | |
| # checkout keeps the working tree and index, so committing captures the | |
| # fully initialised branch content as one root commit. | |
| git checkout --orphan _squashed | |
| git commit -m "Initialize hotfix branch" | |
| git push --force-with-lease="${BRANCH}:${REMOTE_SHA}" \ | |
| origin "HEAD:refs/heads/${BRANCH}" | |
| echo "Squashed initialization commits into a single commit on ${COMMERCIAL_REPO}@${BRANCH}." |