Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/actions/resolve-release-branch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Resolve Release Branch Action

Resolves the maintenance branch a given project version belongs to.

## Description

Three workflows need the same answer to the same question — *which branch does this version
live on?* — and until this action existed they each answered it with their own copy of the
same 60 lines. `post-release.yml` needs it to merge a release branch back, `update-versions.yml`
to push a version bump, and `create-oss-release-branch.yml` to know which OSS branch to cut a
release from.

The rule is:

1. Take the version's line and try `<major>.<minor>.x`. If that branch exists, use it.
2. On a **commercial** repository, stop there. Those repositories have no `main` at all —
`spring-cloud-config-commercial`'s default branch is `4.3.x` — so there is no sane
fallback to make.
3. Otherwise fall back to `main`, but only after reading `main`'s `pom.xml` and confirming it
is on the same `<major>.<minor>` line. Without that check a version whose branch has been
deleted, or a typo, would quietly act on whatever `main` happens to be.

Step 3 is the part worth keeping in one place. It is why a `5.1.0` release of a project whose
`5.1.x` branch does not exist yet correctly resolves to `main`, while a `3.9.9` release of the
same project is refused rather than silently rewriting `main`.

## Inputs

| Input | Description | Required | Default |
|-------|-------------|----------|---------|
| `repo` | Full repository path (e.g. `spring-cloud/spring-cloud-config`) | Yes | |
| `version` | The version whose line is wanted. Any qualifier is stripped first, so `5.0.4-SNAPSHOT`, `5.1.0-INTERNAL-SNAPSHOT`, `5.1.0-M1` and `5.0.3` all resolve the same way. | Yes | |
| `commercial` | Repository is a commercial one, so there is no `main` to fall back to | No | `false` |
| `token` | Token with read access to the repository | Yes | |

## Outputs

| Output | Description |
|--------|-------------|
| `branch` | The resolved branch, or empty when `status` is not `ok` |
| `status` | `ok`, `branch-not-found`, or `version-mismatch` |
| `message` | Why, when `status` is not `ok`. Empty otherwise. |

## It reports, it does not fail

An unresolvable branch exits `0` with a non-`ok` status rather than failing the step. Two of
the three callers run this across a matrix of sixteen-odd projects, and one project that
cannot be resolved should appear as a row in the run summary, not take the whole release
down. A caller that targets a single project — `create-oss-release-branch.yml` — checks the
status itself and fails there.

## Usage

```yaml
- name: Resolve target branch
id: branch
uses: ./.github/actions/resolve-release-branch
with:
repo: spring-cloud/spring-cloud-config
version: 5.0.4-SNAPSHOT
commercial: 'false'
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}

- name: Do the work
if: steps.branch.outputs.status == 'ok'
run: echo "Working on ${{ steps.branch.outputs.branch }}"
```

## Examples

| Repository | Version | Result |
|---|---|---|
| `spring-cloud-config` | `5.0.5` | `5.0.x` — the branch exists |
| `spring-cloud-config` | `5.1.0` | `main` — no `5.1.x` yet, and `main` is at `5.1.0-SNAPSHOT` |
| `spring-cloud-config` | `5.1.0-M1` | `main` — the qualifier does not change the line |
| `spring-cloud-config` | `3.9.9` | `version-mismatch` — no `3.9.x`, and `main` is not on that line |
| `spring-cloud-config-commercial` | `4.9.9` | `branch-not-found` — commercial, so no `main` fallback |
148 changes: 148 additions & 0 deletions .github/actions/resolve-release-branch/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
name: 'Resolve Release Branch'
description: >
Resolves the maintenance branch a given project version belongs to: <major>.<minor>.x
when it exists, otherwise main for OSS repositories whose main is on that same line.

inputs:
repo:
description: 'Full repository path (e.g. spring-cloud/spring-cloud-config)'
required: true
version:
description: >
The version whose line is wanted. A qualifier is stripped before the line is taken,
so 5.0.4-SNAPSHOT, 5.1.0-INTERNAL-SNAPSHOT, 5.1.0-M1 and 5.0.3 all resolve to the
same branch.
required: true
commercial:
description: >
When true the repository is a commercial one, which has no main branch to fall back
on - spring-cloud-config-commercial's default is 4.3.x - so a missing .x branch is
reported rather than guessed at.
required: false
default: 'false'
token:
description: 'GitHub token with read access to the repository'
required: true

outputs:
branch:
description: 'The resolved branch, or empty when status is not ok'
value: ${{ steps.resolve.outputs.branch }}
status:
description: >
ok | branch-not-found | version-mismatch. Callers report these rather than failing,
so one project that cannot be resolved does not take a whole matrix down.
value: ${{ steps.resolve.outputs.status }}
message:
description: 'Why, when status is not ok. Empty otherwise.'
value: ${{ steps.resolve.outputs.message }}

runs:
using: composite
steps:
- name: Resolve the branch
id: resolve
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
REPO: ${{ inputs.repo }}
VERSION: ${{ inputs.version }}
COMMERCIAL: ${{ inputs.commercial }}
run: |
node - << 'JSEOF'
const fs = require('fs');
const { execFileSync } = require('child_process');

const repo = process.env.REPO;
const version = process.env.VERSION;
const commercial = process.env.COMMERCIAL === 'true';

const out = process.env.GITHUB_OUTPUT;
const emit = (k, v) => fs.appendFileSync(out, `${k}=${v}\n`);
// Never a hard failure: a caller running this across a matrix wants the one project
// that cannot be resolved reported in its summary, not the whole run red.
const stop = (status, message) => {
console.log(message);
emit('status', status);
emit('branch', '');
emit('message', message);
process.exit(0);
};

// Drop the qualifier, then the last segment, and append .x. Works for every shape
// these repositories use: OSS 5.0.3 and 5.0.4-SNAPSHOT -> 5.0.x, 3-part commercial
// 4.2.8 -> 4.2.x, 5.1.0-INTERNAL-SNAPSHOT -> 5.1.x, 5.1.0-M1 -> 5.1.x.
const plain = version.replace(/-[A-Za-z].*$/, '');
const parts = plain.split('.');
if (parts.length < 2 || !parts.every(p => /^\d+$/.test(p))) {
stop('version-mismatch',
`ERROR: '${version}' is not a numeric version, so no branch can be derived from it.`);
}
const target = parts.slice(0, -1).join('.') + '.x';

const branchExists = branch => {
try {
execFileSync('gh', ['api', `repos/${repo}/branches/${branch}`, '--jq', '.name'],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
return true;
} catch (err) { return false; }
};

if (branchExists(target)) {
console.log(`Target branch: ${target}`);
emit('status', 'ok');
emit('branch', target);
emit('message', '');
process.exit(0);
}

// Commercial repos have no main branch at all - spring-cloud-config-commercial's
// default is 4.3.x - so there is no sane fallback to make.
if (commercial) {
stop('branch-not-found',
`ERROR: ${repo} has no ${target} branch, and commercial repos have no main to ` +
'fall back to. Skipping this project.');
}

if (!branchExists('main')) {
stop('branch-not-found', `ERROR: ${repo} has neither ${target} nor main.`);
}

// Falling back to main is only safe if main really is the line this version belongs
// to - otherwise we would act on an unrelated major.minor.
const expected = parts.slice(0, 2).join('.');
let pom;
try {
const b64 = execFileSync('gh', ['api',
`repos/${repo}/contents/pom.xml?ref=main`, '--jq', '.content'],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
pom = Buffer.from(b64.replace(/\s/g, ''), 'base64').toString('utf8');
} catch (err) {
stop('branch-not-found', `ERROR: no ${target} branch and could not read pom.xml on main.`);
}

// The root <version> is the project's own; fall back to <parent><version> when the
// root pom inherits it.
const withoutParent = pom.replace(/<parent>[\s\S]*?<\/parent>/, '');
let m = withoutParent.match(/<version>([^<]+)<\/version>/);
if (!m) {
const parent = pom.match(/<parent>[\s\S]*?<\/parent>/);
if (parent) m = parent[0].match(/<version>([^<]+)<\/version>/);
}
const pomVersion = m ? m[1].trim() : '';

if (!pomVersion.startsWith(`${expected}.`)) {
stop('version-mismatch',
`ERROR: ${repo} has no ${target} branch, and main is at '${pomVersion}', which is ` +
`not on the ${expected} line. Refusing to act on main for ${version}.`);
}

console.log(`No ${target} branch; main is at ${pomVersion} - using main.`);
emit('status', 'ok');
emit('branch', 'main');
emit('message', '');
JSEOF

branding:
icon: 'git-branch'
color: 'green'
56 changes: 42 additions & 14 deletions .github/actions/spring-release-train-project-ready/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,36 @@ A composite GitHub Action that prepares a Spring Cloud project for release train

This action orchestrates the steps required to mark a Spring Cloud project as ready within a release train:

1. **Checkout** the `release/<project-version>` branch of `spring-cloud/<project>`
2. **Update versions** using the `update-project-versions` action, resolving all dependency versions from the jenkins-releaser-config properties file for the given release train
3. **Verify** that no pre-release versions (`-SNAPSHOT`, `-RC*`, `-M*`) remain in any Maven or Gradle build file
4. **Commit and push** the version changes (if any) with the message `"Release <project-version>"`
5. **Trigger** the `release-train-ready.yml` workflow on the project's release branch
6. **Remove from Antora playbook** — removes `release/<project-version>` from `content.sources.branches` in the `antora-playbook.yml` on the repo's `docs-build` branch (no-op when the docs-build branch, playbook, or branch entry is absent)
1. **Resolve the version** for this project from the release train's properties file, and confirm it has not already been released
2. **Checkout** the `release/<version>` branch of `spring-cloud/<project>`
3. **Update versions** using the `update-project-versions` action, resolving all dependency versions from the jenkins-releaser-config properties file for the given release train
4. **Verify** that no pre-release versions (`-SNAPSHOT`, `-RC*`, `-M*`) remain in any Maven or Gradle build file
5. **Commit and push** the version changes (if any) with the message `"Release <version>"`
6. **Trigger** the `release-train-ready.yml` workflow on the project's release branch
7. **Remove from Antora playbook** — removes `release/<version>` from `content.sources.branches` in the `antora-playbook.yml` on the repo's `docs-build` branch (no-op when the docs-build branch, playbook, or branch entry is absent)

Release branches are not registered in `config/projects.json`, so nothing is removed from it here. The long-lived `<major>.<minor>.x-internal` branch **is** registered, and it is deregistered by [`retire-branch.yml`](../../workflows/retire-branch.yml) when the minor line is retired — not on every release.

If version verification fails (step 3), the action stops immediately — no commit, push, or workflow dispatch occurs.
If verification fails (step 4), the action stops immediately — no commit, push, or workflow dispatch occurs.

### The version is derived, not passed in

`spring-cloud-release-train-version` and `project` together determine everything else. This
project's entry in that train's properties file **is** the version being released —
`2026_0_0-m1.properties` says `spring-cloud-config=5.1.0-M1` — which names the
`release/5.1.0-M1` branch to check out, dispatch into and drop from the Antora playbook.
There is nothing for a separate version input to say that these two do not already fix.

Step 1 also refuses when `v<version>` already exists, in either the OSS or the commercial
repository. Release branches are not deleted after a release, so `release/5.0.5` is still
there long after 5.0.5 shipped; without that check, naming an already-released train would
re-stamp that branch and re-dispatch readiness for it.

## Inputs

| Input | Description | Required | Default |
|-------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------|
| `project` | The spring-cloud GitHub project name (e.g. `spring-cloud-config`). The action checks out `spring-cloud/<project>` at `release/<project-version>`. Append `-commercial` for commercial variants. | Yes | — |
| `project-version` | The version of the project being released (e.g. `4.2.0`). Identifies the `release/<project-version>` branch. | Yes | — |
| `project` | The spring-cloud GitHub project name (e.g. `spring-cloud-config`). Selects the repository to act on; append `-commercial` for commercial variants. The release branch inside it is derived, not passed in. | Yes | — |
| `spring-cloud-release-train-version` | The Spring Cloud release train version matching the jenkins-releaser-config properties file (e.g. `2025.0.0`). Used to resolve dependency versions. | Yes | — |
| `spring-release-train-version` | The Spring release train version to mark this project ready in (e.g. `2026.07`). Passed as the `release-train` input to the project's `release-train-ready.yml` workflow. | Yes | — |
| `token` | GitHub token for checkout, push, and workflow dispatch. | Yes | — |
Expand All @@ -42,22 +55,38 @@ jobs:
uses: spring-cloud/spring-cloud-github-actions/.github/actions/spring-release-train-project-ready@v1
with:
project: spring-cloud-config
project-version: '4.2.0'
spring-cloud-release-train-version: '2025.0.0'
spring-release-train-version: '2026.07'
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
```

### Where the releaser config comes from

**Always `spring-cloud/spring-cloud-release-commercial`, for OSS releases too** — the same
choice `post-release.yml`, `update-versions.yml`, `setup-next-release-train.yml` and
`create-oss-release-branch.yml` all make. That repository holds the releaser config for
every train now, and its train files are plain OSS train files: `2026_0_0-m1.properties` is
`spring-cloud-config=5.1.0-M1` and so on, with no commercial-only versions in it.

The OSS repository's copy of `jenkins-releaser-config` stopped at 2025.1.3 and disagrees
with reality where the two still overlap, so reading it for an OSS release would 404 on a
current train and stamp versions that were never released on an older one.

Both reads go there — the version check, and the `commercial: 'true'` passed to
`update-project-versions` — so the file validated against and the file stamped from are
always the same one.

### Commercial Variant

When the project name ends in `-commercial`, the `commercial` flag is automatically set to `true` when calling `update-project-versions`, so the releaser config is fetched from `spring-cloud-release-commercial` instead of `spring-cloud-release`.
The `-commercial` suffix selects the **project repository** to check out and dispatch into,
and nothing else. It is stripped before looking the project up in the properties file,
since the config lists `spring-cloud-config` rather than `spring-cloud-config-commercial`.

```yaml
- name: Mark spring-cloud-config-commercial ready in release train
uses: spring-cloud/spring-cloud-github-actions/.github/actions/spring-release-train-project-ready@v1
with:
project: spring-cloud-config-commercial
project-version: '4.2.0'
spring-cloud-release-train-version: '2025.0.0'
spring-release-train-version: '2026.07'
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
Expand All @@ -82,7 +111,6 @@ jobs:
- uses: spring-cloud/spring-cloud-github-actions/.github/actions/spring-release-train-project-ready@v1
with:
project: ${{ matrix.project }}
project-version: '4.2.0'
spring-cloud-release-train-version: '2025.0.0'
spring-release-train-version: '2026.07'
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
Expand Down Expand Up @@ -123,7 +151,7 @@ The action triggers `release-train-ready.yml` via `gh workflow run` with:
The token provided (or the `GH_ACTIONS_REPO_TOKEN` secret) must have:
- **Contents: write** on `spring-cloud/<project>` — to push commits to the release branch
- **Actions: write** on `spring-cloud/<project>` — to dispatch the `release-train-ready.yml` workflow
- **Contents: read** on `spring-cloud/spring-cloud-release` (or `spring-cloud-release-commercial` for commercial projects) — to fetch the jenkins-releaser-config properties file
- **Contents: read** on `spring-cloud/spring-cloud-release-commercial` — to fetch the jenkins-releaser-config properties file, for OSS releases too (see [Where the releaser config comes from](#where-the-releaser-config-comes-from))

## License

Expand Down
Loading
Loading