Skip to content

Commit 096d617

Browse files
committed
Initial support for release candidates and milestones in release automation
Fixes #21
1 parent 5b29e56 commit 096d617

12 files changed

Lines changed: 1189 additions & 85 deletions

File tree

‎.github/actions/spring-release-train-project-ready/action.yml‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,22 @@ runs:
5858
release_repo="spring-cloud/spring-cloud-release"
5959
fi
6060
61-
filename="${release_train_version//./_}.properties"
61+
# Deliberately identical to releaseTrainVersionToFileName in
62+
# update-project-versions/src/index.js, which is the action that reads this same
63+
# file a few steps below: a pre-release qualifier is lower-cased, the numeric part
64+
# is not. 2026.0.0-M1 -> 2026_0_0-m1.properties, 2026.0.0 -> 2026_0_0.properties.
65+
# Upper-casing here would validate one file and stamp from another - invisible for
66+
# GA versions, which carry no qualifier at all.
67+
# tr rather than bash's ${x,,} or GNU sed's \L, both of which are unavailable in
68+
# the bash 3.2 on a developer's Mac and so could not be exercised outside a runner.
69+
if [[ "$release_train_version" == *-* ]]; then
70+
lowered_qualifier=$(printf '%s' "${release_train_version#*-}" \
71+
| tr '[:upper:]' '[:lower:]')
72+
normalized="${release_train_version%%-*}-${lowered_qualifier}"
73+
else
74+
normalized="$release_train_version"
75+
fi
76+
filename="${normalized//./_}.properties"
6277
echo "Fetching ${filename} from ${release_repo}@jenkins-releaser-config..."
6378
6479
content=$(gh api "repos/${release_repo}/contents/${filename}?ref=jenkins-releaser-config" \
@@ -121,10 +136,16 @@ runs:
121136
echo "release-ci-settings.xml not found — skipping."
122137
fi
123138
139+
# A milestone or release candidate stamps -M<n>/-RC<n> versions on purpose, and its
140+
# dependencies are a mixture of milestone, release-candidate and GA versions. Only
141+
# -SNAPSHOT is always wrong, so the milestone half of the check is turned off for a
142+
# pre-release. Derived from the version being released rather than taken as an input:
143+
# the two can never legitimately disagree.
124144
- name: Verify no snapshot versions
125145
uses: ./.github/actions/verify-no-snapshot-versions
126146
with:
127147
directory: ${{ inputs.project }}
148+
allow-prerelease: ${{ contains(inputs.project-version, '-M') || contains(inputs.project-version, '-RC') }}
128149

129150
- name: Commit and push changes
130151
shell: bash

‎.github/actions/verify-no-snapshot-versions/README.md‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Any other key has its value checked only when the value is *shaped* like a versi
3535

3636
| Input | Description | Required | Default |
3737
|-------|-------------|----------|---------|
38+
| `allow-prerelease` | Permit `-M<n>` and `-RC<n>` versions. Set when verifying a milestone or release candidate. `-SNAPSHOT` is rejected either way. | No | `false` |
3839
| `directory` | Root directory of the project to verify | No | `.` |
3940
| `exclude-patterns` | Newline-separated list of regular expressions. Any file whose absolute path matches one of these patterns is excluded from version checking. | No | See below |
4041

@@ -123,11 +124,33 @@ steps:
123124

124125
Any version value matching one of these patterns (case-insensitive) is a violation:
125126

126-
| Pattern | Example |
127-
|---------|---------|
128-
| `-SNAPSHOT` | `4.2.0-SNAPSHOT` |
129-
| `-RC<N>` | `3.3.0-RC1`, `3.3.0-RC2` |
130-
| `-M<N>` | `2023.0.0-M1`, `4.2.0-M12` |
127+
| Pattern | Example | Flagged by default | Flagged with `allow-prerelease` |
128+
|---------|---------|--------------------|---------------------------------|
129+
| `-SNAPSHOT` | `4.2.0-SNAPSHOT` | Yes | Yes |
130+
| `-RC<N>` | `3.3.0-RC1`, `3.3.0-RC2` | Yes | No |
131+
| `-M<N>` | `2023.0.0-M1`, `4.2.0-M12` | Yes | No |
132+
133+
### Milestone and release candidate releases
134+
135+
A GA release must contain nothing but GA versions, which is the default.
136+
137+
A milestone or release candidate is different: it stamps `-M<n>`/`-RC<n>` on the project
138+
itself, and it depends on the other projects in the train at *their* pre-release versions
139+
while still depending on GA versions of everything outside it. So a `5.1.0-M1` build
140+
legitimately contains a mixture of `-M<n>`, `-RC<n>` and plain versions, and the default
141+
check would report every one of them.
142+
143+
`allow-prerelease: true` relaxes exactly that, and nothing else. `-SNAPSHOT` is still a
144+
violation, because a snapshot is a moving target that must never be published in any
145+
release.
146+
147+
This is a blanket allow rather than a match against the specific version being released —
148+
during the `5.1.0-RC1` release a stale `5.1.0-M1` left somewhere would pass. That is
149+
deliberate: a pre-release train carries a mixture of phases by design, so there is no
150+
single correct version to match against.
151+
152+
`spring-release-train-project-ready` sets this automatically from the version it is
153+
releasing; callers rarely need to pass it by hand.
131154

132155
## Suppressing Individual Version Checks in pom.xml
133156

‎.github/actions/verify-no-snapshot-versions/__tests__/index.test.js‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,38 @@ describe('isPreRelease', () => {
5858
});
5959
});
6060

61+
// ─── isPreRelease with allow-prerelease ──────────────────────────────────────
62+
// A milestone or release-candidate build legitimately mixes -M<n>, -RC<n> and GA
63+
// versions, so those are permitted; -SNAPSHOT never is.
64+
65+
describe('isPreRelease with allowPrerelease', () => {
66+
it('still rejects -SNAPSHOT', () => {
67+
expect(isPreRelease('4.2.0-SNAPSHOT', true)).toBe(true);
68+
});
69+
70+
it('still rejects -SNAPSHOT whatever its casing', () => {
71+
expect(isPreRelease('4.2.0-snapshot', true)).toBe(true);
72+
});
73+
74+
it('permits a milestone version', () => {
75+
expect(isPreRelease('4.2.0-M1', true)).toBe(false);
76+
});
77+
78+
it('permits a release candidate version', () => {
79+
expect(isPreRelease('3.3.0-RC1', true)).toBe(false);
80+
});
81+
82+
it('permits a milestone alongside a release candidate and a GA version', () => {
83+
for (const v of ['5.1.0-M1', '5.1.0-RC1', '4.2.3']) {
84+
expect(isPreRelease(v, true)).toBe(false);
85+
}
86+
});
87+
88+
it('leaves GA versions alone', () => {
89+
expect(isPreRelease('4.2.0', true)).toBe(false);
90+
});
91+
});
92+
6193
// ─── looksLikeVersion ────────────────────────────────────────────────────────
6294

6395
describe('looksLikeVersion', () => {

‎.github/actions/verify-no-snapshot-versions/action.yml‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
name: 'Verify No Snapshot Versions'
2-
description: 'Verifies that every version in every Maven and Gradle build file in a Spring Cloud project is a release version (no -SNAPSHOT, -RC*, or -M* versions), wherever it is declared'
2+
description: 'Verifies that every version in every Maven and Gradle build file in a Spring Cloud project is a release version (no -SNAPSHOT, and no -RC*/-M* unless allow-prerelease is set), wherever it is declared'
33
author: 'Spring Cloud Team'
44

55
inputs:
6+
allow-prerelease:
7+
description: >
8+
Permit -M<n> and -RC<n> versions. Set this when verifying a milestone or release
9+
candidate: such a build legitimately carries a mixture of milestone,
10+
release-candidate and GA versions, and the default would reject every one of them.
11+
-SNAPSHOT is rejected either way.
12+
required: false
13+
default: 'false'
614
directory:
715
description: 'Root directory of the project to verify. Defaults to the current working directory.'
816
required: false

‎.github/actions/verify-no-snapshot-versions/dist/index.js‎

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28229,12 +28229,19 @@ const fs = __nccwpck_require__(9896);
2822928229
const path = __nccwpck_require__(6928);
2823028230

2823128231
/**
28232-
* Matches pre-release version suffixes that must not appear in a release build:
28232+
* Matches the one suffix that must never appear in any release build, milestone
28233+
* and release candidate included:
2823328234
* -SNAPSHOT (e.g. 4.1.0-SNAPSHOT)
28235+
*/
28236+
const SNAPSHOT_PATTERN = /-SNAPSHOT$/i;
28237+
28238+
/**
28239+
* Matches the pre-release suffixes that are forbidden in a GA build but expected
28240+
* in a milestone or release candidate one:
2823428241
* -RC<N> (e.g. 3.2.0-RC1)
2823528242
* -M<N> (e.g. 2023.0.0-M1)
2823628243
*/
28237-
const PRE_RELEASE_PATTERN = /-SNAPSHOT$|-RC\d+$|-M\d+$/i;
28244+
const MILESTONE_PATTERN = /-(RC|M)\d+$/i;
2823828245

2823928246
/**
2824028247
* Matches values that are shaped like a version number: an optional leading `v`,
@@ -28254,8 +28261,31 @@ const COORDINATE_ELEMENTS = new Set(['dependency', 'plugin', 'extension']);
2825428261

2825528262
const CHECK_OFF_ANNOTATION = '@releaser:version-check-off';
2825628263

28257-
function isPreRelease(version) {
28258-
return PRE_RELEASE_PATTERN.test(String(version).trim());
28264+
/**
28265+
* Whether milestone and release-candidate versions are tolerated. Set once from the
28266+
* action input at the top of run(), and read by isPreRelease below.
28267+
*
28268+
* Module state rather than a parameter threaded through checkPomFile, walkPomNode,
28269+
* checkGradlePropertiesContent and the rest: the flag is a property of the run, not of
28270+
* any one file, and passing it down eight signatures would obscure them for no gain.
28271+
* isPreRelease still takes an explicit override so the unit tests need no setup.
28272+
*/
28273+
let allowPrereleaseVersions = false;
28274+
28275+
/**
28276+
* True when `version` must not appear in the build being verified.
28277+
*
28278+
* -SNAPSHOT always counts. -M<n> and -RC<n> count only when the run is verifying a GA
28279+
* release: a milestone or release-candidate build legitimately carries a mixture of
28280+
* milestone, release-candidate and GA versions, and rejecting them would fail every
28281+
* pre-release the moment it was stamped.
28282+
*
28283+
* Exported for unit testing.
28284+
*/
28285+
function isPreRelease(version, allowPrerelease = allowPrereleaseVersions) {
28286+
const value = String(version).trim();
28287+
if (SNAPSHOT_PATTERN.test(value)) return true;
28288+
return !allowPrerelease && MILESTONE_PATTERN.test(value);
2825928289
}
2826028290

2826128291
/**
@@ -28285,6 +28315,12 @@ async function run() {
2828528315
try {
2828628316
const directory = path.resolve(core.getInput('directory') || '.');
2828728317

28318+
allowPrereleaseVersions = core.getBooleanInput('allow-prerelease');
28319+
if (allowPrereleaseVersions) {
28320+
core.info('allow-prerelease is set: -M<n> and -RC<n> versions are permitted. ' +
28321+
'-SNAPSHOT versions are still rejected.');
28322+
}
28323+
2828828324
if (!fs.existsSync(directory)) {
2828928325
core.setFailed(`Directory not found: ${directory}`);
2829028326
return;
@@ -28321,16 +28357,23 @@ async function run() {
2832128357
core.setOutput('violations', JSON.stringify(allViolations));
2832228358

2832328359
if (allViolations.length === 0) {
28324-
core.info('All versions are release versions. No pre-release versions found.');
28360+
core.info(allowPrereleaseVersions
28361+
? 'No -SNAPSHOT versions found.'
28362+
: 'All versions are release versions. No pre-release versions found.');
2832528363
return;
2832628364
}
2832728365

28328-
core.error(`Found ${allViolations.length} pre-release version(s):`);
28366+
const noun = allowPrereleaseVersions ? 'SNAPSHOT' : 'pre-release';
28367+
core.error(`Found ${allViolations.length} ${noun} version(s):`);
2832928368
for (const v of allViolations) {
2833028369
core.error(` ${v.file}: ${v.location} = ${v.version}`);
2833128370
}
2833228371
core.setFailed(
28333-
`${allViolations.length} pre-release version(s) found. All dependencies must use release versions.`
28372+
`${allViolations.length} ${noun} version(s) found. ` +
28373+
(allowPrereleaseVersions
28374+
? 'A milestone or release candidate may depend on -M<n> and -RC<n> versions, ' +
28375+
'but never on a -SNAPSHOT.'
28376+
: 'All dependencies must use release versions.')
2833428377
);
2833528378
} catch (error) {
2833628379
core.setFailed(`Action failed: ${error.message}`);

‎.github/actions/verify-no-snapshot-versions/src/index.js‎

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ const fs = require('fs');
44
const path = require('path');
55

66
/**
7-
* Matches pre-release version suffixes that must not appear in a release build:
7+
* Matches the one suffix that must never appear in any release build, milestone
8+
* and release candidate included:
89
* -SNAPSHOT (e.g. 4.1.0-SNAPSHOT)
10+
*/
11+
const SNAPSHOT_PATTERN = /-SNAPSHOT$/i;
12+
13+
/**
14+
* Matches the pre-release suffixes that are forbidden in a GA build but expected
15+
* in a milestone or release candidate one:
916
* -RC<N> (e.g. 3.2.0-RC1)
1017
* -M<N> (e.g. 2023.0.0-M1)
1118
*/
12-
const PRE_RELEASE_PATTERN = /-SNAPSHOT$|-RC\d+$|-M\d+$/i;
19+
const MILESTONE_PATTERN = /-(RC|M)\d+$/i;
1320

1421
/**
1522
* Matches values that are shaped like a version number: an optional leading `v`,
@@ -29,8 +36,31 @@ const COORDINATE_ELEMENTS = new Set(['dependency', 'plugin', 'extension']);
2936

3037
const CHECK_OFF_ANNOTATION = '@releaser:version-check-off';
3138

32-
function isPreRelease(version) {
33-
return PRE_RELEASE_PATTERN.test(String(version).trim());
39+
/**
40+
* Whether milestone and release-candidate versions are tolerated. Set once from the
41+
* action input at the top of run(), and read by isPreRelease below.
42+
*
43+
* Module state rather than a parameter threaded through checkPomFile, walkPomNode,
44+
* checkGradlePropertiesContent and the rest: the flag is a property of the run, not of
45+
* any one file, and passing it down eight signatures would obscure them for no gain.
46+
* isPreRelease still takes an explicit override so the unit tests need no setup.
47+
*/
48+
let allowPrereleaseVersions = false;
49+
50+
/**
51+
* True when `version` must not appear in the build being verified.
52+
*
53+
* -SNAPSHOT always counts. -M<n> and -RC<n> count only when the run is verifying a GA
54+
* release: a milestone or release-candidate build legitimately carries a mixture of
55+
* milestone, release-candidate and GA versions, and rejecting them would fail every
56+
* pre-release the moment it was stamped.
57+
*
58+
* Exported for unit testing.
59+
*/
60+
function isPreRelease(version, allowPrerelease = allowPrereleaseVersions) {
61+
const value = String(version).trim();
62+
if (SNAPSHOT_PATTERN.test(value)) return true;
63+
return !allowPrerelease && MILESTONE_PATTERN.test(value);
3464
}
3565

3666
/**
@@ -60,6 +90,12 @@ async function run() {
6090
try {
6191
const directory = path.resolve(core.getInput('directory') || '.');
6292

93+
allowPrereleaseVersions = core.getBooleanInput('allow-prerelease');
94+
if (allowPrereleaseVersions) {
95+
core.info('allow-prerelease is set: -M<n> and -RC<n> versions are permitted. ' +
96+
'-SNAPSHOT versions are still rejected.');
97+
}
98+
6399
if (!fs.existsSync(directory)) {
64100
core.setFailed(`Directory not found: ${directory}`);
65101
return;
@@ -96,16 +132,23 @@ async function run() {
96132
core.setOutput('violations', JSON.stringify(allViolations));
97133

98134
if (allViolations.length === 0) {
99-
core.info('All versions are release versions. No pre-release versions found.');
135+
core.info(allowPrereleaseVersions
136+
? 'No -SNAPSHOT versions found.'
137+
: 'All versions are release versions. No pre-release versions found.');
100138
return;
101139
}
102140

103-
core.error(`Found ${allViolations.length} pre-release version(s):`);
141+
const noun = allowPrereleaseVersions ? 'SNAPSHOT' : 'pre-release';
142+
core.error(`Found ${allViolations.length} ${noun} version(s):`);
104143
for (const v of allViolations) {
105144
core.error(` ${v.file}: ${v.location} = ${v.version}`);
106145
}
107146
core.setFailed(
108-
`${allViolations.length} pre-release version(s) found. All dependencies must use release versions.`
147+
`${allViolations.length} ${noun} version(s) found. ` +
148+
(allowPrereleaseVersions
149+
? 'A milestone or release candidate may depend on -M<n> and -RC<n> versions, ' +
150+
'but never on a -SNAPSHOT.'
151+
: 'All dependencies must use release versions.')
109152
);
110153
} catch (error) {
111154
core.setFailed(`Action failed: ${error.message}`);

0 commit comments

Comments
 (0)