diff --git a/.github/actions/spring-release-train-project-ready/action.yml b/.github/actions/spring-release-train-project-ready/action.yml index e581e42..7412b64 100644 --- a/.github/actions/spring-release-train-project-ready/action.yml +++ b/.github/actions/spring-release-train-project-ready/action.yml @@ -199,6 +199,17 @@ runs: fi exit $watch_exit_code + # A hotfix release/ branch is registered in config/projects.json for its own + # scheduled CI entry (create-hotfix-release-branch.yml); a GA release/ branch + # never is. This runs for both — retire-branch-projects-json is a no-op (logs and + # returns without committing) when the branch isn't registered in the first place. + - name: Remove release branch from projects.json + uses: ./.github/actions/retire-branch-projects-json + with: + repo: spring-cloud/${{ inputs.project }} + branch: release/${{ steps.version.outputs.version }} + token: ${{ inputs.token }} + - name: Remove release branch from Antora playbook uses: ./.github/actions/update-antora-playbook with: diff --git a/.github/workflows/README-create-hotfix-branch.md b/.github/workflows/README-create-hotfix-branch.md index fd0dc76..3f2eaea 100644 --- a/.github/workflows/README-create-hotfix-branch.md +++ b/.github/workflows/README-create-hotfix-branch.md @@ -1,107 +1,81 @@ # create-hotfix-branch -Creates a commercial hotfix release branch directly from an OSS tag, applies all standard commercial branch initialisation steps, stamps the project version to a hotfix snapshot, ensures required release-train workflows are present, and (by default) triggers `release-train-join` in the commercial repo. +Creates a commercial hotfix release branch. By default, the branch is cut from the tip of the commercial repo's `..x-internal` branch, preserving its full git history so the hotfix branch can later be rebased onto an updated `-internal` branch. Setting `use_tag` instead creates the branch from an OSS tag — an orphan branch with no shared history, the workflow's original behaviour. Either way, the workflow stamps the project version to a hotfix snapshot, ensures required release-train workflows are present, and (by default) triggers `release-train-join` in the commercial repo. + +The hotfix version itself is never passed in directly — it's looked up from this project's own entry in the release train's `jenkins-releaser-config` properties file, keyed by `release_train_version`. ## What it does -1. **Derives names** — strips the leading `v` from the tag, appends `.1`, and prefixes with `release/` to form the commercial branch name (e.g. `v5.0.1` → `release/5.0.1.1`). The commercial repo is always the OSS repo with `-commercial` appended. -2. **Initialises the branch** — delegates to [`initialize-commercial-branch`](initialize-commercial-branch.yml), which runs the full suite of commercial setup actions: copy `.settings.xml`, update CI/PR workflows, update licence headers, replace OSS repositories with commercial Broadcom repositories, update distribution management, and update `config/projects.json`. -3. **Creates a milestone** — creates a milestone in the commercial repo for the hotfix version if one does not already exist. -4. **Stamps the project version** — updates the project version in `pom.xml`, `gradle.properties`, and `build.gradle` files to `.1-SNAPSHOT` (e.g. `5.0.1` → `5.0.1.1-SNAPSHOT`). Optionally updates dependency versions at the same time. -5. **Ensures required workflows** — checks that `release-train-join.yml` and `release-train-ready.yml` are present on the new branch. If either is missing, runs the workflow generator for that single branch to create them (see [Workflow generator SHA](#workflow-generator-sha)). -6. **Triggers release-train-join** — dispatches `release-train-join.yml` in the commercial repo and waits for it to complete. This step is skipped when `spring_release_train` is left empty; the branch is still fully created and initialised. -7. **Triggers CI** — squashes all `[skip actions]` initialisation commits into a single root commit and force pushes it (without `[skip actions]`) to start CI now that the branch is fully initialised. +1. **Looks up the hotfix version and derives names** — detects this project's artifact ID from the OSS repo's root `pom.xml`, then reads `releaser.fixed-versions[]` from the `release_train_version`'s properties file in `spring-cloud-release-commercial@jenkins-releaser-config` to get the hotfix version (e.g. `5.0.4.1`). From it: the commercial branch is `release/` (e.g. `release/5.0.4.1`), the internal branch is `..x-internal` (e.g. `5.0.x-internal`), and (when `use_tag` is set) the OSS tag is `v..` (e.g. `v5.0.4`). The commercial repo is always the OSS repo with `-commercial` appended. +2. **Validates the `-internal` branch** (default path only) — fails before making any changes if the `-internal` branch's `pom.xml` version doesn't exactly match `..-INTERNAL-SNAPSHOT` — i.e. if the `-internal` branch is out of date. +3. **Creates the branch**: + - Default (`use_tag: false`): forks `release/` from the tip of the `-internal` branch via the GitHub API, preserving its full git history. Then adds a retargeted `ci-release.yml`/`release-ci-settings.xml` (via `add-commercial-release-files`, which already point the build at the commercial deploy target — no `.settings.xml` copy, workflow rewrite, or `pom.xml` distribution-management edit needed), updates license headers (and `checkstyle-header.txt`), and registers the branch in `config/projects.json` (a hotfix branch can fail its own CI independently of `-internal`, so it needs its own scheduled CI entry — this entry is removed automatically once the hotfix's release is finalized). + - `use_tag: true`: delegates to [`initialize-commercial-branch`](initialize-commercial-branch.yml), which creates an orphan branch from the OSS tag and runs the full suite of commercial setup actions: copy `.settings.xml`, update CI/PR workflows, update licence headers, replace OSS repositories with commercial Broadcom repositories, update distribution management, and update `config/projects.json`. +4. **Creates a milestone** — creates a milestone in the commercial repo for the hotfix version if one does not already exist. +5. **Stamps the project version** — updates the project version in `pom.xml`, `gradle.properties`, and `build.gradle` files to `-SNAPSHOT` (e.g. `5.0.4.1-SNAPSHOT`), and updates dependency versions from the release train. `versions` can supply additional dependency version overrides on top. +6. **Ensures required workflows** — checks that `release-train-join.yml` and `release-train-ready.yml` are present on the new branch. If either is missing, runs the workflow generator for that single branch to create them (see [Workflow generator SHA](#workflow-generator-sha)). +7. **Triggers release-train-join** — dispatches `release-train-join.yml` in the commercial repo and waits for it to complete. This step is skipped when `spring_release_train` is left empty; the branch is still fully created and initialised. +8. **Triggers CI**: + - Default path: squashes only the initialisation commits added on top of the `-internal` fork point into one commit and pushes it — history at and before the fork point is left untouched. + - `use_tag` path: squashes all `[skip actions]` initialisation commits into a single orphan root commit and force pushes it (without `[skip actions]`) to start CI. ## Inputs | Name | Required | Default | Description | |------|----------|---------|-------------| | `oss_repo` | yes | — | OSS repository name in the `spring-cloud` org (e.g. `spring-cloud-stream`) | -| `oss_tag` | yes | — | Tag in the OSS repository to branch from (e.g. `v5.0.1`) | +| `release_train_version` | yes | — | Release train version (e.g. `2025.1.2`) whose `jenkins-releaser-config` properties file names this project's hotfix version and supplies dependency versions | +| `use_tag` | no | `false` | Create the branch from an OSS tag (`v..`) instead of the `..x-internal` branch | | `spring_release_train` | no | — | Spring release train this hotfix belongs to (e.g. `2026.1`). Passed to `release-train-join`, and supplying it is what triggers the join — leave it empty to prepare the branch without joining. | -| `project_version` | no | `.1-SNAPSHOT` | Override the auto-computed hotfix project version | -| `release_train_version` | no | — | Release train version (e.g. `2025.1.2` or `2025.1.2.1-snapshot`). When supplied, all dependency version properties are updated from the Spring Cloud release train. Mutually exclusive with `versions`. | -| `versions` | no | — | JSON map of dependency versions to apply directly (e.g. `{"spring-boot":"3.3.0","spring-cloud-commons":"4.1.1"}`). Mutually exclusive with `release_train_version`. | +| `versions` | no | — | JSON map of dependency versions to apply on top of the release train's dependency versions (e.g. `{"spring-boot":"3.3.0","spring-cloud-commons":"4.1.1"}`) | | `sha` | no | Triggering commit | Commit SHA of this repo to copy release-train action files from when the workflow generator runs. See [Workflow generator SHA](#workflow-generator-sha). | When called as a reusable workflow (`workflow_call`), a `token` secret can also be supplied; if omitted the `GH_ACTIONS_REPO_TOKEN` organisation secret is used. ## Branch and version naming -| OSS tag | Commercial repo | Commercial branch | Default project version | -|---------|----------------|-------------------|------------------------| -| `v5.0.1` | `org/repo-commercial` | `release/5.0.1.1` | `5.0.1.1-SNAPSHOT` | -| `v3.3.2` | `org/repo-commercial` | `release/3.3.2.1` | `3.3.2.1-SNAPSHOT` | -| `v2.0.0` | `org/repo-commercial` | `release/2.0.0.1` | `2.0.0.1-SNAPSHOT` | - -## Version update behaviour - -The version update step always runs. Exactly which versions are changed depends on the inputs supplied: - -| Inputs | Project version | Dependency versions | -|--------|----------------|---------------------| -| Neither `versions` nor `release_train_version` | `.1-SNAPSHOT` | unchanged | -| `project_version` only | explicit override | unchanged | -| `versions` only | `.1-SNAPSHOT` | from `versions` map | -| `versions` + `project_version` | explicit override | from `versions` map | -| `release_train_version` | `.1-SNAPSHOT` | fetched from Spring Cloud release train | -| `release_train_version` + `project_version` | explicit override | fetched from Spring Cloud release train | - -When `release_train_version` is used the action fetches the matching `jenkins-releaser-config` properties file from `spring-cloud-release-commercial`. The version can be specified as a GA version (`2025.1.2`), a full hotfix version (`2025.1.2.1`), or a SNAPSHOT version (`2025.1.2.1-snapshot`) — the correct parent GA properties file is always resolved automatically. - -## Workflow generator SHA - -When the `ensure-workflows` step needs to generate `release-train-join.yml` and `release-train-ready.yml` for a new branch, it invokes the [`generate-workflows-for-branch`](../actions/generate-workflows-for-branch/) action. That action copies release-train action files from this repo into the commercial repo. The `sha` input controls which commit of this repo is used as the source. When omitted, the commit that triggered the workflow is used. - -This is useful if you need to ensure a specific version of the build/test action logic is deployed to the new branch: +Given a release train whose `jenkins-releaser-config` properties file has `releaser.fixed-versions[]=5.0.4.1`: -```bash -gh workflow run create-hotfix-release-branch.yml \ - -f oss_repo=spring-cloud-foo \ - -f oss_tag=v5.0.1 \ - -f spring_release_train=2026.1 \ - -f sha=348109524f9790dc1e20d48043fb1ef4765373b8 -``` +| Derived value | Example | +|---------------|---------| +| Commercial repo | `org/repo-commercial` | +| Commercial branch | `release/5.0.4.1` | +| Internal branch (default path) | `5.0.x-internal` | +| Expected internal `pom.xml` version (default path) | `5.0.4-INTERNAL-SNAPSHOT` | +| OSS tag (`use_tag` path only) | `v5.0.4` | +| Stamped project version | `5.0.4.1-SNAPSHOT` | ## Usage -### Manual dispatch (simplest — auto-computes everything) +### Manual dispatch — default path, from `-internal` ```bash gh workflow run create-hotfix-release-branch.yml \ -f oss_repo=spring-cloud-foo \ - -f oss_tag=v5.0.1 \ + -f release_train_version=2025.1.2 \ -f spring_release_train=2026.1 ``` -This creates `spring-cloud/spring-cloud-foo-commercial` branch `release/5.0.1.1`, stamps the project version to `5.0.1.1-SNAPSHOT`, and triggers `release-train-join`. +This looks up `spring-cloud-foo`'s fixed version for release train `2025.1.2` (e.g. `5.0.4.1`), verifies `5.0.x-internal` in `spring-cloud/spring-cloud-foo-commercial` is at `5.0.4-INTERNAL-SNAPSHOT`, forks `release/5.0.4.1` from it, stamps the project version to `5.0.4.1-SNAPSHOT`, and triggers `release-train-join`. -### With an explicit project version +### From an OSS tag instead of `-internal` ```bash gh workflow run create-hotfix-release-branch.yml \ -f oss_repo=spring-cloud-foo \ - -f oss_tag=v5.0.1 \ + -f release_train_version=2025.1.2 \ -f spring_release_train=2026.1 \ - -f project_version=5.0.1.2-SNAPSHOT + -f use_tag=true ``` -### With dependency versions from a release train - -```bash -gh workflow run create-hotfix-release-branch.yml \ - -f oss_repo=spring-cloud-foo \ - -f oss_tag=v5.0.1 \ - -f spring_release_train=2026.1 \ - -f release_train_version=2025.1.2 -``` +Same version lookup, but creates the branch as an orphan copy of tag `v5.0.4`, with full commercial customisation, instead of forking `5.0.x-internal`. -### With explicit dependency versions (no release train lookup) +### With explicit dependency version overrides ```bash gh workflow run create-hotfix-release-branch.yml \ -f oss_repo=spring-cloud-foo \ - -f oss_tag=v5.0.1 \ + -f release_train_version=2025.1.2 \ -f spring_release_train=2026.1 \ -f versions='{"spring-boot":"3.3.5","spring-cloud-commons":"4.1.2"}' ``` @@ -113,7 +87,7 @@ Create and initialise the branch but opt out of joining the release train: ```bash gh workflow run create-hotfix-release-branch.yml \ -f oss_repo=spring-cloud-foo \ - -f oss_tag=v5.0.1 + -f release_train_version=2025.1.2 ``` Omitting `spring_release_train` is what opts out: everything else runs, and the branch is @@ -127,17 +101,31 @@ jobs: uses: spring-cloud/spring-cloud-github-actions/.github/workflows/create-hotfix-release-branch.yml@v1 with: oss_repo: spring-cloud-foo - oss_tag: v5.0.1 - spring_release_train: '2026.1' release_train_version: '2025.1.2' + spring_release_train: '2026.1' secrets: token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} ``` +## Workflow generator SHA + +When the `ensure-workflows` step needs to generate `release-train-join.yml` and `release-train-ready.yml` for a new branch, it invokes the [`generate-workflows-for-branch`](../actions/generate-workflows-for-branch/) action. That action copies release-train action files from this repo into the commercial repo. The `sha` input controls which commit of this repo is used as the source. When omitted, the commit that triggered the workflow is used. + +This is useful if you need to ensure a specific version of the build/test action logic is deployed to the new branch: + +```bash +gh workflow run create-hotfix-release-branch.yml \ + -f oss_repo=spring-cloud-foo \ + -f release_train_version=2025.1.2 \ + -f spring_release_train=2026.1 \ + -f sha=348109524f9790dc1e20d48043fb1ef4765373b8 +``` + ## Related workflows | Workflow | Use when | |----------|----------| +| [`create-oss-release-branch`](create-oss-release-branch.yml) | Cutting a GA (non-hotfix) `release/` branch from `-internal` | | [`create-commercial-branch`](create-commercial-branch.yml) | Copying an OSS branch (not a tag) to the commercial repo | | [`initialize-commercial-branch`](initialize-commercial-branch.yml) | Full control over repo names, branch names, and all options | | [Run GitHub Actions Workflow Generator](run-github-actions-workflow-generator.README.md) | Regenerating workflows across all repos/branches in bulk | diff --git a/.github/workflows/README-retire-branch.md b/.github/workflows/README-retire-branch.md index a9e13a7..2f69e02 100644 --- a/.github/workflows/README-retire-branch.md +++ b/.github/workflows/README-retire-branch.md @@ -135,5 +135,5 @@ retired and frozen stays locked when the freeze is lifted. ## Related workflows - [`create-commercial-branch.yml`](README-create-commercial-branch.md) — creates a new commercial branch -- [`create-hotfix-release-branch.yml`](README-create-hotfix-branch.md) — creates a hotfix branch from an OSS tag +- [`create-hotfix-release-branch.yml`](README-create-hotfix-branch.md) — creates a hotfix branch, by default forked from the `-internal` branch - [`lock-unlock-branches.yml`](README-lock-branches.md) — temporarily freezes branches during a release, via a separate ruleset diff --git a/.github/workflows/create-hotfix-release-branch.yml b/.github/workflows/create-hotfix-release-branch.yml index b7c7c02..167533f 100644 --- a/.github/workflows/create-hotfix-release-branch.yml +++ b/.github/workflows/create-hotfix-release-branch.yml @@ -1,15 +1,31 @@ name: Create Hotfix Release Branch -# Creates a commercial hotfix release branch directly from an OSS tag. +# Creates a commercial hotfix release branch. By default it is cut from the tip of the +# commercial repo's ..x-internal branch, preserving its full git history so +# the hotfix branch can later be rebased onto an updated -internal branch. Setting use_tag +# instead creates it from an OSS tag, as an orphan branch with no shared history (today's +# original behaviour) — full commercial customisation, squashed to a single commit. # -# The commercial repo is always the OSS repo with -commercial appended. -# The release branch is named release/.1, derived from the tag by -# stripping the leading 'v' and appending '.1' (e.g. v5.0.1 -> release/5.0.1.1). +# The commercial repo is always the OSS repo with -commercial appended. The hotfix version +# (e.g. 5.0.4.1) is looked up from this project's own entry in the release train's +# jenkins-releaser-config properties file (releaser.fixed-versions[]), keyed by +# release_train_version. That release train is always still unfinalized when a hotfix +# branch is cut, so the looked-up value always carries a -SNAPSHOT qualifier (e.g. +# 5.0.4.1-SNAPSHOT) - derive strips it before validating and using the version. From that +# version: +# - the release branch is release/ (e.g. release/5.0.4.1) +# - the internal branch is ..x-internal (e.g. 5.0.x-internal) +# - the internal branch's pom.xml is expected to already be at +# ..-INTERNAL-SNAPSHOT (e.g. 5.0.4-INTERNAL-SNAPSHOT) +# - the OSS tag, used only when use_tag is set, is v.. (e.g. v5.0.4) # -# After the branch is initialised, the project version is stamped to -# .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. +# Before branching from -internal, the workflow validates that the -internal branch's +# pom.xml version matches the expected -INTERNAL-SNAPSHOT version above. If it doesn't, the +# -internal branch is out of date and the workflow fails before making any changes. +# +# After the branch is initialised, the project version is stamped to -SNAPSHOT +# (e.g. 5.0.4.1-SNAPSHOT), and dependency versions are updated from the release train. +# Optionally, an explicit versions override can be supplied on top. # Finally, release-train-join.yml is triggered in the commercial repo, when a # spring_release_train is supplied — leaving that input empty prepares the branch # and stops short of joining. @@ -21,27 +37,22 @@ on: 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)' + release_train_version: + description: "Release train version (e.g. 2025.1.2) whose jenkins-releaser-config properties file names this project's hotfix version and supplies dependency versions" required: true type: string + use_tag: + description: 'Create the branch from an OSS tag (v..) instead of the ..x-internal branch' + required: false + type: boolean + default: false spring_release_train: description: 'Spring release train this hotfix is part of (e.g. 2026.1). Supplying it joins that train once the branch is prepared; leave it empty to prepare the branch without joining.' required: false type: string default: '' - project_version: - description: 'Override the auto-computed hotfix project version (default: .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.' + description: 'JSON map of dependency versions to apply (e.g. {"spring-boot":"3.3.0","spring-cloud-commons":"4.1.1"}), applied on top of the release train dependency versions' required: false type: string default: '' @@ -57,27 +68,22 @@ on: 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)' + release_train_version: + description: "Release train version (e.g. 2025.1.2) whose jenkins-releaser-config properties file names this project's hotfix version and supplies dependency versions" required: true type: string + use_tag: + description: 'Create the branch from an OSS tag (v..) instead of the ..x-internal branch' + required: false + type: boolean + default: false spring_release_train: description: 'Spring release train this hotfix is part of (e.g. 2026.1). Supplying it joins that train once the branch is prepared; leave it empty to prepare the branch without joining.' required: false type: string default: '' - project_version: - description: 'Override the auto-computed hotfix project version (default: .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.' + description: 'JSON map of dependency versions to apply (e.g. {"spring-boot":"3.3.0"}), applied on top of the release train dependency versions' required: false type: string default: '' @@ -95,59 +101,255 @@ 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. + # Looks up this project's hotfix version from the release train's releaser config, and + # derives every other branch/version name from it. GitHub Actions expressions have no + # replace/substring/split 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 }} + 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 }} + internal_branch: ${{ steps.derive.outputs.internal_branch }} + expected_internal_version: ${{ steps.derive.outputs.expected_internal_version }} + oss_tag: ${{ steps.derive.outputs.oss_tag }} + auto_version: ${{ steps.derive.outputs.auto_version }} steps: - - name: Derive commercial repo and branch from tag + - name: Checkout + uses: actions/checkout@v4 + + - name: Look up hotfix version and derive branch names id: derive shell: bash + env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} 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" + RELEASE_TRAIN_VERSION="${{ inputs.release_train_version }}" - initialize: + # Detect the project name from the OSS repo's root pom.xml artifactId, the same + # way update-project-versions/src/index.js's detectProjectName does. + ARTIFACT_ID=$(gh api "repos/${OSS_REPO}/contents/pom.xml" --jq '.content' | base64 -d | python3 -c " + import xml.etree.ElementTree as ET, sys + root = ET.parse('/dev/stdin').getroot() + ns = (root.tag.split('}')[0] + '}') if '}' in root.tag else '' + a = root.find(f'{ns}artifactId') + print(a.text.strip() if a is not None else '', end='') + ") + if [[ -z "$ARTIFACT_ID" ]]; then + echo "ERROR: could not read artifactId from ${OSS_REPO}'s root pom.xml." + exit 1 + fi + PROJECT_NAME=$(echo "$ARTIFACT_ID" | sed -E 's/-dependencies$//; s/-parent$//') + echo "Artifact ID: $ARTIFACT_ID" + echo "Project name: $PROJECT_NAME" + + # Fetch this project's fixed version for the release train from + # spring-cloud-release-commercial@jenkins-releaser-config. + CONFIG_FILE=$(node .github/scripts/releaser-config-file.js "$RELEASE_TRAIN_VERSION") + CONFIG_URL="https://raw.githubusercontent.com/spring-cloud/spring-cloud-release-commercial/jenkins-releaser-config/${CONFIG_FILE}" + echo "Fetching releaser config from ${CONFIG_URL}" + HTTP_STATUS=$(curl -s -o /tmp/releaser-config.properties -w '%{http_code}' \ + -H "Authorization: Bearer ${GH_TOKEN}" "$CONFIG_URL") + if [[ "$HTTP_STATUS" != "200" ]]; then + echo "ERROR: failed to fetch releaser config from ${CONFIG_URL} (HTTP ${HTTP_STATUS})." + exit 1 + fi + + HOTFIX_VERSION=$(PROJECT_NAME="$PROJECT_NAME" python3 -c " + import os, re + project = os.environ['PROJECT_NAME'] + with open('/tmp/releaser-config.properties') as f: + content = f.read() + for line in content.split('\n'): + m = re.match(r'^releaser\.fixed-versions\[([^\]]+)\]=(.+)\$', line) + if m and m.group(1).strip() == project: + print(m.group(2).strip(), end='') + break + ") + if [[ -z "$HOTFIX_VERSION" ]]; then + echo "ERROR: project '${PROJECT_NAME}' was not found in the releaser config for release train ${RELEASE_TRAIN_VERSION}." + echo "Ensure the root pom.xml artifactId matches a key in the properties file." + exit 1 + fi + echo "Hotfix version: $HOTFIX_VERSION" + + # The releaser config is for a release train that hasn't been finalized yet, so + # every fixed version in it - including this project's - still carries a + # -SNAPSHOT qualifier (e.g. 5.0.4.1-SNAPSHOT). That is the expected shape for a + # hotfix branch creation, not an error: strip it before validating the remaining + # major.minor.patch.hotfix segments are numeric. + HOTFIX_VERSION="${HOTFIX_VERSION%-SNAPSHOT}" + + # A hotfix version has exactly 4 numeric segments: major.minor.patch.hotfix + IFS='.' read -r MAJOR MINOR PATCH HOTFIX_NUM EXTRA <<< "$HOTFIX_VERSION" + if [[ -z "$MAJOR" || -z "$MINOR" || -z "$PATCH" || -z "$HOTFIX_NUM" || -n "$EXTRA" \ + || ! "${MAJOR}${MINOR}${PATCH}${HOTFIX_NUM}" =~ ^[0-9]+$ ]]; then + echo "ERROR: '${HOTFIX_VERSION}' is not a 4-segment hotfix version (major.minor.patch.hotfix)." + exit 1 + fi + + INTERNAL_BRANCH="${MAJOR}.${MINOR}.x-internal" + EXPECTED_INTERNAL_VERSION="${MAJOR}.${MINOR}.${PATCH}-INTERNAL-SNAPSHOT" + OSS_TAG="v${MAJOR}.${MINOR}.${PATCH}" + COMMERCIAL_BRANCH="release/${HOTFIX_VERSION}" + AUTO_VERSION="${HOTFIX_VERSION}-SNAPSHOT" + + echo "commercial_repo=${COMMERCIAL_REPO}" >> "$GITHUB_OUTPUT" + echo "commercial_branch=${COMMERCIAL_BRANCH}" >> "$GITHUB_OUTPUT" + echo "commercial_project=${COMMERCIAL_PROJECT}" >> "$GITHUB_OUTPUT" + echo "commercial_version=${HOTFIX_VERSION}" >> "$GITHUB_OUTPUT" + echo "internal_branch=${INTERNAL_BRANCH}" >> "$GITHUB_OUTPUT" + echo "expected_internal_version=${EXPECTED_INTERNAL_VERSION}" >> "$GITHUB_OUTPUT" + echo "oss_tag=${OSS_TAG}" >> "$GITHUB_OUTPUT" + echo "auto_version=${AUTO_VERSION}" >> "$GITHUB_OUTPUT" + + echo "OSS repo: $OSS_REPO" + echo "Commercial repo: $COMMERCIAL_REPO" + echo "Commercial project: $COMMERCIAL_PROJECT" + echo "Commercial branch: $COMMERCIAL_BRANCH" + echo "Internal branch: $INTERNAL_BRANCH" + echo "Expected internal version: $EXPECTED_INTERNAL_VERSION" + echo "OSS tag (use_tag only): $OSS_TAG" + echo "Auto version: $AUTO_VERSION" + + # Fails before any branch is touched if the -internal branch's pom.xml version doesn't + # match the base version this hotfix is built on — i.e. the -internal branch is out of + # date. A no-op when use_tag is set. + validate-internal-branch: needs: derive + runs-on: ubuntu-latest + steps: + - name: Validate -internal branch pom.xml version + if: ${{ !inputs.use_tag }} + shell: bash + env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + run: | + COMMERCIAL_REPO="${{ needs.derive.outputs.commercial_repo }}" + INTERNAL_BRANCH="${{ needs.derive.outputs.internal_branch }}" + EXPECTED_VERSION="${{ needs.derive.outputs.expected_internal_version }}" + + CONTENT=$(gh api "repos/${COMMERCIAL_REPO}/contents/pom.xml" -X GET -f ref="$INTERNAL_BRANCH" --jq '.content') || { + echo "ERROR: could not read pom.xml from '${INTERNAL_BRANCH}' in ${COMMERCIAL_REPO}. Does the branch exist?" + exit 1 + } + + ACTUAL_VERSION=$(echo "$CONTENT" | base64 -d | python3 -c " + import xml.etree.ElementTree as ET, sys + root = ET.parse('/dev/stdin').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='') + ") + + echo "Internal branch: $INTERNAL_BRANCH" + echo "Expected version: $EXPECTED_VERSION" + echo "Actual version: $ACTUAL_VERSION" + + if [[ "$ACTUAL_VERSION" != "$EXPECTED_VERSION" ]]; then + echo "ERROR: '${INTERNAL_BRANCH}' in ${COMMERCIAL_REPO} has version '${ACTUAL_VERSION}' but expected '${EXPECTED_VERSION}'." + echo "The -internal branch is out of date — update it before creating the hotfix branch." + exit 1 + fi + echo "-internal branch version matches. OK to proceed." + + initialize: + needs: [derive, validate-internal-branch] + if: ${{ inputs.use_tag }} uses: ./.github/workflows/initialize-commercial-branch.yml with: oss_repo: ${{ format('spring-cloud/{0}', inputs.oss_repo) }} - oss_tag: ${{ inputs.oss_tag }} + oss_tag: ${{ needs.derive.outputs.oss_tag }} commercial_repo: ${{ needs.derive.outputs.commercial_repo }} commercial_branch: ${{ needs.derive.outputs.commercial_branch }} set_default_branch: false secrets: inherit + # Cuts release/ from the tip of the -internal branch, preserving its full git + # history (so the hotfix branch can later be rebased onto an updated -internal branch), + # then layers on only what -internal doesn't already have: a retargeted ci-release.yml + # (mirrors create-oss-release-branch.yml's release/-from-internal flow — + # distribution-management/workflow-rewrite/settings.xml are redundant here, since + # ci-release.yml + release-ci-settings.xml already point the build at the commercial + # deploy target), Broadcom license headers (and checkstyle-header.txt in the same + # commit), and a config/projects.json registration — a hotfix branch carries real changes + # on top of -internal and can fail its own CI independently of it, unlike a GA + # release/, so it needs its own scheduled CI entry. The entry is removed again + # by spring-release-train-project-ready once the hotfix ships. A no-op when use_tag is set. + create-internal-release-branch: + needs: [derive, validate-internal-branch] + if: ${{ !inputs.use_tag }} + runs-on: ubuntu-latest + outputs: + base_sha: ${{ steps.create.outputs.base_sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.sha || github.sha }} + + - name: Create release branch from internal branch + id: create + env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + run: | + COMMERCIAL_REPO="${{ needs.derive.outputs.commercial_repo }}" + COMMERCIAL_BRANCH="${{ needs.derive.outputs.commercial_branch }}" + INTERNAL_BRANCH="${{ needs.derive.outputs.internal_branch }}" + + if gh api "repos/${COMMERCIAL_REPO}/git/ref/heads/${COMMERCIAL_BRANCH}" --silent 2>/dev/null; then + echo "ERROR: ${COMMERCIAL_BRANCH} already exists in ${COMMERCIAL_REPO}." + exit 1 + fi + + BASE_SHA=$(gh api "repos/${COMMERCIAL_REPO}/git/ref/heads/${INTERNAL_BRANCH}" --jq '.object.sha') + echo "Creating ${COMMERCIAL_BRANCH} in ${COMMERCIAL_REPO} from ${INTERNAL_BRANCH} at ${BASE_SHA}..." + + gh api "repos/${COMMERCIAL_REPO}/git/refs" \ + --method POST \ + --field ref="refs/heads/${COMMERCIAL_BRANCH}" \ + --field sha="${BASE_SHA}" + + echo "base_sha=${BASE_SHA}" >> "$GITHUB_OUTPUT" + echo "Branch ${COMMERCIAL_BRANCH} created." + + - name: Add commercial release CI files + uses: ./.github/actions/add-commercial-release-files + with: + repository: ${{ needs.derive.outputs.commercial_repo }} + branch: ${{ needs.derive.outputs.commercial_branch }} + retarget-ci-release-from: ${{ needs.derive.outputs.internal_branch }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + + - name: Update license headers + uses: ./.github/actions/update-license-headers + with: + repository: ${{ needs.derive.outputs.commercial_repo }} + branch: ${{ needs.derive.outputs.commercial_branch }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + + - name: Update projects.json + uses: ./.github/actions/update-projects-json + with: + oss-repo: ${{ format('spring-cloud/{0}', inputs.oss_repo) }} + oss-branch: '' + commercial-branch: ${{ needs.derive.outputs.commercial_branch }} + set-default-branch: false + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + update-versions: - needs: [derive, initialize] + needs: [derive, initialize, create-internal-release-branch] + if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Clone commercial branch and compute hotfix version - id: setup + - name: Clone commercial branch shell: bash env: GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} @@ -161,41 +363,10 @@ jobs: 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 + # Updates dependency version properties from the release train. This also stamps the + # project version to the release train's 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 }} @@ -203,14 +374,13 @@ jobs: 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. + # Always stamp the project version to the hotfix snapshot. 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 }} + project-version: ${{ needs.derive.outputs.auto_version }} versions: ${{ inputs.versions || '{}' }} directory: _version_repo @@ -223,7 +393,7 @@ jobs: echo "No version changes to commit." exit 0 fi - git commit -m "Updating project version to ${{ steps.setup.outputs.effective_version }} [skip actions]" + git commit -m "Updating project version to ${{ needs.derive.outputs.auto_version }} [skip actions]" git push origin "${{ needs.derive.outputs.commercial_branch }}" echo "Version updates committed." @@ -349,16 +519,15 @@ jobs: 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] + needs: [derive, initialize, create-internal-release-branch, trigger-release-train-join] + if: ${{ !cancelled() }} runs-on: ubuntu-latest steps: - - name: Squash initialization commits and push to trigger CI + # The tag path is an orphan branch with no history worth keeping, so its + # initialization commits are collapsed into a single root commit. + - name: Squash initialization commits into an orphan root commit and push to trigger CI + if: ${{ inputs.use_tag }} shell: bash env: GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} @@ -385,3 +554,35 @@ jobs: 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}." + + # The -internal path preserves real history, so only the initialization commits + # added on top of the fork point are squashed — everything at and before base_sha + # (the -internal branch's history) is left untouched, so the branch can later be + # rebased onto an updated -internal branch. + - name: Squash initialization commits onto the internal branch fork point and push to trigger CI + if: ${{ !inputs.use_tag }} + shell: bash + env: + GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + run: | + COMMERCIAL_REPO="${{ needs.derive.outputs.commercial_repo }}" + BRANCH="${{ needs.derive.outputs.commercial_branch }}" + BASE_SHA="${{ needs.create-internal-release-branch.outputs.base_sha }}" + + 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" + + if [[ "$(git rev-parse HEAD)" != "$BASE_SHA" ]]; then + git reset --soft "$BASE_SHA" + git commit -m "Initialize hotfix release branch" + git push --force-with-lease origin "$BRANCH" + echo "Squashed initialization commits into a single commit on ${COMMERCIAL_REPO}@${BRANCH}." + else + git commit --allow-empty -m "Initialize hotfix release branch" + git push origin "$BRANCH" + echo "No initialization commits to squash; pushed empty commit to trigger CI on ${COMMERCIAL_REPO}@${BRANCH}." + fi diff --git a/README.md b/README.md index a8a7c16..3a46d91 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Shared GitHub Actions workflows and composite actions for Spring Cloud projects. | [rollout-deploy-docs-trigger.yml](.github/workflows/rollout-deploy-docs-trigger.yml) | Pushes the canonical `Deploy Docs` trigger to every source branch in `projects.json` that already has one. Fixes the `contents: read` gap, unifies the token, and allow-lists each branch. Defaults to a dry run. | [README](.github/workflows/README-rollout-deploy-docs-trigger.md) | | [initialize-commercial-branch.yml](.github/workflows/initialize-commercial-branch.yml) | Full-control workflow that creates a new commercial branch from an OSS branch and runs all commercial setup steps (settings, CI/PR workflows, licenses, repositories, distribution management, Antora playbook, projects.json). | [Example](examples/initialize-commercial-branch.yml) | | [create-commercial-branch.yml](.github/workflows/create-commercial-branch.yml) | Simplified wrapper over `initialize-commercial-branch` for the common case: copies an OSS branch to `-commercial` using the same branch name. | — | -| [create-hotfix-branch.yml](.github/workflows/create-hotfix-release-branch.yml) | Creates a commercial hotfix `release/[version]` branch directly from an OSS tag, applies all commercial setup steps, stamps the project version, ensures release-train workflows are present, triggers `release-train-join`, and starts CI. | [README](.github/workflows/README-create-hotfix-branch.md) | +| [create-hotfix-branch.yml](.github/workflows/create-hotfix-release-branch.yml) | Creates a commercial hotfix `release/[version]` branch, by default forked from the `-internal` branch (preserving history), or from an OSS tag when `use_tag` is set. Stamps the project version, ensures release-train workflows are present, triggers `release-train-join`, and starts CI. | [README](.github/workflows/README-create-hotfix-branch.md) | | [retire-branch.yml](.github/workflows/retire-branch.yml) | Retires a branch: removes it from `projects.json`, removes its Dependabot entries, and locks the branch permanently via the repository's `Locked Branches` ruleset. | [README](.github/workflows/README-retire-branch.md) | | [lock-branches.yml](.github/workflows/lock-unlock-branches.yml) | Locks or unlocks the branches of some or all projects via a `Release Freeze` ruleset per repository — freeze while a release is staged, unfreeze before `post-release` needs to push. Defaults to a dry run. | [README](.github/workflows/README-lock-branches.md) | | [post-release.yml](.github/workflows/post-release.yml) | Runs the chores that follow a release train: verifies every project was tagged, closes milestones, publishes GitHub releases, seeds the next snapshot properties file, opens the next milestones, merges `release/` branches back into `.x` and bumps them to the new snapshots, nudges Dependabot, opens the website PR documenting the released versions — with the announcement blog post too, on an OSS train — bumps the Spring Cloud version on start.spring.io, and rolls the release train's GitHub Project board over to the next version. Defaults to a dry run. | [README](.github/workflows/README-post-release.md) | diff --git a/docs/release-automation.md b/docs/release-automation.md index 8869eae..be96f32 100644 --- a/docs/release-automation.md +++ b/docs/release-automation.md @@ -581,27 +581,43 @@ The merge-back still runs: `release/` merges into the commercial `.x` l ## Hotfixes **[`create-hotfix-release-branch.yml`](../.github/workflows/README-create-hotfix-branch.md)** -cuts a commercial hotfix directly from an **OSS tag**. +cuts a commercial hotfix release branch. By default it forks the **`-internal` branch**, +preserving real git history; setting `use_tag` falls back to the workflow's original +behaviour of branching from an **OSS tag** as an orphan copy. -| | Normal release | Hotfix | -|---|---|---| -| Source | a branch (`main`, `4.2.x`) | a **tag** (`v5.0.1`) | -| Branch | `release/` | `release/.1` (`v5.0.1` → `release/5.0.1.1`) | -| Mechanism | full-history `-internal` branch, then refs-API cut | orphan branch via `initialize-commercial-branch`, no `-internal` branch | -| History | preserved | discarded — `trigger-ci` squashes to a single root commit and force-pushes | -| Version stamping | stays `-INTERNAL-SNAPSHOT` until readiness | explicit `.1-SNAPSHOT` immediately, before join | -| `deployment-destination` | `Maven Central` or `Spring Enterprise` | always `Spring Enterprise` | -| Milestone | OSS repo | commercial repo | +The hotfix version is never passed in — it's looked up from this project's own +`releaser.fixed-versions[]` entry in the `release_train_version`'s properties file +(`spring-cloud-release-commercial@jenkins-releaser-config`), the same file +[`create-oss-release-branch.yml`](#the-oss-release-end-to-end) reads for a normal release. + +| | Normal release | Hotfix (default) | Hotfix (`use_tag`) | +|---|---|---|---| +| Source | a branch (`main`, `4.2.x`) | the commercial repo's `-internal` branch | an OSS **tag** (`v5.0.4`) | +| Branch | `release/` | `release/` (`5.0.4.1` → `release/5.0.4.1`) | `release/` | +| Mechanism | full-history `-internal` branch, then refs-API cut | refs-API cut from `-internal`, same mechanism as a normal release | orphan branch via `initialize-commercial-branch`, no `-internal` branch | +| History | preserved | preserved | discarded — `trigger-ci` squashes to a single root commit and force-pushes | +| Pre-flight check | — | `-internal`'s pom.xml must already be at `..-INTERNAL-SNAPSHOT`, or the job fails | — | +| Commercial customisation | full `initialize-commercial-branch` suite | only `add-commercial-release-files` (retargeted `ci-release.yml`) + license headers — `-internal` doesn't carry the rest, but `ci-release.yml`/`release-ci-settings.xml` already cover the deploy target | full `initialize-commercial-branch` suite | +| `config/projects.json` | scheduled (`-internal`), not registered (`release/`) | registered — a hotfix branch's CI can fail independently of `-internal`'s; removed by `spring-release-train-project-ready` once the hotfix ships | registered (as it always has been) | +| Version stamping | stays `-INTERNAL-SNAPSHOT` until readiness | explicit `-SNAPSHOT` immediately, before join | explicit `-SNAPSHOT` immediately, before join | +| `deployment-destination` | `Maven Central` or `Spring Enterprise` | always `Spring Enterprise` | always `Spring Enterprise` | +| Milestone | OSS repo | commercial repo | commercial repo | ```mermaid flowchart TD - TAG(["OSS tag
v5.0.1"]) --> DERIVE + TRAIN(["release_train_version
2025.1.2"]) --> DERIVE - DERIVE["derive
version 5.0.1 → release/5.0.1.1
always a .1 patch suffix"] --> INIT + DERIVE["derive
releaser.fixed-versions[project] → 5.0.4.1
→ release/5.0.4.1, 5.0.x-internal,
expected 5.0.4-INTERNAL-SNAPSHOT"] --> VALIDATE + + VALIDATE{"validate-internal-branch
-internal pom.xml ==
5.0.4-INTERNAL-SNAPSHOT?
skipped when use_tag"} -->|no, use_tag| INIT + VALIDATE -->|yes| CIRB + VALIDATE -->|no, fails| FAIL(["Workflow fails —
-internal is out of date"]) INIT["initialize
calls initialize-commercial-branch
with oss_tag
orphan branch — no history"] --> UV - UV["update-versions
stamp 5.0.1.1-SNAPSHOT now,
not INTERNAL-SNAPSHOT later"] --> MS + CIRB["create-internal-release-branch
refs-API cut from -internal tip,
retarget ci-release.yml,
license headers, projects.json"] --> UV + + UV["update-versions
stamp 5.0.4.1-SNAPSHOT now,
not INTERNAL-SNAPSHOT later"] --> MS MS["create-milestone
in the commercial repo"] --> EW @@ -609,7 +625,7 @@ flowchart TD JOIN["release-train-join (generated)
deployment-destination =
Spring Enterprise
— always"] --> TCI - TCI["trigger-ci
squash all [skip actions] commits
into one root commit,
force-push --force-with-lease"] --> MR + TCI["trigger-ci
use_tag: re-orphan, force-push
default: soft-reset to fork point,
squash on top, force-push"] --> MR MR["spring-io/release-train
meta-release: build, tag, deploy"] --> PRH @@ -619,10 +635,10 @@ flowchart TD classDef ext fill:#fee2e2,stroke:#ef4444,color:#7f1d1d classDef warn fill:#fef9c3,stroke:#eab308,color:#713f12 classDef term fill:#f1f5f9,stroke:#64748b,color:#0f172a - class DERIVE,INIT,UV,MS,EW,PRH here + class DERIVE,VALIDATE,INIT,CIRB,UV,MS,EW,PRH here class JOIN,MR ext class TCI warn - class TAG,DONE term + class TRAIN,DONE,FAIL term ``` ### Inputs @@ -630,51 +646,52 @@ flowchart TD | Input | Required | Notes | |---|---|---| | `oss_repo` | yes | e.g. `spring-cloud-stream` — the commercial repo is always this plus `-commercial` | -| `oss_tag` | yes | e.g. `v5.0.1` | +| `release_train_version` | yes | e.g. `2025.1.2` — its `jenkins-releaser-config` properties file names this project's hotfix version and supplies dependency versions | +| `use_tag` | no | Branch from an OSS tag (`v..`) instead of the `-internal` branch. Default `false` | | `spring_release_train` | no | The Spring release train this hotfix joins. Supplying it joins that train; leave it empty to prepare the branch without joining | -| `project_version` | no | Override the auto-computed `.1-SNAPSHOT` | -| `release_train_version` | no | When set, dependency versions are pulled from that Spring Cloud train's properties file | -| `versions` | no | JSON map of explicit dependency versions, e.g. `{"spring-boot":"3.3.0"}`. **Mutually exclusive** with `release_train_version` | +| `versions` | no | JSON map of dependency versions applied on top of the release train's, e.g. `{"spring-boot":"3.3.0"}` | | `sha` | no | Commit of *this* repo to copy release-train action files from | -### The seven jobs +### The jobs | Job | Line | What it does | |---|---|---| -| `derive` | `:109` | Strip the leading `v`, append `.1`, prefix `release/` — `v5.0.1` → `release/5.0.1.1`. Done in shell because Actions expressions have no string-replace function | -| `initialize` | `:138` | Calls `initialize-commercial-branch.yml` with `oss_tag` (not `oss_branch`) and `set_default_branch: false`, `secrets: inherit`. Produces an **orphan** branch carrying the commercial setup — Broadcom license headers, commercial Artifactory repositories and ``, restricted CI/PR workflows, `.settings.xml`, Antora playbook and `projects.json` entries — and none of the OSS history | -| `update-versions` | `:149` | Clone the new branch, read the root pom version with Python's `ElementTree` (handles optional XML namespace prefixes), compute `.1-SNAPSHOT`. If `release_train_version` was given, first apply that train's dependency versions with `commercial: 'true'`; then **always** stamp the project version, since that first pass would otherwise leave the train's version on the project. Commit `[skip actions]` | -| `create-milestone` | `:237` | Milestone in the **commercial** repo — not the OSS repo, unlike every other flow | -| `ensure-workflows` | `:251` | Check the branch with the shared [`check-release-train-workflows`](../.github/actions/check-release-train-workflows/) action; run the generator only if it reports something missing. Also resolves the primary JDK from `projects.json` — looking up `commercial.jdkVersions[release/]`, which `initialize` populated — falling back to the commercial default, then the global default, warning at each step | -| `trigger-release-train-join` | `:329` | Always `deployment-destination=Spring Enterprise`, then `gh run watch --exit-status` | -| `trigger-ci` | `:364` | Re-orphan, commit the whole tree as `Initialize hotfix branch`, force-push | +| `derive` | `:104` | Reads the OSS repo's root `pom.xml` `artifactId` to detect the project name, fetches `releaser.fixed-versions[]` from the release train's properties file to get the hotfix version (e.g. `5.0.4.1`), and splits it into `release/`, `..x-internal`, the expected `-INTERNAL-SNAPSHOT` version, and (for `use_tag`) `v..` | +| `validate-internal-branch` | `:212` | Fetches `pom.xml` from the `-internal` branch via the contents API and fails the workflow if its version doesn't exactly match `derive`'s expected `-INTERNAL-SNAPSHOT` version. A no-op when `use_tag` is set | +| `initialize` | `:250` | `use_tag` only. Calls `initialize-commercial-branch.yml` with `oss_tag` (not `oss_branch`) and `set_default_branch: false`, `secrets: inherit`. Produces an **orphan** branch carrying the commercial setup — Broadcom license headers, commercial Artifactory repositories and ``, restricted CI/PR workflows, `.settings.xml`, Antora playbook and `projects.json` entries — and none of the OSS history | +| `create-internal-release-branch` | `:273` | Default path only. Forks `release/` from the `-internal` branch's tip via the same refs-API cut `create-oss-release-branch.yml` uses, preserving history. Then runs only `add-commercial-release-files` (retargeting the inherited `ci-release.yml`), `update-license-headers`, and `update-projects-json` — no settings.xml/workflow-rewrite/distribution-management, since `ci-release.yml`/`release-ci-settings.xml` already cover the commercial deploy target | +| `update-versions` | `:334` | Clone the new branch. If `release_train_version` was given, first apply that train's dependency versions with `commercial: 'true'`; then **always** stamp the project version to `derive`'s pre-computed `-SNAPSHOT`, since that first pass would otherwise leave the train's version on the project. Commit `[skip actions]` | +| `create-milestone` | `:390` | Milestone in the **commercial** repo — not the OSS repo, unlike every other flow | +| `ensure-workflows` | `:404` | Check the branch with the shared [`check-release-train-workflows`](../.github/actions/check-release-train-workflows/) action; run the generator only if it reports something missing. Also resolves the primary JDK from `projects.json` — looking up `commercial.jdkVersions[release/]`, which branch creation populated — falling back to the commercial default, then the global default, warning at each step | +| `trigger-release-train-join` | `:482` | Always `deployment-destination=Spring Enterprise`, then `gh run watch --exit-status` | +| `trigger-ci` | `:512` | `use_tag`: re-orphan, commit the whole tree as `Initialize hotfix branch`, force-push. Default: soft-reset to the `-internal` fork point, squash only the initialisation commits on top, force-push — history at and before the fork point is untouched | ### Two jobs worth a closer look -**`initialize` produces an orphan branch.** A hotfix carries none of the project's git history — -`git log` on `release/5.0.1.1` shows a single commit. The diff against the tag it came from is -the entire branch. This is inherited from `create-commercial-branch`, which orphans deliberately -so OSS history never lands in a commercial repo. +**`use_tag`'s `initialize` produces an orphan branch.** A hotfix created this way carries none +of the project's git history — `git log` on `release/5.0.4.1` shows a single commit. The diff +against the tag it came from is the entire branch. This is inherited from +`create-commercial-branch`, which orphans deliberately so OSS history never lands in a +commercial repo. -**`trigger-ci` rewrites history.** It re-orphans the branch to collapse the `[skip actions]` -setup commits into one root commit, then force-pushes with +**`trigger-ci` rewrites history differently per path.** For `use_tag`, it re-orphans the branch +to collapse the `[skip actions]` setup commits into one root commit, then force-pushes with `--force-with-lease=":"` -([`create-hotfix-release-branch.yml:385-392`](../.github/workflows/create-hotfix-release-branch.yml)), -so a concurrent update to the branch aborts the push rather than being silently overwritten. The +([`create-hotfix-release-branch.yml:519-550`](../.github/workflows/create-hotfix-release-branch.yml)), +so a concurrent update to the branch aborts the push rather than being silently overwritten. For +the default path, it instead does `git reset --soft "$BASE_SHA"` to the SHA `-internal` was +forked at, squashing only the commits added on top and leaving `-internal`'s own history intact +underneath — exactly `create-oss-release-branch.yml`'s `trigger-ci` job for a normal release — +so `release/` can later be rebased onto an updated `-internal` branch. Either way the push deliberately omits `[skip actions]`, which is what starts CI. ### Version stamping is the real difference A normal release leaves versions alone until `spring-release-train-project-ready` stamps them. -A hotfix stamps immediately, in `update-versions`, before the train is even joined. Three ways -to control what it stamps: - -- **Default** — `.1-SNAPSHOT`. So a branch cut from `v5.0.1` whose pom says - `5.0.1` becomes `5.0.1.1-SNAPSHOT`. -- **`project_version`** — an explicit override, replacing the computed value. -- **`release_train_version` or `versions`** — update *dependency* versions too, either from a - train's properties file or from an inline JSON map. The project version is stamped afterward - either way, so a train pass cannot leave the train's version on the project. +A hotfix stamps immediately, in `update-versions`, before the train is even joined, always to +the version `derive` computed from the release train's properties file (`-SNAPSHOT`). +`versions` can still supply additional dependency version overrides on top, applied in the same +pass as the project-version stamp. ### Post-release for a hotfix