code-foundry 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/code-foundry.yml +1 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/codeql.yml +7 -2
- package/.oxfmtrc.json +7 -0
- package/.oxlintrc.json +5 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +3 -2
- package/docs/CONFIGURATION.md +24 -24
- package/docs/WORKFLOWS.md +16 -16
- package/package.json +20 -17
- package/src/cli.mjs +60 -14
- package/src/commands/ci.mjs +166 -57
- package/src/commands/doctor.mjs +98 -31
- package/src/commands/fleet.mjs +139 -29
- package/src/commands/release.mjs +378 -96
- package/src/commands/sync.mjs +332 -104
- package/src/lib/config.mjs +8 -2
- package/src/lib/github-doctor.mjs +153 -51
- package/src/lib/overlay.mjs +6 -2
- package/src/lib/profile.mjs +26 -8
- package/src/lib/release-hook.mjs +17 -5
- package/src/lib/release-manifest.mjs +81 -19
- package/src/lib/release-policy.mjs +157 -55
- package/src/lib/test-discovery.mjs +7 -2
- package/src/runtime.mjs +259 -61
- package/.prettierignore +0 -9
- package/.prettierrc +0 -6
|
@@ -24,8 +24,11 @@ const DEFAULT_RELEASE_FILES = new Set([
|
|
|
24
24
|
export function readReleaseConfig(root) {
|
|
25
25
|
const file = join(root, 'release-please-config.json')
|
|
26
26
|
if (!existsSync(file)) return {}
|
|
27
|
-
try {
|
|
28
|
-
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
29
|
+
} catch {
|
|
30
|
+
return {}
|
|
31
|
+
}
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -54,7 +57,12 @@ export function approvedReleaseFiles(config = {}) {
|
|
|
54
57
|
function addExtraFiles(allowed, entries, prefix = '') {
|
|
55
58
|
if (!Array.isArray(entries)) return
|
|
56
59
|
for (const entry of entries) {
|
|
57
|
-
const path =
|
|
60
|
+
const path =
|
|
61
|
+
typeof entry === 'string'
|
|
62
|
+
? entry
|
|
63
|
+
: entry && typeof entry === 'object' && 'path' in entry
|
|
64
|
+
? entry.path
|
|
65
|
+
: ''
|
|
58
66
|
if (typeof path === 'string' && path) addPath(allowed, prefix, path)
|
|
59
67
|
}
|
|
60
68
|
}
|
|
@@ -67,8 +75,9 @@ function addPath(allowed, prefix, path) {
|
|
|
67
75
|
|
|
68
76
|
/** @param {string[]} paths @param {Set<string>} allowed @returns {string[]} */
|
|
69
77
|
export function unexpectedReleasePaths(paths, allowed) {
|
|
70
|
-
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))]
|
|
71
|
-
|
|
78
|
+
return [...new Set(paths.map((path) => path.trim()).filter(Boolean))].filter(
|
|
79
|
+
(path) => !allowed.has(path)
|
|
80
|
+
)
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/**
|
|
@@ -85,7 +94,12 @@ export function validateCargoLockVersions(root, config = {}) {
|
|
|
85
94
|
const addEntries = (entries, prefix = '') => {
|
|
86
95
|
if (!Array.isArray(entries)) return
|
|
87
96
|
for (const entry of entries) {
|
|
88
|
-
const value =
|
|
97
|
+
const value =
|
|
98
|
+
typeof entry === 'string'
|
|
99
|
+
? entry
|
|
100
|
+
: entry && typeof entry === 'object' && 'path' in entry
|
|
101
|
+
? entry.path
|
|
102
|
+
: ''
|
|
89
103
|
if (typeof value !== 'string' || !value.endsWith('Cargo.lock')) continue
|
|
90
104
|
const clean = value.replace(/^\.\//, '').replace(/\\/g, '/')
|
|
91
105
|
paths.add(prefix ? `${prefix}/${clean}` : clean)
|
|
@@ -110,18 +124,25 @@ export function validateCargoLockVersions(root, config = {}) {
|
|
|
110
124
|
const manifestLines = readFileSync(manifestPath, 'utf8').split(/\r?\n/)
|
|
111
125
|
const packageStart = manifestLines.findIndex((line) => line.trim() === '[package]')
|
|
112
126
|
if (packageStart < 0) continue
|
|
113
|
-
const nextSection = manifestLines.findIndex(
|
|
114
|
-
|
|
127
|
+
const nextSection = manifestLines.findIndex(
|
|
128
|
+
(line, index) => index > packageStart && /^\s*\[/.test(line)
|
|
129
|
+
)
|
|
130
|
+
const packageSection = manifestLines
|
|
131
|
+
.slice(packageStart + 1, nextSection < 0 ? manifestLines.length : nextSection)
|
|
132
|
+
.join('\n')
|
|
115
133
|
const packageName = packageSection.match(/^name\s*=\s*"([^"]+)"\s*$/m)?.[1]
|
|
116
134
|
const packageVersion = packageSection.match(/^version\s*=\s*"([^"]+)"\s*$/m)?.[1]
|
|
117
135
|
if (!packageName || !packageVersion) continue
|
|
118
136
|
const lock = readFileSync(lockPath, 'utf8')
|
|
119
|
-
const lockPackage = [
|
|
120
|
-
.
|
|
137
|
+
const lockPackage = [
|
|
138
|
+
...lock.matchAll(/\[\[package\]\]\s+name\s*=\s*"([^"]+)"\s+version\s*=\s*"([^"]+)"/g),
|
|
139
|
+
].find((match) => match[1] === packageName)
|
|
121
140
|
if (!lockPackage) {
|
|
122
141
|
errors.push(`${relativeLockPath} is missing the ${packageName} package entry.`)
|
|
123
142
|
} else if (lockPackage[2] !== packageVersion) {
|
|
124
|
-
errors.push(
|
|
143
|
+
errors.push(
|
|
144
|
+
`${relativeLockPath} version ${lockPackage[2]} does not match ${relativeLockPath.replace(/Cargo\.lock$/, 'Cargo.toml')} version ${packageVersion}.`
|
|
145
|
+
)
|
|
125
146
|
}
|
|
126
147
|
}
|
|
127
148
|
return errors
|
|
@@ -134,11 +155,7 @@ export function validateCargoLockVersions(root, config = {}) {
|
|
|
134
155
|
* @param {{ mainIsAncestor: boolean, directChangedPaths?: string[], allowed?: Set<string> }} input
|
|
135
156
|
*/
|
|
136
157
|
export function classifyPromotion(input) {
|
|
137
|
-
const {
|
|
138
|
-
mainIsAncestor,
|
|
139
|
-
directChangedPaths = [],
|
|
140
|
-
allowed = approvedReleaseFiles(),
|
|
141
|
-
} = input
|
|
158
|
+
const { mainIsAncestor, directChangedPaths = [], allowed = approvedReleaseFiles() } = input
|
|
142
159
|
const paths = [...new Set(directChangedPaths.map((path) => path.trim()).filter(Boolean))]
|
|
143
160
|
const unexpected = unexpectedReleasePaths(paths, allowed)
|
|
144
161
|
return {
|
|
@@ -149,14 +166,20 @@ export function classifyPromotion(input) {
|
|
|
149
166
|
|
|
150
167
|
/** @param {{ releasePleaseToken?: string, githubToken?: string }} input */
|
|
151
168
|
export function selectReleaseCredential(input) {
|
|
152
|
-
if (input.releasePleaseToken)
|
|
153
|
-
|
|
169
|
+
if (input.releasePleaseToken)
|
|
170
|
+
return { token: input.releasePleaseToken, source: 'release-please-token', autoMerge: true }
|
|
171
|
+
if (input.githubToken)
|
|
172
|
+
return { token: input.githubToken, source: 'github-token', autoMerge: false }
|
|
154
173
|
return { token: '', source: 'missing', autoMerge: false }
|
|
155
174
|
}
|
|
156
175
|
|
|
157
176
|
/** @param {Array<{number?: number, title?: string, headRefName?: string}>} prs */
|
|
158
177
|
export function selectGeneratedReleasePrs(prs) {
|
|
159
|
-
return prs.filter(
|
|
178
|
+
return prs.filter(
|
|
179
|
+
(pr) =>
|
|
180
|
+
String(pr.title ?? '').startsWith('chore(main): release ') &&
|
|
181
|
+
String(pr.headRefName ?? '').startsWith('release-please--branches--main')
|
|
182
|
+
)
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
/**
|
|
@@ -168,7 +191,8 @@ export function selectGeneratedReleasePrs(prs) {
|
|
|
168
191
|
export function validateReleasePullRequests(prs, changedPathsByPr, allowed) {
|
|
169
192
|
const errors = []
|
|
170
193
|
const generated = selectGeneratedReleasePrs(prs)
|
|
171
|
-
if (!generated.length)
|
|
194
|
+
if (!generated.length)
|
|
195
|
+
errors.push('Release Please reported a PR, but no generated release PR was found.')
|
|
172
196
|
for (const pr of generated) {
|
|
173
197
|
const number = Number(pr.number)
|
|
174
198
|
const paths = changedPathsByPr.get(number)
|
|
@@ -177,7 +201,10 @@ export function validateReleasePullRequests(prs, changedPathsByPr, allowed) {
|
|
|
177
201
|
continue
|
|
178
202
|
}
|
|
179
203
|
const unexpected = unexpectedReleasePaths(paths, allowed)
|
|
180
|
-
if (unexpected.length)
|
|
204
|
+
if (unexpected.length)
|
|
205
|
+
errors.push(
|
|
206
|
+
`Generated release PR #${number} contains unexpected paths: ${unexpected.join(', ')}`
|
|
207
|
+
)
|
|
181
208
|
}
|
|
182
209
|
return { valid: errors.length === 0, generated, errors }
|
|
183
210
|
}
|
|
@@ -200,21 +227,35 @@ const VERSION_METADATA_FILES = new Set([
|
|
|
200
227
|
* @returns {{ valid: boolean, errors: string[], changedPaths: string[] }}
|
|
201
228
|
*/
|
|
202
229
|
export function validateGeneratedReleaseDiff(input) {
|
|
203
|
-
const {
|
|
230
|
+
const {
|
|
231
|
+
headRef = '',
|
|
232
|
+
headRepo = '',
|
|
233
|
+
repository = '',
|
|
234
|
+
changedPaths = [],
|
|
235
|
+
config = {},
|
|
236
|
+
root = process.cwd(),
|
|
237
|
+
} = input
|
|
204
238
|
/** @type {string[]} */
|
|
205
239
|
const errors = []
|
|
206
240
|
if (!isReleasePleaseHead(headRef)) {
|
|
207
241
|
errors.push(`Head branch ${headRef || '(missing)'} is not a generated Release Please branch.`)
|
|
208
242
|
}
|
|
209
243
|
if (!repository || !headRepo) {
|
|
210
|
-
errors.push(
|
|
244
|
+
errors.push(
|
|
245
|
+
'Generated release validation requires both the head and base repository identities.'
|
|
246
|
+
)
|
|
211
247
|
} else if (headRepo !== repository) {
|
|
212
|
-
errors.push(
|
|
248
|
+
errors.push(
|
|
249
|
+
`Head repository ${headRepo} is not ${repository}; generated release validation requires a same-repository pull request.`
|
|
250
|
+
)
|
|
213
251
|
}
|
|
214
252
|
const paths = [...new Set(changedPaths.map((path) => String(path).trim()).filter(Boolean))]
|
|
215
253
|
if (!paths.length) errors.push('Generated release pull request has no changed files.')
|
|
216
254
|
const unexpected = unexpectedReleasePaths(paths, approvedReleaseFiles(config))
|
|
217
|
-
if (unexpected.length)
|
|
255
|
+
if (unexpected.length)
|
|
256
|
+
errors.push(
|
|
257
|
+
`Generated release pull request contains unexpected paths: ${unexpected.join(', ')}`
|
|
258
|
+
)
|
|
218
259
|
const versionMetadata = new Set(VERSION_METADATA_FILES)
|
|
219
260
|
addExtraFiles(versionMetadata, config['extra-files'])
|
|
220
261
|
const packages = config.packages
|
|
@@ -241,11 +282,19 @@ export function validateGeneratedReleaseDiff(input) {
|
|
|
241
282
|
export function buildReleaseRecoveryPlan(input) {
|
|
242
283
|
const tags = [...new Set(input.tags.filter((tag) => /^v?\d+\.\d+\.\d+/.test(tag)))]
|
|
243
284
|
/** @type {string[]} */
|
|
244
|
-
const releaseTags = [
|
|
285
|
+
const releaseTags = [
|
|
286
|
+
...new Set(
|
|
287
|
+
input.releases.map((release) => release.tagName).filter((tag) => typeof tag === 'string')
|
|
288
|
+
),
|
|
289
|
+
]
|
|
245
290
|
const tagSet = new Set(tags)
|
|
246
291
|
const releaseSet = new Set(releaseTags)
|
|
247
|
-
const missingGitHubReleases = tags.filter(
|
|
248
|
-
|
|
292
|
+
const missingGitHubReleases = tags.filter(
|
|
293
|
+
(tag) => !releaseSet.has(tag) && !releaseSet.has(tag.replace(/^v/, ''))
|
|
294
|
+
)
|
|
295
|
+
const orphanGitHubReleases = releaseTags.filter(
|
|
296
|
+
(tag) => !tagSet.has(tag) && !tagSet.has(tag.replace(/^v/, ''))
|
|
297
|
+
)
|
|
249
298
|
const latestTag = [...tags].sort(compareVersions).at(-1) ?? ''
|
|
250
299
|
const latestPackageVersion = [...input.packageVersions].sort(compareVersions).at(-1) ?? ''
|
|
251
300
|
return {
|
|
@@ -254,22 +303,33 @@ export function buildReleaseRecoveryPlan(input) {
|
|
|
254
303
|
missingGitHubReleases,
|
|
255
304
|
orphanGitHubReleases,
|
|
256
305
|
pendingReleasePullRequests: input.releasePrs,
|
|
257
|
-
packageVersionMismatch: Boolean(
|
|
306
|
+
packageVersionMismatch: Boolean(
|
|
307
|
+
latestTag &&
|
|
308
|
+
latestPackageVersion &&
|
|
309
|
+
normalizeVersion(latestTag) !== normalizeVersion(latestPackageVersion)
|
|
310
|
+
),
|
|
258
311
|
actions: [
|
|
259
312
|
...missingGitHubReleases.map((tag) => `create GitHub release metadata for ${tag}`),
|
|
260
|
-
...(latestTag &&
|
|
313
|
+
...(latestTag &&
|
|
314
|
+
latestPackageVersion &&
|
|
315
|
+
normalizeVersion(latestTag) !== normalizeVersion(latestPackageVersion)
|
|
316
|
+
? [`review package/tag mismatch (${latestPackageVersion} vs ${latestTag})`]
|
|
317
|
+
: []),
|
|
261
318
|
],
|
|
262
319
|
}
|
|
263
320
|
}
|
|
264
321
|
|
|
265
322
|
/** @param {string} value */
|
|
266
|
-
function normalizeVersion(value) {
|
|
323
|
+
function normalizeVersion(value) {
|
|
324
|
+
return value.replace(/^v/, '').split('-')[0]
|
|
325
|
+
}
|
|
267
326
|
|
|
268
327
|
/** @param {string} a @param {string} b */
|
|
269
328
|
function compareVersions(a, b) {
|
|
270
329
|
const left = normalizeVersion(a).split('.').map(Number)
|
|
271
330
|
const right = normalizeVersion(b).split('.').map(Number)
|
|
272
|
-
for (let index = 0; index < 3; index += 1)
|
|
331
|
+
for (let index = 0; index < 3; index += 1)
|
|
332
|
+
if ((left[index] ?? 0) !== (right[index] ?? 0)) return (left[index] ?? 0) - (right[index] ?? 0)
|
|
273
333
|
return a.localeCompare(b)
|
|
274
334
|
}
|
|
275
335
|
|
|
@@ -304,17 +364,24 @@ export function classifyReconciliation(input) {
|
|
|
304
364
|
allowed = approvedReleaseFiles(),
|
|
305
365
|
} = input
|
|
306
366
|
if (!mainSha || !stagingSha) return { action: 'fail', reason: 'Missing branch SHA.' }
|
|
307
|
-
if (mainSha === stagingSha)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
367
|
+
if (mainSha === stagingSha)
|
|
368
|
+
return { action: 'aligned', reason: 'Branches already point at the same commit.' }
|
|
369
|
+
const hasIndeterminateMainCommit = mainOnlyCommits.some(
|
|
370
|
+
(commit) => typeof commit.sha !== 'string' || !Array.isArray(commit.changedPaths)
|
|
371
|
+
)
|
|
372
|
+
if (hasIndeterminateMainCommit)
|
|
373
|
+
return {
|
|
374
|
+
action: 'fail',
|
|
375
|
+
reason: 'Unable to inspect main-only commit metadata.',
|
|
376
|
+
}
|
|
377
|
+
const hasIndeterminateStagingCommit = stagingOnlyCommits.some(
|
|
378
|
+
(commit) => typeof commit.sha !== 'string' || !Array.isArray(commit.changedPaths)
|
|
379
|
+
)
|
|
380
|
+
if (hasIndeterminateStagingCommit)
|
|
381
|
+
return {
|
|
382
|
+
action: 'fail',
|
|
383
|
+
reason: 'Unable to inspect staging-only commit metadata.',
|
|
384
|
+
}
|
|
318
385
|
if (Array.isArray(directChangedPaths)) {
|
|
319
386
|
const paths = [...new Set(directChangedPaths.map((path) => path.trim()).filter(Boolean))]
|
|
320
387
|
const unexpected = unexpectedReleasePaths(paths, allowed)
|
|
@@ -322,11 +389,20 @@ export function classifyReconciliation(input) {
|
|
|
322
389
|
// --cherry-pick. If the final trees are identical and no non-equivalent
|
|
323
390
|
// commits remain, classify the branches consistently as aligned.
|
|
324
391
|
if (!paths.length && !mainOnlyCommits.length && !stagingOnlyCommits.length) {
|
|
325
|
-
return {
|
|
392
|
+
return {
|
|
393
|
+
action: 'aligned',
|
|
394
|
+
targetSha: mainSha,
|
|
395
|
+
reason: 'Branches have different history but identical content.',
|
|
396
|
+
}
|
|
326
397
|
}
|
|
327
|
-
const stagingOnlyReleaseOnly =
|
|
328
|
-
|
|
329
|
-
|
|
398
|
+
const stagingOnlyReleaseOnly =
|
|
399
|
+
stagingOnlyCommits.length > 0 &&
|
|
400
|
+
stagingOnlyCommits.every(
|
|
401
|
+
(commit) =>
|
|
402
|
+
Array.isArray(commit.changedPaths) &&
|
|
403
|
+
commit.changedPaths.length > 0 &&
|
|
404
|
+
unexpectedReleasePaths(commit.changedPaths, allowed).length === 0
|
|
405
|
+
)
|
|
330
406
|
if (!unexpected.length && !stagingOnlyReleaseOnly) {
|
|
331
407
|
return {
|
|
332
408
|
action: 'fast-forward',
|
|
@@ -355,7 +431,7 @@ export function classifyReconciliation(input) {
|
|
|
355
431
|
: null
|
|
356
432
|
const unexpectedMain = unexpectedReleasePaths(
|
|
357
433
|
[...new Set(mainOnlyCommits.flatMap((commit) => commit.changedPaths || []))],
|
|
358
|
-
allowed
|
|
434
|
+
allowed
|
|
359
435
|
).filter((path) => directTreePaths === null || directTreePaths.has(path))
|
|
360
436
|
// Without a final-tree comparison, retain the historical fail-closed
|
|
361
437
|
// behavior. When the final delta is known, main is the validated release
|
|
@@ -377,8 +453,17 @@ export function classifyReconciliation(input) {
|
|
|
377
453
|
reason: 'staging contains unpromoted commits; replay them onto main.',
|
|
378
454
|
}
|
|
379
455
|
}
|
|
380
|
-
if (mainOnlyCommits.length)
|
|
381
|
-
|
|
456
|
+
if (mainOnlyCommits.length)
|
|
457
|
+
return {
|
|
458
|
+
action: 'fast-forward',
|
|
459
|
+
targetSha: mainSha,
|
|
460
|
+
reason: 'main only added approved release metadata.',
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
action: 'aligned',
|
|
464
|
+
targetSha: mainSha,
|
|
465
|
+
reason: 'Branches have different history but identical content.',
|
|
466
|
+
}
|
|
382
467
|
}
|
|
383
468
|
|
|
384
469
|
export { DEFAULT_RELEASE_FILES }
|
|
@@ -418,7 +503,16 @@ export function reconciliationPullRequestTitle({ targetHead, sourceBase }) {
|
|
|
418
503
|
* }} input
|
|
419
504
|
*/
|
|
420
505
|
export function buildReconciliationPullRequestBody(input) {
|
|
421
|
-
const {
|
|
506
|
+
const {
|
|
507
|
+
sourceBase,
|
|
508
|
+
targetHead,
|
|
509
|
+
mainSha,
|
|
510
|
+
stagingSha,
|
|
511
|
+
targetSha,
|
|
512
|
+
deliverySha = '',
|
|
513
|
+
action,
|
|
514
|
+
pushError = '',
|
|
515
|
+
} = input
|
|
422
516
|
const lines = [
|
|
423
517
|
'## Automated release reconciliation',
|
|
424
518
|
'',
|
|
@@ -436,7 +530,7 @@ export function buildReconciliationPullRequestBody(input) {
|
|
|
436
530
|
'',
|
|
437
531
|
`Merging this pull request synchronizes \`${targetHead}\` with the exact reconciled tree above; no unrelated commits are introduced.`,
|
|
438
532
|
'',
|
|
439
|
-
`Code Foundry treats validated \`${sourceBase}\` as the shipped source of truth and preserves unpromoted \`${targetHead}\` work by replaying it on top. Indeterminate history, replay conflicts, authentication failures, and ambiguous or stale state fail the release job closed and are never pushed around branch protection. Later releases retry the direct push first; when branch policy still rejects it, this pull request is updated instead of duplicated
|
|
533
|
+
`Code Foundry treats validated \`${sourceBase}\` as the shipped source of truth and preserves unpromoted \`${targetHead}\` work by replaying it on top. Indeterminate history, replay conflicts, authentication failures, and ambiguous or stale state fail the release job closed and are never pushed around branch protection. Later releases retry the direct push first; when branch policy still rejects it, this pull request is updated instead of duplicated.`
|
|
440
534
|
)
|
|
441
535
|
return lines.join('\n')
|
|
442
536
|
}
|
|
@@ -457,13 +551,21 @@ export function selectReconciliationPullRequest(prs, input) {
|
|
|
457
551
|
const { targetBase, branch, title } = input
|
|
458
552
|
const owned = prs.filter((pr) => String(pr.headRefName ?? '') === branch)
|
|
459
553
|
if (!owned.length) return { create: true }
|
|
460
|
-
const reusable = owned.filter(
|
|
554
|
+
const reusable = owned.filter(
|
|
555
|
+
(pr) => String(pr.baseRefName ?? '') === targetBase && String(pr.title ?? '') === title
|
|
556
|
+
)
|
|
461
557
|
if (reusable.length === 1) return { create: false, reuse: reusable[0] }
|
|
462
558
|
if (reusable.length > 1) {
|
|
463
|
-
return {
|
|
559
|
+
return {
|
|
560
|
+
create: false,
|
|
561
|
+
error: `Multiple open pull requests use the reconciliation branch ${branch}; refusing to pick one.`,
|
|
562
|
+
}
|
|
464
563
|
}
|
|
465
564
|
const foreign = owned
|
|
466
|
-
.map(
|
|
565
|
+
.map(
|
|
566
|
+
(pr) =>
|
|
567
|
+
`#${pr.number ?? '?'} (base ${pr.baseRefName ?? '?'}, title ${JSON.stringify(pr.title ?? '')})`
|
|
568
|
+
)
|
|
467
569
|
.join(', ')
|
|
468
570
|
return {
|
|
469
571
|
create: false,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
const TEST_FILE_PATTERN =
|
|
3
|
+
const TEST_FILE_PATTERN =
|
|
4
|
+
/(?:\.(?:test|spec|unit|integration|smoke|e2e)\.(?:[cm]?[jt]sx?|py)|(?:^|\/)test_[^/]+\.py$|(?:^|\/)tests\/.*\.rs$)/i
|
|
4
5
|
|
|
5
6
|
/** @param {string} file */
|
|
6
7
|
export function isTestFile(file) {
|
|
@@ -12,7 +13,11 @@ export function testCategory(file) {
|
|
|
12
13
|
const normalized = file.replaceAll('\\', '/').toLowerCase()
|
|
13
14
|
const base = normalized.split('/').at(-1) ?? normalized
|
|
14
15
|
|
|
15
|
-
if (
|
|
16
|
+
if (
|
|
17
|
+
/(^|\/)(e2e|end-to-end|acceptance)(\/|$)|(?:^|[._-])(e2e|acceptance)(?:[._-]|$)/.test(
|
|
18
|
+
normalized
|
|
19
|
+
)
|
|
20
|
+
) {
|
|
16
21
|
return 'e2e'
|
|
17
22
|
}
|
|
18
23
|
if (/(^|\/)(integration)(\/|$)|(?:^|[._-])integration(?:[._-]|$)/.test(normalized)) {
|