Skip to content

Commit 65b584f

Browse files
authored
Merge pull request #125 from morri-son/enhance-controller-release
Enhance controller release
2 parents cba790a + f126fef commit 65b584f

File tree

6 files changed

+953
-101
lines changed

6 files changed

+953
-101
lines changed

.github/scripts/prepare-registry-constructor.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @ts-check
22
import fs from 'fs';
33
import yaml from 'js-yaml';
4-
import {computeNextVersions} from "./compute-rc-version.js";
4+
import {computeNextVersions} from "./release-versioning.js";
55
import {execSync} from "child_process";
66
import {dirname} from "path";
77

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// @ts-check
2-
import { execSync } from "child_process";
2+
import { execFileSync } from "child_process";
33

44
// --------------------------
55
// GitHub Actions entrypoint
@@ -17,8 +17,25 @@ export default async function computeRcVersion({ core }) {
1717
const basePrefix = parseBranch(releaseBranch);
1818
const tagPrefix = `${componentPath}/v`;
1919

20-
const latestStable = run(core, `git tag --list '${tagPrefix}${basePrefix}.*' | sort -V | tail -n1`);
21-
const latestRc = run(core, `git tag --list '${tagPrefix}${basePrefix}.*-rc.*' | sort -V | tail -n1`);
20+
// Get latest stable tag using Git's native version sort (descending)
21+
// Filter out RC tags after fetching since git doesn't support negative pattern matching
22+
const stableTags = run(core, "git", [
23+
"tag", "--list", `${tagPrefix}${basePrefix}.*`,
24+
"--sort=-version:refname"
25+
]);
26+
const latestStable = stableTags
27+
.split("\n")
28+
.filter(tag => tag && !/-rc\.\d+$/.test(tag))[0] || "";
29+
30+
// Get latest RC tag using Git's native version sort (descending)
31+
const rcTags = run(core, "git", [
32+
"tag", "--list", `${tagPrefix}${basePrefix}.*-rc.*`,
33+
"--sort=-version:refname"
34+
]);
35+
const latestRc = rcTags.split("\n").filter(Boolean)[0] || "";
36+
37+
core.info(`Latest stable: ${latestStable || "(none)"}`);
38+
core.info(`Latest RC: ${latestRc || "(none)"}`);
2239

2340
const { baseVersion, rcVersion } = computeNextVersions(basePrefix, latestStable, latestRc, false);
2441

@@ -56,14 +73,22 @@ export default async function computeRcVersion({ core }) {
5673
// --------------------------
5774
// Core helpers
5875
// --------------------------
59-
export function run(core, cmd) {
60-
core.info(`> ${cmd}`);
76+
/**
77+
* Run a shell command safely using execFileSync.
78+
* @param {*} core - GitHub Actions core module
79+
* @param {string} executable - The executable to run (e.g., "git", "grep")
80+
* @param {string[]} args - Array of arguments
81+
* @returns {string} Command output or empty string on failure
82+
*/
83+
export function run(core, executable, args) {
84+
const cmdStr = `${executable} ${args.join(" ")}`;
85+
core.info(`> ${cmdStr}`);
6186
try {
62-
const out = execSync(cmd).toString().trim();
87+
const out = execFileSync(executable, args, { encoding: "utf-8" }).trim();
6388
if (out) core.info(`Output: ${out}`);
6489
return out;
6590
} catch (err) {
66-
core.warning(`Command failed: ${cmd}\n${err.message}`);
91+
core.warning(`Command failed: ${cmdStr}\n${err.message}`);
6792
return "";
6893
}
6994
}
@@ -166,7 +191,7 @@ export function isStableNewer(stable, rc) {
166191
const stableParts = parseVersion(stable);
167192
const rcParts = parseVersion(rc);
168193

169-
// Compare [major, minor, patch] lexicographically
194+
// Compare [major, minor, patch] numerically
170195
for (let i = 0; i < 3; i++) {
171196
const s = stableParts[i] || 0;
172197
const r = rcParts[i] || 0;
@@ -190,3 +215,45 @@ export function parseVersion(tag) {
190215
const version = tag.replace(/^.*v/, "").replace(/-rc\.\d+$/, "");
191216
return version.split(".").map(Number);
192217
}
218+
219+
// --------------------------
220+
// Latest release determination
221+
// --------------------------
222+
223+
/** GitHub Actions entrypoint for determining if release should be latest */
224+
export async function determineLatestRelease({ core, github, context }) {
225+
const { COMPONENT_PATH: componentPath, PROMOTION_VERSION: promotionVersion } = process.env;
226+
if (!componentPath || !promotionVersion) return core.setFailed("Missing COMPONENT_PATH or PROMOTION_VERSION");
227+
228+
const tagPrefix = `${componentPath}/v`;
229+
let releases = [];
230+
try {
231+
releases = (await github.rest.repos.listReleases({ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 })).data;
232+
} catch (e) {
233+
core.setFailed(`Could not fetch releases: ${e.message}`);
234+
return;
235+
}
236+
237+
const highestFinal = extractHighestFinalVersion(releases, tagPrefix);
238+
const setLatest = shouldSetLatest(promotionVersion, highestFinal);
239+
240+
core.setOutput('set_latest', setLatest ? 'true' : 'false');
241+
core.setOutput('highest_final_version', highestFinal || '(none)');
242+
core.info(setLatest ? `✅ Will set :latest (${promotionVersion} >= ${highestFinal || 'none'})` : `⚠️ Will NOT set :latest (${promotionVersion} < ${highestFinal})`);
243+
244+
await core.summary.addRaw('---').addEOL().addHeading('Latest Tag Decision', 2)
245+
.addTable([[{ data: 'Field', header: true }, { data: 'Value', header: true }], ['Final Version', promotionVersion], ['Highest Final Version', highestFinal || '(none)'], ['Will Set Latest', setLatest ? '✅ Yes' : '⚠️ No']]).write();
246+
}
247+
248+
/** Extract highest final (non-prerelease) version from releases */
249+
export function extractHighestFinalVersion(releases, tagPrefix) {
250+
const versions = releases.filter(r => !r.prerelease && r.tag_name.startsWith(tagPrefix))
251+
.map(r => r.tag_name.replace(tagPrefix, '')).filter(v => /^\d+\.\d+\.\d+$/.test(v));
252+
if (!versions.length) return '';
253+
return versions.sort((a, b) => isStableNewer(`v${a}`, `v${b}`) ? 1 : -1).pop();
254+
}
255+
256+
/** Determine if promotion version should be tagged as latest */
257+
export function shouldSetLatest(promotionVersion, highestFinal) {
258+
return !highestFinal || !isStableNewer(`v${highestFinal}`, `v${promotionVersion}`);
259+
}

.github/scripts/compute-rc-version.test.js renamed to .github/scripts/release-versioning.test.js

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import assert from "assert";
2-
import { computeNextVersions, isStableNewer, parseBranch, parseVersion } from "./compute-rc-version.js";
2+
import {
3+
computeNextVersions,
4+
isStableNewer,
5+
parseBranch,
6+
parseVersion,
7+
extractHighestFinalVersion,
8+
shouldSetLatest,
9+
} from "./release-versioning.js";
310

411
// ----------------------------------------------------------
512
// parseVersion tests
@@ -31,12 +38,19 @@ assert.deepStrictEqual(v1, {
3138
rcVersion: "0.1.1-rc.1",
3239
}, "RC version should be bumped when starting from a stable version");
3340

34-
// 2. New RC from existing RC (RC increments)
41+
// 2. Stable + RC on same base => start next minor RC line
3542
const v2 = computeNextVersions("0.1", "cli/v0.1.1", "cli/v0.1.1-rc.4", false);
3643
assert.deepStrictEqual(v2, {
3744
baseVersion: "0.1.2",
3845
rcVersion: "0.1.2-rc.1",
39-
}, "RC version should be incremented when starting from an existing RC");
46+
}, "When stable exists for the same base as latest RC, start next minor RC line");
47+
48+
// 2b. No stable yet for current line + existing RC => continue same RC line
49+
const v2b = computeNextVersions("0.4", "", "cli/v0.4.0-rc.3", false);
50+
assert.deepStrictEqual(v2b, {
51+
baseVersion: "0.4.0",
52+
rcVersion: "0.4.0-rc.4",
53+
}, "Without a stable tag, RC line must continue on the same base");
4054

4155
// 3. Same base between stable and RC with minor version bump
4256
const v3 = computeNextVersions("0.1", "cli/v0.1.0", "cli/v0.1.0-rc.4", true);
@@ -112,4 +126,68 @@ assert.ok(
112126
"Should return false if no stable tag"
113127
);
114128

115-
console.log("✅ All tests passed.");
129+
// ----------------------------------------------------------
130+
// extractHighestFinalVersion tests
131+
// ----------------------------------------------------------
132+
const mockReleases = [
133+
{ prerelease: false, tag_name: "cli/v0.1.0" },
134+
{ prerelease: true, tag_name: "cli/v0.1.1-rc.1" },
135+
{ prerelease: false, tag_name: "cli/v0.1.2" },
136+
{ prerelease: false, tag_name: "cli/v0.2.0" },
137+
{ prerelease: false, tag_name: "other/v1.0.0" },
138+
{ prerelease: true, tag_name: "cli/v0.3.0-rc.1" },
139+
];
140+
141+
assert.strictEqual(
142+
extractHighestFinalVersion(mockReleases, "cli/v"),
143+
"0.2.0",
144+
"Should return highest non-prerelease version for prefix"
145+
);
146+
147+
assert.strictEqual(
148+
extractHighestFinalVersion(mockReleases, "other/v"),
149+
"1.0.0",
150+
"Should filter by tag prefix"
151+
);
152+
153+
assert.strictEqual(
154+
extractHighestFinalVersion([], "cli/v"),
155+
"",
156+
"Should return empty string for no releases"
157+
);
158+
159+
assert.strictEqual(
160+
extractHighestFinalVersion([{ prerelease: true, tag_name: "cli/v0.1.0-rc.1" }], "cli/v"),
161+
"",
162+
"Should return empty string if only prereleases exist"
163+
);
164+
165+
// ----------------------------------------------------------
166+
// shouldSetLatest tests
167+
// ----------------------------------------------------------
168+
assert.ok(
169+
shouldSetLatest("0.2.0", ""),
170+
"Should return true if no existing final version"
171+
);
172+
173+
assert.ok(
174+
shouldSetLatest("0.2.0", "0.1.0"),
175+
"Should return true if promotion > highest"
176+
);
177+
178+
assert.ok(
179+
shouldSetLatest("0.2.0", "0.2.0"),
180+
"Should return true if promotion == highest"
181+
);
182+
183+
assert.ok(
184+
!shouldSetLatest("0.1.0", "0.2.0"),
185+
"Should return false if promotion < highest"
186+
);
187+
188+
assert.ok(
189+
shouldSetLatest("0.10.0", "0.9.0"),
190+
"Should handle numeric comparison correctly (0.10 > 0.9)"
191+
);
192+
193+
console.log("✅ All tests passed.");

0 commit comments

Comments
 (0)