@@ -103,42 +103,129 @@ function getGitHubEventPayload () {
103103 return JSON . parse ( readFileSync ( path , 'utf8' ) )
104104}
105105
106- function getJobIDFromDiagFile ( runnerTemp ) {
107- if ( ! runnerTemp || ! existsSync ( runnerTemp ) ) { return null }
106+ const uniq = ( items ) => [ ...new Set ( items ) ]
107+
108+ /**
109+ * GitHub runner diagnostic logs live under the runner installation directory in `_diag`.
110+ * On many runners, we can derive the installation directory from RUNNER_TEMP:
111+ * <runnerRoot>/_work/_temp -> <runnerRoot>/_diag
112+ *
113+ * This is much more robust than relying on hardcoded paths, especially on self-hosted runners
114+ * and GHES environments where the runner may be installed under arbitrary directories/users.
115+ */
116+ function getGithubDiagnosticDirsFromEnv ( runnerTemp ) {
117+ const dirs = [ ]
118+
119+ if ( runnerTemp ) {
120+ // RUNNER_TEMP is typically: <runnerRoot>/_work/_temp
121+ const runnerRoot = path . resolve ( runnerTemp , '..' , '..' ) . replaceAll ( path . sep , '/' )
122+ // Bounded-depth patterns cover every runner layout we've observed
123+ // (including cached/<version>/_diag) without assuming a `cached` wrapper
124+ // and without walking the whole tree.
125+ dirs . push (
126+ path . posix . join ( runnerRoot , 'actions-runner' , '_diag' ) ,
127+ `${ runnerRoot } /actions-runner/*/_diag` ,
128+ `${ runnerRoot } /actions-runner/*/*/_diag` ,
129+ path . posix . join ( runnerRoot , '_diag' ) ,
130+ `${ runnerRoot } /*/_diag` ,
131+ `${ runnerRoot } /*/*/_diag`
132+ )
133+ }
108134
109- // RUNNER_TEMP usually looks like:
110- // Linux/mac hosted: /home/runner/work/_temp
111- // Windows hosted: C:\actions-runner\_work\_temp
112- // Self-hosted (unix): /opt/actions-runner/_work/_temp
135+ return uniq ( dirs . filter ( Boolean ) )
136+ }
113137
114- const workDir = path . dirname ( runnerTemp ) // .../work or .../_work
115- const runnerRoot = path . dirname ( workDir ) // /home/runner/ (runner root)
138+ function hasMagicChars ( str ) {
139+ return str . includes ( '*' ) || str . includes ( '?' )
140+ }
116141
117- const dirs = [
118- path . join ( runnerRoot , 'cached' , '_diag' ) ,
119- path . join ( runnerRoot , '_diag' ) ,
120- path . join ( runnerRoot , 'actions-runner' , 'cached' , '_diag' ) ,
121- path . join ( runnerRoot , 'actions-runner' , '_diag' ) ,
122- ]
142+ // Expands a glob pattern with only `*`/`?` at path-segment boundaries (no `**`)
143+ // into matching concrete paths using readdirSync — no external dependency needed.
144+ function expandGlobPattern ( pattern ) {
145+ const parts = pattern . split ( / [ / \\ ] / )
146+ const wildcardIdx = parts . findIndex ( p => hasMagicChars ( p ) )
147+ if ( wildcardIdx === - 1 ) return [ pattern ]
123148
124- const isWin = process . platform === 'win32'
149+ const prefix = parts . slice ( 0 , wildcardIdx ) . join ( '/' )
150+ const results = [ ]
125151
126- // Hardcoded fallbacks
127- if ( isWin ) {
128- dirs . push (
129- 'C:/actions-runner/cached/_diag' ,
130- 'C:/actions-runner/_diag' ,
131- )
132- } else {
133- dirs . push (
134- '/home/runner/actions-runner/cached/_diag' ,
135- '/home/runner/actions-runner/_diag' ,
136- '/opt/actions-runner/_diag' ,
137- )
152+ function walk ( dir , segIdx ) {
153+ if ( segIdx === parts . length ) {
154+ results . push ( dir )
155+ return
156+ }
157+ const seg = parts [ segIdx ]
158+ if ( ! hasMagicChars ( seg ) ) {
159+ walk ( `${ dir } /${ seg } ` , segIdx + 1 )
160+ return
161+ }
162+ try {
163+ const re = new RegExp (
164+ '^' + seg . replaceAll ( / [ . + ^ $ { } ( ) | [ \] \\ ] / g, String . raw `\$&` ) . replaceAll ( '*' , String . raw `[^/\\]*` ) . replaceAll ( '?' , String . raw `[^/\\]` ) + '$'
165+ )
166+ for ( const entry of readdirSync ( dir ) ) {
167+ if ( re . test ( entry ) ) {
168+ walk ( `${ dir } /${ entry } ` , segIdx + 1 )
169+ }
170+ }
171+ } catch {
172+ // directory doesn't exist or isn't accessible
173+ }
138174 }
139175
140- // Remove duplicates
141- const possibleDiagsPaths = [ ...new Set ( dirs ) ]
176+ walk ( prefix , wildcardIdx )
177+ return results
178+ }
179+
180+ /**
181+ * Expands a mixed list of literal directories and glob patterns into concrete
182+ * directories. Literals pass through unchanged (existence is checked later).
183+ */
184+ function expandDiagnosticDirCandidates ( candidates ) {
185+ const expanded = [ ]
186+ for ( const candidate of candidates ) {
187+ if ( hasMagicChars ( candidate ) ) {
188+ expanded . push ( ...expandGlobPattern ( candidate ) )
189+ } else {
190+ expanded . push ( candidate )
191+ }
192+ }
193+
194+ return uniq ( expanded )
195+ }
196+
197+ const githubWellKnownDiagnosticDirsUnix = [
198+ '/home/runner/actions-runner/_diag' ,
199+ '/opt/actions-runner/_diag' ,
200+ ]
201+ const githubWellKnownDiagnosticDirsWin = [
202+ 'C:/actions-runner/_diag' ,
203+ ]
204+
205+ // Glob patterns covering layouts that namespace `_diag` under one or two
206+ // intermediate directories. This includes both observed SaaS layouts
207+ // (<runnerRoot>/cached/_diag pre-2.334.0, <runnerRoot>/cached/<version>/_diag
208+ // since v2.334.0) and hypothetical future layouts that follow the same shape
209+ // without a `cached` wrapper (e.g. <runnerRoot>/<version>/_diag). Depth is
210+ // bounded on purpose: `*` matches a single segment, so no filesystem walk.
211+ const githubWellKnownDiagnosticDirPatternsUnix = [
212+ '/home/runner/actions-runner/*/_diag' ,
213+ '/home/runner/actions-runner/*/*/_diag' ,
214+ ]
215+ const githubWellKnownDiagnosticDirPatternsWin = [ 'C:/actions-runner/*/_diag' , 'C:/actions-runner/*/*/_diag' ]
216+
217+ const githubJobIDRegex = / " j o b " : \s * { [ \s \S ] * ?" v " \s * : \s * ( \d + ) (?: \. 0 ) ? /
218+
219+ function getJobIDFromDiagFile ( ) {
220+ const runnerTemp = getValueFromEnvSources ( 'RUNNER_TEMP' )
221+ if ( ! runnerTemp || ! existsSync ( runnerTemp ) ) { return null }
222+
223+ const isWin = process . platform === 'win32'
224+ const patterns = isWin ? githubWellKnownDiagnosticDirPatternsWin : githubWellKnownDiagnosticDirPatternsUnix
225+ const literals = isWin ? githubWellKnownDiagnosticDirsWin : githubWellKnownDiagnosticDirsUnix
226+ const possibleDiagsPaths = expandDiagnosticDirCandidates ( [
227+ ...getGithubDiagnosticDirsFromEnv ( runnerTemp ) , ...patterns , ...literals ,
228+ ] )
142229
143230 // This will hold the names of the worker log files that (potentially) contain the Job ID
144231 let workerLogFiles = [ ]
@@ -177,7 +264,7 @@ function getJobIDFromDiagFile (runnerTemp) {
177264 const filePath = path . posix . join ( chosenDiagPath , logFile )
178265 const content = readFileSync ( filePath , 'utf8' )
179266
180- const match = content . match ( / " j o b " : \s * { [ \s \S ] * ? " v " \s * : \s * ( \d + ) (?: \. 0 ) ? / )
267+ const match = content . match ( githubJobIDRegex )
181268
182269 // match[1] is the captured group with the display name
183270 if ( match && match [ 1 ] ) { return match [ 1 ] }
@@ -188,6 +275,7 @@ function getJobIDFromDiagFile (runnerTemp) {
188275
189276module . exports = {
190277 normalizeRef,
278+ expandGlobPattern,
191279 getJobIDFromDiagFile,
192280 getCIMetadata ( ) {
193281 const env = getEnvironmentVariables ( )
@@ -366,7 +454,6 @@ module.exports = {
366454 GITHUB_RUN_ATTEMPT ,
367455 GITHUB_JOB ,
368456 GITHUB_BASE_REF ,
369- RUNNER_TEMP ,
370457 JOB_CHECK_RUN_ID ,
371458 } = env
372459
@@ -378,7 +465,7 @@ module.exports = {
378465 }
379466
380467 // Build the job url extracting the job ID. If extraction fails, job url is constructed as a generalized url
381- const GITHUB_JOB_ID = JOB_CHECK_RUN_ID ?? getJobIDFromDiagFile ( RUNNER_TEMP )
468+ const GITHUB_JOB_ID = JOB_CHECK_RUN_ID ?? getJobIDFromDiagFile ( )
382469 const jobUrl =
383470 GITHUB_JOB_ID === null
384471 ? `${ GITHUB_SERVER_URL } /${ GITHUB_REPOSITORY } /commit/${ GITHUB_SHA } /checks`
0 commit comments