Create Hotfix Branch #290
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 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. | |
| 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: '' | |
| 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: '' | |
| 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 | |
| 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 | |
| trigger-ci: | |
| needs: [derive, trigger-release-train-join] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Push empty commit 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" | |
| git commit --allow-empty -m "Initialize hotfix branch" | |
| git push origin "$BRANCH" | |
| echo "Empty commit pushed to trigger CI on ${COMMERCIAL_REPO}@${BRANCH}." |