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 && ! / - r c \. \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 ( / - r c \. \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+ }
0 commit comments