Skip to content

Commit c832bf7

Browse files
authored
[test optimization] Update extraction of runner diagnostic dir using glob pattern matching (#8115)
1 parent 41789b4 commit c832bf7

6 files changed

Lines changed: 383 additions & 34 deletions

File tree

packages/dd-trace/src/plugins/util/ci.js

Lines changed: 119 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = /"job":\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(/"job":\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

189276
module.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`

packages/dd-trace/test/plugins/util/fixtures/runner/actions-runner/_diag/Worker_20240115-102345-12345.log renamed to packages/dd-trace/test/plugins/util/fixtures/runner/actions-runner/cached/2.334.0/_diag/Worker_20240115-102345-12345.log

File renamed without changes.

packages/dd-trace/test/plugins/util/fixtures/runner_empty/actions-runner/_diag/Worker_empty.log renamed to packages/dd-trace/test/plugins/util/fixtures/runner_empty/actions-runner/cached/2.334.0/_diag/Worker_empty.log

File renamed without changes.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
[2024-01-15 10:23:45Z INFO WorkerMessageServer] THIS IS A MOCK DIAGNOSTICS FILE CREATED BY CLAUDE FOR TESTING getJobIDFromDiagFile FUNCTION
2+
[2024-01-15 10:23:45Z INFO WorkerMessageServer] Worker starting, pid: 12345
3+
[2024-01-15 10:23:45Z INFO WorkerMessageServer] Connecting to runner process
4+
[2024-01-15 10:23:45Z INFO WorkerMessageServer] Connection established
5+
[2024-01-15 10:23:46Z INFO Worker] Processing JobRequest message
6+
[2024-01-15 10:23:46Z INFO Worker] Received job message:
7+
{
8+
"messageType": "PipelineAgentJobRequest",
9+
"plan": {
10+
"scopeIdentifier": "3a7e8b2d-1f4c-4e9a-b6d3-9c2f1a8e5d7b",
11+
"planType": "Build",
12+
"planId": "f2c4a6e8-3b5d-4f7a-9c1e-2d4f6a8c0e2a",
13+
"version": "1",
14+
"artifactUri": "https://pipelines.actions.githubusercontent.com/abc123XYZ",
15+
"requestedForId": "1234567"
16+
},
17+
"timeline": {
18+
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
19+
"changeId": 1,
20+
"location": null
21+
},
22+
"jobId": "d4e5f6a7-b8c9-0123-def4-567890abcdef",
23+
"jobDisplayName": "build-and-test",
24+
"jobName": "build-and-test",
25+
"requestId": 9876543210,
26+
"lockedUntil": "2024-01-15T11:23:46.000Z",
27+
"resources": {
28+
"endpoints": [
29+
{
30+
"data": {
31+
"downloadUrl": "https://codeload.github.com/"
32+
},
33+
"name": "GITHUB_TOKEN",
34+
"url": "https://api.github.com/",
35+
"authorization": {
36+
"scheme": "OAuth",
37+
"parameters": {
38+
"accessToken": "***"
39+
}
40+
},
41+
"isShared": false,
42+
"isReady": true
43+
}
44+
],
45+
"files": [],
46+
"repositories": [
47+
{
48+
"alias": "self",
49+
"id": "repo-id-abc123",
50+
"type": "GitHub",
51+
"name": "my-org/my-repo",
52+
"url": "https://github.com/my-org/my-repo",
53+
"version": "abc123def456",
54+
"ref": "refs/heads/main"
55+
}
56+
],
57+
"containers": []
58+
},
59+
"variables": {
60+
"system.github.job": { "value": "build-and-test", "isSecret": false },
61+
"GITHUB_RUN_ID": { "value": "7654321", "isSecret": false },
62+
"GITHUB_RUN_NUMBER": { "value": "42", "isSecret": false },
63+
"GITHUB_WORKFLOW": { "value": "CI", "isSecret": false }
64+
},
65+
"steps": [
66+
{
67+
"type": "Task",
68+
"id": "step-id-0001",
69+
"name": "actions/checkout",
70+
"displayName": "Checkout repository",
71+
"enabled": true,
72+
"continueOnError": false,
73+
"condition": "succeeded()",
74+
"timeoutInMinutes": 0,
75+
"inputs": {
76+
"repository": "my-org/my-repo",
77+
"ref": "refs/heads/main",
78+
"token": "***"
79+
},
80+
"environment": {},
81+
"retryCountOnTaskFailure": 0
82+
}
83+
],
84+
"contextData": {},
85+
"workspace": { "clean": null },
86+
"mask": [
87+
{ "type": "regex", "value": "\\*\\*\\*" }
88+
],
89+
"oidcToken": null,
90+
"finaStrategy": null,
91+
"job": {
92+
"v": 9876543210,
93+
"name": "build-and-test",
94+
"id": "d4e5f6a7-b8c9-0123-def4-567890abcdef",
95+
"attempt": 1,
96+
"workflowName": "CI",
97+
"headBranch": "refs/heads/main",
98+
"headSha": "abc123def456abc123def456abc123def456abc1"
99+
}
100+
}
101+
[2024-01-15 10:23:46Z INFO Worker] Job assignment accepted, jobId: d4e5f6a7-b8c9-0123-def4-567890abcdef
102+
[2024-01-15 10:23:46Z INFO JobRunner] Starting job execution
103+
[2024-01-15 10:23:47Z INFO StepRunner] Running step: Checkout repository
104+
[2024-01-15 10:24:01Z INFO StepRunner] Step completed: Checkout repository (result: Succeeded)
105+
[2024-01-15 10:24:01Z INFO JobRunner] All steps completed
106+
[2024-01-15 10:24:02Z INFO Worker] Job completed, result: Succeeded
107+
[2024-01-15 10:24:02Z INFO WorkerMessageServer] Sending job result to runner process
108+
[2024-01-15 10:24:02Z INFO WorkerMessageServer] Worker shutting down
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
This file exists to simulate the structure of github actions runner
2+
It is here just so github can keep track of the whole directory structure

0 commit comments

Comments
 (0)