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.
@@ -16,7 +16,10 @@ export function readConfig(file) {
16
16
  for (const line of readFileSync(file, 'utf8').split(/\r?\n/)) {
17
17
  const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/)
18
18
  if (!match) continue
19
- const value = match[2].replace(/\s+#.*$/, '').trim().replace(/^['"]|['"]$/g, '')
19
+ const value = match[2]
20
+ .replace(/\s+#.*$/, '')
21
+ .trim()
22
+ .replace(/^['"]|['"]$/g, '')
20
23
  config[match[1]] = value
21
24
  }
22
25
  return config
@@ -29,7 +32,10 @@ export function configured(value, fallback) {
29
32
 
30
33
  /** @param {string} value @returns {string[]} */
31
34
  export function listValue(value) {
32
- return value.split(',').map((item) => item.trim()).filter(Boolean)
35
+ return value
36
+ .split(',')
37
+ .map((item) => item.trim())
38
+ .filter(Boolean)
33
39
  }
34
40
 
35
41
  /** @param {string} value @param {string} item */
@@ -18,21 +18,39 @@ export function doctorGithub(root) {
18
18
  const rulesets = hydrateRulesets(repository, ghJson(['api', `repos/${repository}/rulesets`]))
19
19
  const mainRulesets = rulesetsForBranch(rulesets, 'main')
20
20
  const protection = ghJson(['api', `repos/${repository}/branches/main/protection`])
21
- const required = mainRulesets.length ? requiredContextsFromRulesets(mainRulesets) : requiredContextsFromProtection(protection)
21
+ const required = mainRulesets.length
22
+ ? requiredContextsFromRulesets(mainRulesets)
23
+ : requiredContextsFromProtection(protection)
22
24
  details.requiredChecks = required
23
- if (!protection && !mainRulesets.length) warnings.push('main branch protection or repository rulesets are not readable or are not configured.')
24
- else if (!required.length) warnings.push(mainRulesets.length ? 'main ruleset has no required status checks.' : 'main protection has no required status checks.')
25
+ if (!protection && !mainRulesets.length)
26
+ warnings.push(
27
+ 'main branch protection or repository rulesets are not readable or are not configured.'
28
+ )
29
+ else if (!required.length)
30
+ warnings.push(
31
+ mainRulesets.length
32
+ ? 'main ruleset has no required status checks.'
33
+ : 'main protection has no required status checks.'
34
+ )
25
35
  const duplicateNames = required.filter((name) => /\b([^/]+) \/ \1\b/i.test(name))
26
- if (duplicateNames.length) errors.push(`required checks contain duplicate workflow prefixes: ${duplicateNames.join(', ')}`)
36
+ if (duplicateNames.length)
37
+ errors.push(`required checks contain duplicate workflow prefixes: ${duplicateNames.join(', ')}`)
27
38
 
28
39
  const sha = String(ghJson(['api', `repos/${repository}/git/ref/heads/main`])?.object?.sha ?? '')
29
- const checks = sha ? ghJson(['api', `repos/${repository}/commits/${sha}/check-runs?per_page=100`])?.check_runs ?? [] : []
40
+ const checks = sha
41
+ ? (ghJson(['api', `repos/${repository}/commits/${sha}/check-runs?per_page=100`])?.check_runs ??
42
+ [])
43
+ : []
30
44
  const observed = checks.map(/** @param {any} check */ (check) => check.name).filter(Boolean)
31
45
  details.observedChecks = observed
32
- if (!observed.length) warnings.push('no check runs were observed on the current main commit; exact check validation is deferred until CI runs.')
46
+ if (!observed.length)
47
+ warnings.push(
48
+ 'no check runs were observed on the current main commit; exact check validation is deferred until CI runs.'
49
+ )
33
50
  if (required.length) {
34
51
  const missing = required.filter((name) => observed.length && !observed.includes(name))
35
- if (missing.length) warnings.push(`required checks not observed on current main: ${missing.join(', ')}`)
52
+ if (missing.length)
53
+ warnings.push(`required checks not observed on current main: ${missing.join(', ')}`)
36
54
  }
37
55
 
38
56
  const workflowIssues = inspectWorkflows(root)
@@ -40,34 +58,79 @@ export function doctorGithub(root) {
40
58
  warnings.push(...workflowIssues.warnings)
41
59
 
42
60
  const secrets = ghJson(['secret', 'list', '--repo', repository, '--json', 'name'])
43
- const secretNames = Array.isArray(secrets) ? secrets.map(/** @param {any} secret */ (secret) => secret.name) : []
61
+ const secretNames = Array.isArray(secrets)
62
+ ? secrets.map(/** @param {any} secret */ (secret) => secret.name)
63
+ : []
44
64
  details.secrets = {
45
65
  codeFoundryTokenPresent: secretNames.includes('CODE_FOUNDRY_TOKEN'),
46
66
  stagingDeployKeyPresent: secretNames.includes('STAGING_DEPLOY_KEY'),
47
67
  }
48
68
  if (!details.secrets.codeFoundryTokenPresent) {
49
- warnings.push('CODE_FOUNDRY_TOKEN is absent. PR workflow triggers and automation may require manual readiness.')
69
+ warnings.push(
70
+ 'CODE_FOUNDRY_TOKEN is absent. PR workflow triggers and automation may require manual readiness.'
71
+ )
50
72
  }
51
73
 
52
74
  const config = readConfig(root)
53
- if (['workflow-dispatch', 'dispatch'].includes(config.post_release_mode) && config.post_release !== 'false' && !details.secrets.codeFoundryTokenPresent && !details.secrets.releasePleaseTokenPresent) {
75
+ if (
76
+ ['workflow-dispatch', 'dispatch'].includes(config.post_release_mode) &&
77
+ config.post_release !== 'false' &&
78
+ !details.secrets.codeFoundryTokenPresent &&
79
+ !details.secrets.releasePleaseTokenPresent
80
+ ) {
54
81
  errors.push('post-release workflow-dispatch mode requires CODE_FOUNDRY_TOKEN to be present.')
55
82
  }
56
83
  const credentialChecks = {
57
84
  npmTokenPresent: secretNames.includes('NPM_TOKEN'),
58
85
  turboTokenPresent: secretNames.includes('TURBO_TOKEN'),
59
- turboTeamPresent: Boolean(ghJson(['variable', 'list', '--repo', repository, '--json', 'name'])?.some(/** @param {{ name?: string }} variable */ (variable) => variable.name === 'TURBO_TEAM')),
86
+ turboTeamPresent: Boolean(
87
+ ghJson(['variable', 'list', '--repo', repository, '--json', 'name'])?.some(
88
+ /** @param {{ name?: string }} variable */ (variable) => variable.name === 'TURBO_TEAM'
89
+ )
90
+ ),
60
91
  opencodeApiKeyPresent: secretNames.includes('OPENCODE_API_KEY'),
61
92
  }
62
93
  details.credentials = credentialChecks
63
- if (config.npm_publish === 'true' && !credentialChecks.npmTokenPresent) warnings.push('npm_publish is enabled but NPM_TOKEN is not configured; npm trusted publishing must be configured for tokenless publication.')
64
- if (['true', 'auto'].includes(config.turbo_remote ?? 'false') && (!credentialChecks.turboTokenPresent || !credentialChecks.turboTeamPresent)) warnings.push('turbo_remote is enabled but TURBO_TOKEN and/or TURBO_TEAM is not configured; remote caching will be skipped.')
65
- if (['true', 'auto'].includes(config.opencode_security ?? 'false') && !credentialChecks.opencodeApiKeyPresent) warnings.push('opencode_security is enabled but OPENCODE_API_KEY is not configured; the optional scan will be skipped.')
94
+ if (config.npm_publish === 'true' && !credentialChecks.npmTokenPresent)
95
+ warnings.push(
96
+ 'npm_publish is enabled but NPM_TOKEN is not configured; npm trusted publishing must be configured for tokenless publication.'
97
+ )
98
+ if (
99
+ ['true', 'auto'].includes(config.turbo_remote ?? 'false') &&
100
+ (!credentialChecks.turboTokenPresent || !credentialChecks.turboTeamPresent)
101
+ )
102
+ warnings.push(
103
+ 'turbo_remote is enabled but TURBO_TOKEN and/or TURBO_TEAM is not configured; remote caching will be skipped.'
104
+ )
105
+ if (
106
+ ['true', 'auto'].includes(config.opencode_security ?? 'false') &&
107
+ !credentialChecks.opencodeApiKeyPresent
108
+ )
109
+ warnings.push(
110
+ 'opencode_security is enabled but OPENCODE_API_KEY is not configured; the optional scan will be skipped.'
111
+ )
66
112
 
67
- const prs = ghJson(['pr', 'list', '--repo', repository, '--state', 'open', '--base', 'main', '--json', 'number,title,headRefName'])
113
+ const prs = ghJson([
114
+ 'pr',
115
+ 'list',
116
+ '--repo',
117
+ repository,
118
+ '--state',
119
+ 'open',
120
+ '--base',
121
+ 'main',
122
+ '--json',
123
+ 'number,title,headRefName',
124
+ ])
68
125
  const openPrs = Array.isArray(prs) ? prs : []
69
- details.openPromotionPrs = openPrs.filter(/** @param {any} pr */ (pr) => pr.headRefName === 'staging' || /promote staging/i.test(pr.title))
70
- details.openReleasePrs = openPrs.filter(/** @param {any} pr */ (pr) => String(pr.headRefName).startsWith('release-please--') || /^chore\(main\): release /.test(pr.title))
126
+ details.openPromotionPrs = openPrs.filter(
127
+ /** @param {any} pr */ (pr) => pr.headRefName === 'staging' || /promote staging/i.test(pr.title)
128
+ )
129
+ details.openReleasePrs = openPrs.filter(
130
+ /** @param {any} pr */ (pr) =>
131
+ String(pr.headRefName).startsWith('release-please--') ||
132
+ pr.title.startsWith('chore(main): release ')
133
+ )
71
134
  if (details.openPromotionPrs.length > 1) errors.push('multiple staging promotion PRs are open.')
72
135
  if (details.openReleasePrs.length > 1) errors.push('multiple Release Please PRs are open.')
73
136
  return { errors, warnings, details }
@@ -75,23 +138,33 @@ export function doctorGithub(root) {
75
138
 
76
139
  /** @param {any} protection @returns {string[]} */
77
140
  function requiredContextsFromProtection(protection) {
78
- return [...new Set([
79
- ...(protection?.required_status_checks?.contexts ?? []),
80
- ...(protection?.required_status_checks?.checks ?? []).map(/** @param {any} check */ (check) => check.context),
81
- ].filter(Boolean))]
141
+ return [
142
+ ...new Set(
143
+ [
144
+ ...(protection?.required_status_checks?.contexts ?? []),
145
+ ...(protection?.required_status_checks?.checks ?? []).map(
146
+ /** @param {any} check */ (check) => check.context
147
+ ),
148
+ ].filter(Boolean)
149
+ ),
150
+ ]
82
151
  }
83
152
 
84
153
  /** @param {any[]} rulesets @returns {string[]} */
85
154
  function requiredContextsFromRulesets(rulesets) {
86
- return [...new Set(
87
- (Array.isArray(rulesets) ? rulesets : []).flatMap((/** @type {any} */ ruleset) =>
88
- (ruleset.rules ?? []).flatMap((/** @type {any} */ rule) =>
89
- rule?.type === 'required_status_checks' ?
90
- (rule.parameters?.required_status_checks ?? []).map((/** @type {any} */ check) => check?.context).filter(Boolean) :
91
- []
92
- ),
155
+ return [
156
+ ...new Set(
157
+ (Array.isArray(rulesets) ? rulesets : []).flatMap((/** @type {any} */ ruleset) =>
158
+ (ruleset.rules ?? []).flatMap((/** @type {any} */ rule) =>
159
+ rule?.type === 'required_status_checks'
160
+ ? (rule.parameters?.required_status_checks ?? [])
161
+ .map((/** @type {any} */ check) => check?.context)
162
+ .filter(Boolean)
163
+ : []
164
+ )
165
+ )
93
166
  ),
94
- )]
167
+ ]
95
168
  }
96
169
 
97
170
  /** @param {string} repository @param {any[] | null} rulesets @returns {any[]} */
@@ -107,13 +180,14 @@ function hydrateRulesets(repository, rulesets) {
107
180
  /** @param {any[] | null} rulesets @param {string} branch @returns {any[]} */
108
181
  function rulesetsForBranch(rulesets, branch) {
109
182
  if (!Array.isArray(rulesets)) return []
110
- return rulesets.filter((ruleset) => (
111
- ruleset?.target === 'branch' &&
112
- Array.isArray(ruleset?.conditions?.ref_name?.include) &&
113
- Array.isArray(ruleset?.conditions?.ref_name?.exclude) &&
114
- includesBranch(ruleset.conditions.ref_name.include, branch) &&
115
- !includesBranch(ruleset.conditions.ref_name.exclude, branch)
116
- ))
183
+ return rulesets.filter(
184
+ (ruleset) =>
185
+ ruleset?.target === 'branch' &&
186
+ Array.isArray(ruleset?.conditions?.ref_name?.include) &&
187
+ Array.isArray(ruleset?.conditions?.ref_name?.exclude) &&
188
+ includesBranch(ruleset.conditions.ref_name.include, branch) &&
189
+ !includesBranch(ruleset.conditions.ref_name.exclude, branch)
190
+ )
117
191
  }
118
192
 
119
193
  /** @param {(string | null | undefined)[]} patterns @param {string} branch @returns {boolean} */
@@ -123,8 +197,11 @@ function includesBranch(patterns, branch) {
123
197
  if (!pattern) return false
124
198
  if (pattern === '~ALL') return true
125
199
  if (pattern === target || pattern === branch) return true
126
- if (!/[\*?]/.test(pattern)) return false
127
- const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*').replace(/\\\?/g, '.')
200
+ if (!/[*?]/.test(pattern)) return false
201
+ const escaped = pattern
202
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
203
+ .replace(/\\\*/g, '.*')
204
+ .replace(/\\\?/g, '.')
128
205
  const regex = new RegExp(`^${escaped}$`)
129
206
  return regex.test(target) || regex.test(branch)
130
207
  })
@@ -139,12 +216,21 @@ function inspectWorkflows(root) {
139
216
  const directory = join(root, '.github/workflows')
140
217
  /** @type {string[]} */
141
218
  let files = []
142
- try { files = readdirSync(directory).filter((file) => file.endsWith('.yml') || file.endsWith('.yaml')) }
143
- catch { return { errors: ['.github/workflows is missing.'], warnings } }
219
+ try {
220
+ files = readdirSync(directory).filter((file) => file.endsWith('.yml') || file.endsWith('.yaml'))
221
+ } catch {
222
+ return { errors: ['.github/workflows is missing.'], warnings }
223
+ }
144
224
  for (const file of files) {
145
225
  const content = readFileSync(join(directory, file), 'utf8')
146
- if (!/^permissions:\s*$/m.test(content) && /uses:.*\.github\/workflows\//.test(content)) warnings.push(`${file} does not declare top-level permissions.`)
147
- if (file === 'release.yml' && /contents:\s+write/.test(content) && !/pull-requests:\s+write/.test(content)) errors.push('release.yml needs pull-requests: write for guarded Release Please PR handling.')
226
+ if (!/^permissions:\s*$/m.test(content) && /uses:.*\.github\/workflows\//.test(content))
227
+ warnings.push(`${file} does not declare top-level permissions.`)
228
+ if (
229
+ file === 'release.yml' &&
230
+ /contents:\s+write/.test(content) &&
231
+ !/pull-requests:\s+write/.test(content)
232
+ )
233
+ errors.push('release.yml needs pull-requests: write for guarded Release Please PR handling.')
148
234
  }
149
235
  return { errors, warnings }
150
236
  }
@@ -153,27 +239,43 @@ function inspectWorkflows(root) {
153
239
  function remoteRepository(root) {
154
240
  const result = spawnSync('git', ['remote', 'get-url', 'origin'], { cwd: root, encoding: 'utf8' })
155
241
  const value = result.status === 0 ? result.stdout.trim() : ''
156
- return value.replace(/^git@github\.com:/, '').replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '')
242
+ return value
243
+ .replace(/^git@github\.com:/, '')
244
+ .replace(/^https?:\/\/github\.com\//, '')
245
+ .replace(/\.git$/, '')
157
246
  }
158
247
 
159
248
  /** @param {string[]} args @returns {any} */
160
249
  function ghJson(args) {
161
250
  const result = spawnSync('gh', args, { encoding: 'utf8' })
162
251
  if (result.status !== 0) return null
163
- try { return JSON.parse(result.stdout) }
164
- catch { return null }
252
+ try {
253
+ return JSON.parse(result.stdout)
254
+ } catch {
255
+ return null
256
+ }
165
257
  }
166
258
 
167
259
  /** @param {string} command */
168
- function commandExists(command) { return spawnSync(command, ['--version'], { stdio: 'ignore' }).status === 0 }
260
+ function commandExists(command) {
261
+ return spawnSync(command, ['--version'], { stdio: 'ignore' }).status === 0
262
+ }
169
263
 
170
264
  /** @param {string} root @returns {Record<string, string>} */
171
265
  function readConfig(root) {
172
266
  const file = join(root, '.github/code-foundry.yml')
173
267
  try {
174
- return Object.fromEntries(readFileSync(file, 'utf8').split(/\r?\n/).flatMap((line) => {
175
- const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/)
176
- return match ? [[match[1], match[2].replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '')]] : []
177
- }))
178
- } catch { return {} }
268
+ return Object.fromEntries(
269
+ readFileSync(file, 'utf8')
270
+ .split(/\r?\n/)
271
+ .flatMap((line) => {
272
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/)
273
+ return match
274
+ ? [[match[1], match[2].replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '')]]
275
+ : []
276
+ })
277
+ )
278
+ } catch {
279
+ return {}
280
+ }
179
281
  }
@@ -23,7 +23,11 @@ export function customWorkflowFiles(root, standardFiles) {
23
23
  export function overlayPolicy(root, config) {
24
24
  const mode = config.sync_mode ?? 'overlay'
25
25
  const custom = config.custom_workflows ?? 'preserve'
26
- if (!['overlay', 'strict'].includes(mode)) throw new Error(`Unsupported sync_mode: ${mode}; use overlay or strict.`)
27
- if (custom !== 'preserve') throw new Error(`Unsupported custom_workflows: ${custom}; custom workflows are always preserved.`)
26
+ if (!['overlay', 'strict'].includes(mode))
27
+ throw new Error(`Unsupported sync_mode: ${mode}; use overlay or strict.`)
28
+ if (custom !== 'preserve')
29
+ throw new Error(
30
+ `Unsupported custom_workflows: ${custom}; custom workflows are always preserved.`
31
+ )
28
32
  return { mode, custom_workflows: custom }
29
33
  }
@@ -47,15 +47,19 @@ export function detectLanguages(root) {
47
47
  if (
48
48
  existsSync(join(root, 'package.json')) ||
49
49
  files.some((file) => /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts)$/.test(file)) ||
50
- files.some((file) => /(?:bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$/.test(file))
51
- ) languages.push('typescript')
50
+ files.some((file) =>
51
+ /(?:bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$/.test(file)
52
+ )
53
+ )
54
+ languages.push('typescript')
52
55
  if (existsSync(join(root, 'Cargo.toml')) || hasSource(root, /\.rs$/)) languages.push('rust')
53
56
  if (
54
57
  existsSync(join(root, 'pyproject.toml')) ||
55
58
  existsSync(join(root, 'requirements.txt')) ||
56
59
  existsSync(join(root, 'requirements-dev.txt')) ||
57
60
  hasSource(root, /\.py$/)
58
- ) languages.push('python')
61
+ )
62
+ languages.push('python')
59
63
  if (files.some((file) => file.endsWith('.sol'))) languages.push('solidity')
60
64
  return languages.length ? languages : ['none']
61
65
  }
@@ -72,16 +76,23 @@ export function detectPackageManager(root) {
72
76
 
73
77
  /** @param {string} root */
74
78
  export function detectProfile(root) {
75
- if (existsSync(join(root, 'turbo.json')) || existsSync(join(root, 'pnpm-workspace.yaml'))) return 'monorepo'
79
+ if (existsSync(join(root, 'turbo.json')) || existsSync(join(root, 'pnpm-workspace.yaml')))
80
+ return 'monorepo'
76
81
  if (existsSync(join(root, 'package.json'))) {
77
82
  try {
78
83
  const packageJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
79
- if (Array.isArray(packageJson.workspaces) || typeof packageJson.workspaces === 'object') return 'monorepo'
84
+ if (Array.isArray(packageJson.workspaces) || typeof packageJson.workspaces === 'object')
85
+ return 'monorepo'
80
86
  } catch {
81
87
  // Doctor reports malformed package.json separately.
82
88
  }
83
89
  }
84
- if (existsSync(join(root, 'package.json')) || existsSync(join(root, 'pyproject.toml')) || existsSync(join(root, 'Cargo.toml'))) return 'application'
90
+ if (
91
+ existsSync(join(root, 'package.json')) ||
92
+ existsSync(join(root, 'pyproject.toml')) ||
93
+ existsSync(join(root, 'Cargo.toml'))
94
+ )
95
+ return 'application'
85
96
  return 'minimal'
86
97
  }
87
98
 
@@ -110,7 +121,9 @@ export function resolveProfile(root) {
110
121
  */
111
122
  export function recommendRunners(root) {
112
123
  const languages = detectLanguages(root)
113
- const heavy = languages.some((language) => ['rust', 'python', 'solidity'].includes(language)) || hasBrowserProject(root)
124
+ const heavy =
125
+ languages.some((language) => ['rust', 'python', 'solidity'].includes(language)) ||
126
+ hasBrowserProject(root)
114
127
  return {
115
128
  runner: heavy ? 'ubuntu-latest' : 'ubuntu-slim',
116
129
  ci_runner: heavy ? 'ubuntu-latest' : 'ubuntu-slim',
@@ -125,7 +138,12 @@ export function recommendRunners(root) {
125
138
 
126
139
  /** @param {string} root */
127
140
  function hasBrowserProject(root) {
128
- return ['playwright.config.ts', 'playwright.config.js', 'cypress.config.ts', 'cypress.config.js'].some((file) => existsSync(join(root, file)))
141
+ return [
142
+ 'playwright.config.ts',
143
+ 'playwright.config.js',
144
+ 'cypress.config.ts',
145
+ 'cypress.config.js',
146
+ ].some((file) => existsSync(join(root, file)))
129
147
  }
130
148
 
131
149
  /** @param {string} root */
@@ -11,16 +11,26 @@ import { createHash } from 'node:crypto'
11
11
  */
12
12
  export function selectHookDelivery(input) {
13
13
  const mode = input.mode ?? 'auto'
14
- if (mode === 'false' || mode === 'disabled') return { delivery: 'disabled', reason: 'Post-release hooks are disabled by configuration.' }
14
+ if (mode === 'false' || mode === 'disabled')
15
+ return { delivery: 'disabled', reason: 'Post-release hooks are disabled by configuration.' }
15
16
  if (mode === 'workflow-dispatch' || (mode === 'auto' && input.tokenPresent)) {
16
17
  return input.tokenPresent
17
- ? { delivery: 'workflow-dispatch', reason: 'A PAT is available; use one explicit workflow dispatch.' }
18
+ ? {
19
+ delivery: 'workflow-dispatch',
20
+ reason: 'A PAT is available; use one explicit workflow dispatch.',
21
+ }
18
22
  : { delivery: 'unavailable', reason: 'workflow-dispatch requires a PAT.' }
19
23
  }
20
24
  if (mode === 'release-event' || (mode === 'auto' && input.releaseEventEnabled !== false)) {
21
- return { delivery: 'release-event', reason: 'Use the published-release event; do not issue a fallback dispatch.' }
25
+ return {
26
+ delivery: 'release-event',
27
+ reason: 'Use the published-release event; do not issue a fallback dispatch.',
28
+ }
29
+ }
30
+ return {
31
+ delivery: 'unavailable',
32
+ reason: 'No supported post-release delivery mechanism is configured.',
22
33
  }
23
- return { delivery: 'unavailable', reason: 'No supported post-release delivery mechanism is configured.' }
24
34
  }
25
35
 
26
36
  /** @param {string} repository @param {string} tag @returns {string} */
@@ -36,5 +46,7 @@ export function releaseDeliveryKey(repository, tag) {
36
46
  * @param {string} tag
37
47
  */
38
48
  export function hasDeliveredHook(runs, tag) {
39
- return runs.some((run) => run.headBranch === tag || run.displayTitle?.includes(`release-tag=${tag}`))
49
+ return runs.some(
50
+ (run) => run.headBranch === tag || run.displayTitle?.includes(`release-tag=${tag}`)
51
+ )
40
52
  }
@@ -3,7 +3,27 @@
3
3
  import { existsSync, readFileSync, readdirSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
 
6
- const ignored = new Set(['.git', '.code-foundry', '.venv', 'node_modules', 'target', 'vendor', 'dist', 'build', '.next', '.kilo', '.turbo', '.cache', '.vercel', '.output', '.nuxt', '.svelte-kit', '.parcel-cache', 'out', 'coverage'])
6
+ const ignored = new Set([
7
+ '.git',
8
+ '.code-foundry',
9
+ '.venv',
10
+ 'node_modules',
11
+ 'target',
12
+ 'vendor',
13
+ 'dist',
14
+ 'build',
15
+ '.next',
16
+ '.kilo',
17
+ '.turbo',
18
+ '.cache',
19
+ '.vercel',
20
+ '.output',
21
+ '.nuxt',
22
+ '.svelte-kit',
23
+ '.parcel-cache',
24
+ 'out',
25
+ 'coverage',
26
+ ])
7
27
 
8
28
  /** @typedef {{ directory: string, manifest: string, releaseType: 'node'|'python'|'rust', packageName?: string, extraFiles: string[] }} ReleasePackage */
9
29
 
@@ -16,11 +36,29 @@ export function detectReleasePackages(root) {
16
36
  const directory = file.includes('/') ? file.slice(0, file.lastIndexOf('/')) : '.'
17
37
  if (name === 'package.json') {
18
38
  const parsed = readJson(join(root, file))
19
- packages.push({ directory, manifest: file, releaseType: 'node', packageName: typeof parsed?.name === 'string' ? parsed.name : undefined, extraFiles: detectExtraFiles(root, directory) })
39
+ packages.push({
40
+ directory,
41
+ manifest: file,
42
+ releaseType: 'node',
43
+ packageName: typeof parsed?.name === 'string' ? parsed.name : undefined,
44
+ extraFiles: detectExtraFiles(root, directory),
45
+ })
20
46
  } else if (name === 'Cargo.toml') {
21
- packages.push({ directory, manifest: file, releaseType: 'rust', packageName: tomlPackageName(join(root, file)), extraFiles: detectExtraFiles(root, directory) })
47
+ packages.push({
48
+ directory,
49
+ manifest: file,
50
+ releaseType: 'rust',
51
+ packageName: tomlPackageName(join(root, file)),
52
+ extraFiles: detectExtraFiles(root, directory),
53
+ })
22
54
  } else if (name === 'pyproject.toml') {
23
- packages.push({ directory, manifest: file, releaseType: 'python', packageName: pyprojectName(join(root, file)), extraFiles: detectExtraFiles(root, directory) })
55
+ packages.push({
56
+ directory,
57
+ manifest: file,
58
+ releaseType: 'python',
59
+ packageName: pyprojectName(join(root, file)),
60
+ extraFiles: detectExtraFiles(root, directory),
61
+ })
24
62
  }
25
63
  })
26
64
  return packages.sort((a, b) => a.directory.localeCompare(b.directory))
@@ -45,19 +83,22 @@ export function buildReleaseConfig(root, existing = {}) {
45
83
  const extra = Array.isArray(result['extra-files']) ? result['extra-files'] : []
46
84
  result['extra-files'] = extra
47
85
  for (const file of packages[0].extraFiles) {
48
- if (!extra.some((entry) => (typeof entry === 'string' ? entry : entry?.path) === file)) extra.push(file)
86
+ if (!extra.some((entry) => (typeof entry === 'string' ? entry : entry?.path) === file))
87
+ extra.push(file)
49
88
  }
50
89
  return result
51
90
  }
52
91
  /** @type {Record<string, any>} */
53
- const configuredPackages = { ...(result.packages ?? {}) }
92
+ const configuredPackages = { ...result.packages }
54
93
  for (const entry of packages) {
55
- const current = { ...(configuredPackages[entry.directory] ?? {}) }
94
+ const current = { ...configuredPackages[entry.directory] }
56
95
  current['release-type'] ??= entry.releaseType
57
96
  if (entry.packageName) current['package-name'] ??= entry.packageName
58
97
  /** @type {any[]} */
59
98
  const extra = Array.isArray(current['extra-files']) ? [...current['extra-files']] : []
60
- for (const file of entry.extraFiles) if (!extra.some((item) => (typeof item === 'string' ? item : item?.path) === file)) extra.push(file)
99
+ for (const file of entry.extraFiles)
100
+ if (!extra.some((item) => (typeof item === 'string' ? item : item?.path) === file))
101
+ extra.push(file)
61
102
  if (extra.length) current['extra-files'] = extra
62
103
  configuredPackages[entry.directory] = current
63
104
  }
@@ -75,9 +116,12 @@ export function buildReleaseConfig(root, existing = {}) {
75
116
  * @returns {Record<string, string>|null}
76
117
  */
77
118
  export function buildReleaseManifest(root, config = {}) {
78
- const packages = config.packages && typeof config.packages === 'object' && !Array.isArray(config.packages)
79
- ? config.packages
80
- : config['release-type'] ? { '.': {} } : null
119
+ const packages =
120
+ config.packages && typeof config.packages === 'object' && !Array.isArray(config.packages)
121
+ ? config.packages
122
+ : config['release-type']
123
+ ? { '.': {} }
124
+ : null
81
125
  if (!packages || !Object.keys(packages).length) return null
82
126
  /** @type {Record<string, string>} */
83
127
  const manifest = {}
@@ -94,7 +138,9 @@ function readDirectoryVersion(directory) {
94
138
  try {
95
139
  const parsed = JSON.parse(readFileSync(packagePath, 'utf8'))
96
140
  if (typeof parsed?.version === 'string' && parsed.version) return parsed.version
97
- } catch { /* fall through to other manifests */ }
141
+ } catch {
142
+ /* fall through to other manifests */
143
+ }
98
144
  }
99
145
  for (const file of ['pyproject.toml', 'Cargo.toml']) {
100
146
  const path = join(directory, file)
@@ -110,13 +156,17 @@ export function validateReleaseConfig(root, config) {
110
156
  const detected = detectReleasePackages(root)
111
157
  if (!detected.length) return []
112
158
  const configured = config.packages && typeof config.packages === 'object' ? config.packages : null
113
- if (detected.length > 1 && !configured) return ['mixed-language or multi-package repositories require release-please packages configuration']
159
+ if (detected.length > 1 && !configured)
160
+ return [
161
+ 'mixed-language or multi-package repositories require release-please packages configuration',
162
+ ]
114
163
  if (!configured) return []
115
164
  const errors = []
116
165
  for (const entry of detected) {
117
166
  const item = configured[entry.directory]
118
167
  if (!item) errors.push(`release-please packages is missing ${entry.directory}`)
119
- else if (item['release-type'] !== entry.releaseType) errors.push(`${entry.directory} release-type should be ${entry.releaseType}`)
168
+ else if (item['release-type'] !== entry.releaseType)
169
+ errors.push(`${entry.directory} release-type should be ${entry.releaseType}`)
120
170
  }
121
171
  return errors
122
172
  }
@@ -137,17 +187,29 @@ function walk(root, visit) {
137
187
 
138
188
  /** @param {string} root @param {string} directory @returns {string[]} */
139
189
  function detectExtraFiles(root, directory) {
140
- const candidates = ['version.txt', 'VERSION', '.version', 'version.json', 'src/version.ts', 'src/version.js', 'src/version.py', 'src/version.rs']
190
+ const candidates = [
191
+ 'version.txt',
192
+ 'VERSION',
193
+ '.version',
194
+ 'version.json',
195
+ 'src/version.ts',
196
+ 'src/version.js',
197
+ 'src/version.py',
198
+ 'src/version.rs',
199
+ ]
141
200
  return candidates
142
- .map((file) => directory === '.' ? file : `${directory}/${file}`)
201
+ .map((file) => (directory === '.' ? file : `${directory}/${file}`))
143
202
  .filter((file) => existsSync(join(root, file)))
144
- .map((file) => directory === '.' ? file : file.slice(directory.length + 1))
203
+ .map((file) => (directory === '.' ? file : file.slice(directory.length + 1)))
145
204
  }
146
205
 
147
206
  /** @param {string} file @returns {any} */
148
207
  function readJson(file) {
149
- try { return JSON.parse(readFileSync(file, 'utf8')) }
150
- catch { return {} }
208
+ try {
209
+ return JSON.parse(readFileSync(file, 'utf8'))
210
+ } catch {
211
+ return {}
212
+ }
151
213
  }
152
214
 
153
215
  /** @param {string} file @returns {string|undefined} */