code-foundry 1.2.1 → 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.
@@ -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 { return JSON.parse(readFileSync(file, 'utf8')) }
28
- catch { return {} }
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 = typeof entry === 'string' ? entry : entry && typeof entry === 'object' && 'path' in entry ? entry.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
- .filter((path) => !allowed.has(path))
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 = typeof entry === 'string' ? entry : entry && typeof entry === 'object' && 'path' in entry ? entry.path : ''
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((line, index) => index > packageStart && /^\s*\[/.test(line))
114
- const packageSection = manifestLines.slice(packageStart + 1, nextSection < 0 ? manifestLines.length : nextSection).join('\n')
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 = [...lock.matchAll(/\[\[package\]\]\s+name\s*=\s*"([^"]+)"\s+version\s*=\s*"([^"]+)"/g)]
120
- .find((match) => match[1] === packageName)
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(`${relativeLockPath} version ${lockPackage[2]} does not match ${relativeLockPath.replace(/Cargo\.lock$/, 'Cargo.toml')} version ${packageVersion}.`)
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) return { token: input.releasePleaseToken, source: 'release-please-token', autoMerge: true }
153
- if (input.githubToken) return { token: input.githubToken, source: 'github-token', autoMerge: false }
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((pr) => /^chore\(main\): release /.test(pr.title ?? '') && String(pr.headRefName ?? '').startsWith('release-please--branches--main'))
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) errors.push('Release Please reported a PR, but no generated release PR was found.')
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) errors.push(`Generated release PR #${number} contains unexpected paths: ${unexpected.join(', ')}`)
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 { headRef = '', headRepo = '', repository = '', changedPaths = [], config = {}, root = process.cwd() } = input
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('Generated release validation requires both the head and base repository identities.')
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(`Head repository ${headRepo} is not ${repository}; generated release validation requires a same-repository pull request.`)
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) errors.push(`Generated release pull request contains unexpected paths: ${unexpected.join(', ')}`)
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 = [...new Set(input.releases.map((release) => release.tagName).filter((tag) => typeof tag === 'string'))]
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((tag) => !releaseSet.has(tag) && !releaseSet.has(tag.replace(/^v/, '')))
248
- const orphanGitHubReleases = releaseTags.filter((tag) => !tagSet.has(tag) && !tagSet.has(tag.replace(/^v/, '')))
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(latestTag && latestPackageVersion && normalizeVersion(latestTag) !== normalizeVersion(latestPackageVersion)),
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 && latestPackageVersion && normalizeVersion(latestTag) !== normalizeVersion(latestPackageVersion) ? [`review package/tag mismatch (${latestPackageVersion} vs ${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) { return value.replace(/^v/, '').split('-')[0] }
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) if ((left[index] ?? 0) !== (right[index] ?? 0)) return (left[index] ?? 0) - (right[index] ?? 0)
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) return { action: 'aligned', reason: 'Branches already point at the same commit.' }
308
- const hasIndeterminateMainCommit = mainOnlyCommits.some((commit) => typeof commit.sha !== 'string' || !Array.isArray(commit.changedPaths))
309
- if (hasIndeterminateMainCommit) return {
310
- action: 'fail',
311
- reason: 'Unable to inspect main-only commit metadata.',
312
- }
313
- const hasIndeterminateStagingCommit = stagingOnlyCommits.some((commit) => typeof commit.sha !== 'string' || !Array.isArray(commit.changedPaths))
314
- if (hasIndeterminateStagingCommit) return {
315
- action: 'fail',
316
- reason: 'Unable to inspect staging-only commit metadata.',
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 { action: 'aligned', targetSha: mainSha, reason: 'Branches have different history but identical content.' }
392
+ return {
393
+ action: 'aligned',
394
+ targetSha: mainSha,
395
+ reason: 'Branches have different history but identical content.',
396
+ }
326
397
  }
327
- const stagingOnlyReleaseOnly = stagingOnlyCommits.length > 0 && stagingOnlyCommits.every((commit) =>
328
- Array.isArray(commit.changedPaths) && commit.changedPaths.length > 0 && unexpectedReleasePaths(commit.changedPaths, allowed).length === 0,
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) return { action: 'fast-forward', targetSha: mainSha, reason: 'main only added approved release metadata.' }
381
- return { action: 'aligned', targetSha: mainSha, reason: 'Branches have different history but identical content.' }
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 { sourceBase, targetHead, mainSha, stagingSha, targetSha, deliverySha = '', action, pushError = '' } = input
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((pr) => String(pr.baseRefName ?? '') === targetBase && String(pr.title ?? '') === title)
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 { create: false, error: `Multiple open pull requests use the reconciliation branch ${branch}; refusing to pick one.` }
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((pr) => `#${pr.number ?? '?'} (base ${pr.baseRefName ?? '?'}, title ${JSON.stringify(pr.title ?? '')})`)
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 = /(?:\.(?:test|spec|unit|integration|smoke|e2e)\.(?:[cm]?[jt]sx?|py)|(?:^|\/)test_[^/]+\.py$|(?:^|\/)tests\/.*\.rs$)/i
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 (/(^|\/)(e2e|end-to-end|acceptance)(\/|$)|(?:^|[._-])(e2e|acceptance)(?:[._-]|$)/.test(normalized)) {
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)) {