code-foundry 1.10.1 → 1.12.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 +4 -0
- package/.github/workflows/ci.yml +4 -0
- package/.github/workflows/cloudflare-delivery.yml +354 -0
- package/.github/workflows/cloudflare-deploy.yml +90 -47
- package/.github/workflows/codeql.yml +2 -0
- package/.github/workflows/security.yml +2 -0
- package/.github/workflows/test.yml +5 -0
- package/.github/workflows/validation-no-codeql.yml +1 -0
- package/.github/workflows/validation.yml +1 -0
- package/.github/workflows/validation_self-ci.yml +1 -0
- package/CHANGELOG.md +14 -0
- package/docs/CONFIGURATION.md +23 -10
- package/docs/README.md +1 -0
- package/docs/cloudflare-delivery.md +188 -0
- package/docs/required-capabilities.md +82 -0
- package/package.json +1 -1
- package/src/commands/cloudflare-delivery.mjs +351 -0
- package/src/commands/sync.mjs +4 -0
- package/src/lib/cloudflare-delivery.mjs +233 -0
- package/src/lib/task-policy.mjs +290 -0
- package/src/runtime-core.mjs +794 -0
- package/src/runtime.mjs +179 -764
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { resolve } from 'node:path'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { detectPackageManager, resolveProfile } from './lib/profile.mjs'
|
|
8
|
+
import { configured, readConfig } from './lib/config.mjs'
|
|
9
|
+
import { classifyTestFiles } from './lib/test-discovery.mjs'
|
|
10
|
+
import { classifyValidationMode, evaluateValidationGate } from './lib/validation-policy.mjs'
|
|
11
|
+
import { readReleaseConfig, validateGeneratedReleaseDiff } from './lib/release-policy.mjs'
|
|
12
|
+
import { runNodePackagePerformance } from './lib/node-package-performance.mjs'
|
|
13
|
+
|
|
14
|
+
const root = process.cwd()
|
|
15
|
+
const config = readConfig(resolve(root, '.github/code-foundry.yml'))
|
|
16
|
+
const repoProfile = resolveProfile(root)
|
|
17
|
+
/** @type {string[]} */
|
|
18
|
+
const languages = configured(config.languages, repoProfile.languages).split(',').filter(Boolean)
|
|
19
|
+
const packageManager = configured(config.package_manager, detectPackageManager(root))
|
|
20
|
+
const output = process.env.GITHUB_OUTPUT
|
|
21
|
+
|
|
22
|
+
/** @param {string} language */
|
|
23
|
+
function hasLanguage(language) {
|
|
24
|
+
return languages.includes(language) || languages.includes('all')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasRootJavascriptProject() {
|
|
28
|
+
return Boolean(readPackage()) && packageManager !== 'none'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hasRootPythonProject() {
|
|
32
|
+
return (
|
|
33
|
+
existsSync(resolve(root, 'pyproject.toml')) ||
|
|
34
|
+
existsSync(resolve(root, 'uv.lock')) ||
|
|
35
|
+
capture('git', ['ls-files', '*requirements*.txt']) !== ''
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasRootRustProject() {
|
|
40
|
+
return existsSync(resolve(root, 'Cargo.toml'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {string} name */
|
|
44
|
+
function hasScript(name) {
|
|
45
|
+
return readPackage()?.scripts?.[name] !== undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readPackage() {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'))
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function performanceEnabled() {
|
|
57
|
+
return configured(config.performance, 'auto') !== 'false'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function performanceCommands() {
|
|
61
|
+
const raw = configured(config.performance_command, '').trim()
|
|
62
|
+
if (!raw) return []
|
|
63
|
+
let command
|
|
64
|
+
try {
|
|
65
|
+
command = JSON.parse(raw)
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error('performance_command must be a JSON argv array or an array of argv arrays.')
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(command) || command.length === 0)
|
|
70
|
+
throw new Error('performance_command must be a non-empty JSON array.')
|
|
71
|
+
const commands = command.every((argument) => typeof argument === 'string') ? [command] : command
|
|
72
|
+
if (
|
|
73
|
+
!commands.every(
|
|
74
|
+
(argv) =>
|
|
75
|
+
Array.isArray(argv) &&
|
|
76
|
+
argv.length > 0 &&
|
|
77
|
+
argv.every((argument) => typeof argument === 'string' && argument.length > 0)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
throw new Error(
|
|
81
|
+
'performance_command must contain one argv array or only non-empty argv arrays of non-empty strings.'
|
|
82
|
+
)
|
|
83
|
+
return /** @type {string[][]} */ (commands)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function performanceProfile() {
|
|
87
|
+
const profile = configured(config.performance_profile, '').trim()
|
|
88
|
+
if (!profile) return ''
|
|
89
|
+
if (profile !== 'node-package')
|
|
90
|
+
throw new Error(`Unsupported performance_profile: ${profile}. Expected node-package.`)
|
|
91
|
+
return profile
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const performanceResultsDirectory = 'performance-results'
|
|
95
|
+
|
|
96
|
+
/** @returns {{source: string, argv: string[]}[]} */
|
|
97
|
+
function selectedPerformanceCommands() {
|
|
98
|
+
const name = ['performance:check', 'perf:check'].find((candidate) => hasScript(candidate))
|
|
99
|
+
if (name) {
|
|
100
|
+
const [manager, args] = packageCommand(['run', name])
|
|
101
|
+
return manager ? [{ source: `package-script:${name}`, argv: [manager, ...args] }] : []
|
|
102
|
+
}
|
|
103
|
+
return performanceCommands().map((argv) => ({ source: 'configuration', argv }))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @param {string} startedAt @param {'passed'|'failed'} status @param {{source: string, argv: string[], status: number}[]} commands @param {string[]} artifacts @param {string|null} error */
|
|
107
|
+
function writePerformanceSummary(startedAt, status, commands, artifacts, error = null) {
|
|
108
|
+
const directory = resolve(root, performanceResultsDirectory)
|
|
109
|
+
mkdirSync(directory, { recursive: true })
|
|
110
|
+
writeFileSync(
|
|
111
|
+
resolve(directory, 'summary.json'),
|
|
112
|
+
`${JSON.stringify(
|
|
113
|
+
{
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
kind: 'code-foundry-performance-summary',
|
|
116
|
+
status,
|
|
117
|
+
startedAt,
|
|
118
|
+
completedAt: new Date().toISOString(),
|
|
119
|
+
commands,
|
|
120
|
+
profile: performanceProfile() || null,
|
|
121
|
+
artifacts,
|
|
122
|
+
error,
|
|
123
|
+
},
|
|
124
|
+
null,
|
|
125
|
+
2
|
|
126
|
+
)}\n`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runPerformance() {
|
|
131
|
+
if (!performanceEnabled()) return
|
|
132
|
+
const startedAt = new Date().toISOString()
|
|
133
|
+
/** @type {{source: string, argv: string[], status: number}[]} */
|
|
134
|
+
const records = []
|
|
135
|
+
/** @type {string[]} */
|
|
136
|
+
const artifacts = []
|
|
137
|
+
try {
|
|
138
|
+
for (const command of selectedPerformanceCommands()) {
|
|
139
|
+
const result = spawnSync(command.argv[0], command.argv.slice(1), {
|
|
140
|
+
cwd: root,
|
|
141
|
+
stdio: 'inherit',
|
|
142
|
+
env: process.env,
|
|
143
|
+
})
|
|
144
|
+
if (result.error) throw result.error
|
|
145
|
+
const status = result.status ?? 1
|
|
146
|
+
records.push({ ...command, status })
|
|
147
|
+
if (status !== 0) {
|
|
148
|
+
writePerformanceSummary(startedAt, 'failed', records, artifacts, `command exited ${status}`)
|
|
149
|
+
process.exitCode = status
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (performanceProfile() === 'node-package') {
|
|
154
|
+
const result = runNodePackagePerformance(
|
|
155
|
+
root,
|
|
156
|
+
configured(config.performance_budget_file, 'performance-package-budgets.json'),
|
|
157
|
+
performanceResultsDirectory
|
|
158
|
+
)
|
|
159
|
+
artifacts.push(`${performanceResultsDirectory}/node-package.json`)
|
|
160
|
+
if (!result.passed) {
|
|
161
|
+
writePerformanceSummary(startedAt, 'failed', records, artifacts, result.failures.join('; '))
|
|
162
|
+
process.exitCode = 1
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
writePerformanceSummary(startedAt, 'passed', records, artifacts)
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
169
|
+
writePerformanceSummary(startedAt, 'failed', records, artifacts, message)
|
|
170
|
+
throw error
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @param {string} task */
|
|
175
|
+
function validation(task) {
|
|
176
|
+
if (task === 'mode') {
|
|
177
|
+
writeOutput(
|
|
178
|
+
'mode',
|
|
179
|
+
classifyValidationMode({
|
|
180
|
+
eventName: process.env.FOUNDRY_EVENT_NAME ?? '',
|
|
181
|
+
baseRef: process.env.FOUNDRY_BASE_REF ?? '',
|
|
182
|
+
headRef: process.env.FOUNDRY_HEAD_REF ?? '',
|
|
183
|
+
stagingMode: config.staging_validation_mode,
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (task === 'gate') {
|
|
189
|
+
const mode = process.env.FOUNDRY_MODE ?? ''
|
|
190
|
+
const gate = evaluateValidationGate({
|
|
191
|
+
mode,
|
|
192
|
+
results: {
|
|
193
|
+
ci: process.env.FOUNDRY_CI,
|
|
194
|
+
test: process.env.FOUNDRY_TEST,
|
|
195
|
+
security: process.env.FOUNDRY_SECURITY,
|
|
196
|
+
codeql: process.env.FOUNDRY_CODEQL,
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
if (gate.valid) {
|
|
200
|
+
console.log(`Validation gate passed for ${mode} mode.`)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
for (const failure of gate.failures) console.error(`::error::${failure.job}: ${failure.result}`)
|
|
204
|
+
process.exitCode = 1
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (task === 'release_diff') {
|
|
208
|
+
const baseSha = process.env.FOUNDRY_BASE_SHA ?? ''
|
|
209
|
+
const changedPaths = baseSha
|
|
210
|
+
? capture('git', ['diff', '--name-only', `${baseSha}...HEAD`]).split(/\r?\n/)
|
|
211
|
+
: []
|
|
212
|
+
const result = validateGeneratedReleaseDiff({
|
|
213
|
+
headRef: process.env.FOUNDRY_HEAD_REF ?? '',
|
|
214
|
+
headRepo: process.env.FOUNDRY_HEAD_REPO ?? '',
|
|
215
|
+
repository: process.env.FOUNDRY_REPOSITORY ?? '',
|
|
216
|
+
changedPaths,
|
|
217
|
+
config: readReleaseConfig(root),
|
|
218
|
+
root,
|
|
219
|
+
})
|
|
220
|
+
if (!result.valid) {
|
|
221
|
+
for (const error of result.errors) console.error(`::error::${error}`)
|
|
222
|
+
process.exitCode = 1
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
console.log(`Release policy passed: ${result.changedPaths.length} approved changed path(s).`)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`Unknown validation task: ${task || '(missing)'}`)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** @param {string} key @param {unknown} value */
|
|
232
|
+
function writeOutput(key, value) {
|
|
233
|
+
const line = `${key}=${typeof value === 'string' ? value : JSON.stringify(value)}\n`
|
|
234
|
+
if (output) requireWrite(output, line)
|
|
235
|
+
else console.log(line.trimEnd())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** @param {string} file @param {string} content */
|
|
239
|
+
function requireWrite(file, content) {
|
|
240
|
+
appendFileSync(file, content)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @param {string} command */
|
|
244
|
+
function commandExists(command) {
|
|
245
|
+
return spawnSync(command, ['--version'], { stdio: 'ignore' }).status === 0
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @param {string} command @param {string[]} [args] @param {Record<string, unknown>} [options] */
|
|
249
|
+
function run(command, args = [], options = {}) {
|
|
250
|
+
const result = spawnSync(command, args, {
|
|
251
|
+
cwd: root,
|
|
252
|
+
stdio: 'inherit',
|
|
253
|
+
env: process.env,
|
|
254
|
+
...options,
|
|
255
|
+
})
|
|
256
|
+
if (result.error) throw result.error
|
|
257
|
+
if (result.status !== 0) process.exit(result.status ?? 1)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** @param {string} command @param {string[]} [args] */
|
|
261
|
+
function capture(command, args = []) {
|
|
262
|
+
const result = spawnSync(command, args, { cwd: root, encoding: 'utf8', env: process.env })
|
|
263
|
+
return result.status === 0 ? result.stdout.trim() : ''
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @returns {string[]} */
|
|
267
|
+
function repositoryTestFiles() {
|
|
268
|
+
return capture('git', ['ls-files'])
|
|
269
|
+
.split(/\r?\n/)
|
|
270
|
+
.map((file) => file.trim())
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** @param {'unit'|'integration'|'e2e'|'smoke'} task */
|
|
275
|
+
function taskTestFiles(task) {
|
|
276
|
+
return classifyTestFiles(repositoryTestFiles(), task)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** @param {string[]} args @returns {[string|null, string[]]} */
|
|
280
|
+
function packageCommand(args) {
|
|
281
|
+
switch (packageManager) {
|
|
282
|
+
case 'bun':
|
|
283
|
+
return ['bun', args]
|
|
284
|
+
case 'pnpm':
|
|
285
|
+
return ['pnpm', args]
|
|
286
|
+
case 'yarn':
|
|
287
|
+
return ['yarn', args]
|
|
288
|
+
case 'npm':
|
|
289
|
+
return ['npm', args]
|
|
290
|
+
default:
|
|
291
|
+
return [null, args]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @param {string[]} names */
|
|
296
|
+
function runScript(names) {
|
|
297
|
+
const name = names.find((candidate) => hasScript(candidate))
|
|
298
|
+
if (!name) return false
|
|
299
|
+
const [manager, args] = packageCommand(['run', name])
|
|
300
|
+
if (!manager) return false
|
|
301
|
+
run(manager, args)
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** @param {string} tool @param {string[]} [args] */
|
|
306
|
+
function runTool(tool, args = []) {
|
|
307
|
+
if (['ruff', 'pytest', 'pylint'].includes(tool)) {
|
|
308
|
+
const venvTool = resolve(root, `.venv/bin/${tool}`)
|
|
309
|
+
if (existsSync(venvTool)) return run(venvTool, args)
|
|
310
|
+
}
|
|
311
|
+
const [manager] = packageCommand([])
|
|
312
|
+
if (manager === 'bun') return run('bunx', ['--no-install', tool, ...args])
|
|
313
|
+
if (manager === 'pnpm') return run('pnpm', ['exec', tool, ...args])
|
|
314
|
+
if (manager === 'yarn') return run('yarn', ['exec', tool, ...args])
|
|
315
|
+
if (manager === 'npm') return run('npx', ['--no-install', tool, ...args])
|
|
316
|
+
return run(tool, args)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function install() {
|
|
320
|
+
if (existsSync(resolve(root, 'package.json'))) {
|
|
321
|
+
const lock = ['bun.lock', 'bun.lockb', 'pnpm-lock.yaml', 'yarn.lock', 'package-lock.json'].find(
|
|
322
|
+
(file) => existsSync(resolve(root, file))
|
|
323
|
+
)
|
|
324
|
+
if (lock) {
|
|
325
|
+
/** @type {Record<string, [string, string[]]>} */
|
|
326
|
+
const commands = {
|
|
327
|
+
bun: [
|
|
328
|
+
'bun',
|
|
329
|
+
[
|
|
330
|
+
'install',
|
|
331
|
+
'--frozen-lockfile',
|
|
332
|
+
'--ignore-scripts',
|
|
333
|
+
...(readPackage()?.workspaces ? ['--force', '--linker=hoisted'] : []),
|
|
334
|
+
],
|
|
335
|
+
],
|
|
336
|
+
pnpm: ['pnpm', ['install', '--frozen-lockfile', '--prefer-offline']],
|
|
337
|
+
yarn: ['yarn', ['install', '--immutable']],
|
|
338
|
+
npm: ['npm', ['ci', '--prefer-offline', '--no-audit', '--fund=false']],
|
|
339
|
+
}
|
|
340
|
+
const command = commands[packageManager]
|
|
341
|
+
if (command) run(command[0], command[1])
|
|
342
|
+
if (packageManager === 'bun') runScript(['prepare', 'postinstall'])
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
hasLanguage('python') &&
|
|
347
|
+
(existsSync(resolve(root, 'pyproject.toml')) || existsSync(resolve(root, 'requirements.txt')))
|
|
348
|
+
) {
|
|
349
|
+
if (commandExists('uv') && existsSync(resolve(root, 'uv.lock'))) run('uv', ['sync', '--frozen'])
|
|
350
|
+
else {
|
|
351
|
+
if (!existsSync(resolve(root, '.venv'))) run('python', ['-m', 'venv', '.venv'])
|
|
352
|
+
const python = resolve(root, '.venv/bin/python')
|
|
353
|
+
if (spawnSync(python, ['-m', 'pip', '--version'], { stdio: 'ignore' }).status !== 0) {
|
|
354
|
+
run(python, ['-m', 'ensurepip', '--upgrade'])
|
|
355
|
+
}
|
|
356
|
+
if (existsSync(resolve(root, 'requirements.txt')))
|
|
357
|
+
run(python, [
|
|
358
|
+
'-m',
|
|
359
|
+
'pip',
|
|
360
|
+
'install',
|
|
361
|
+
'--disable-pip-version-check',
|
|
362
|
+
'-r',
|
|
363
|
+
'requirements.txt',
|
|
364
|
+
])
|
|
365
|
+
if (existsSync(resolve(root, 'requirements-dev.txt')))
|
|
366
|
+
run(python, [
|
|
367
|
+
'-m',
|
|
368
|
+
'pip',
|
|
369
|
+
'install',
|
|
370
|
+
'--disable-pip-version-check',
|
|
371
|
+
'-r',
|
|
372
|
+
'requirements-dev.txt',
|
|
373
|
+
])
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (hasLanguage('rust') && existsSync(resolve(root, 'Cargo.toml'))) run('cargo', ['fetch'])
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** @param {string} task */
|
|
380
|
+
function relevant(task) {
|
|
381
|
+
const js = hasLanguage('typescript') || hasLanguage('javascript')
|
|
382
|
+
const python = hasLanguage('python')
|
|
383
|
+
const rust = hasLanguage('rust')
|
|
384
|
+
const scripted = {
|
|
385
|
+
format: ['format:check', 'format', 'fmt'],
|
|
386
|
+
lint: ['lint'],
|
|
387
|
+
type_check: ['type-check', 'typecheck', 'type:check'],
|
|
388
|
+
build: ['build'],
|
|
389
|
+
unit: ['test:unit', 'test:coverage', 'test'],
|
|
390
|
+
integration: ['test:integration'],
|
|
391
|
+
e2e: ['test:e2e', 'e2e'],
|
|
392
|
+
smoke: ['test:smoke', 'smoke'],
|
|
393
|
+
performance: ['performance:check', 'perf:check'],
|
|
394
|
+
}[task]
|
|
395
|
+
if (task === 'performance') {
|
|
396
|
+
if (!performanceEnabled()) return false
|
|
397
|
+
return Boolean(
|
|
398
|
+
(scripted && scripted.some((candidate) => hasScript(candidate))) ||
|
|
399
|
+
performanceCommands().length > 0 ||
|
|
400
|
+
performanceProfile()
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
if (scripted && scripted.some((candidate) => hasScript(candidate)))
|
|
404
|
+
return packageManager !== 'none'
|
|
405
|
+
if (task === 'format' || task === 'lint' || task === 'type_check' || task === 'build') {
|
|
406
|
+
return (
|
|
407
|
+
(js && hasRootJavascriptProject()) ||
|
|
408
|
+
(python && hasRootPythonProject()) ||
|
|
409
|
+
(rust && hasRootRustProject())
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
if (['unit', 'integration', 'e2e', 'smoke'].includes(task)) {
|
|
413
|
+
if (taskTestFiles(/** @type {'unit'|'integration'|'e2e'|'smoke'} */ (task)).length > 0)
|
|
414
|
+
return true
|
|
415
|
+
// A project with inline Rust/Python/JavaScript unit tests is still a unit
|
|
416
|
+
// test target even when it has no conventional test file path. Other
|
|
417
|
+
// categories must have an explicit script or discoverable test files so
|
|
418
|
+
// their workflow jobs become skipped instead of successful no-ops.
|
|
419
|
+
if (task === 'unit') {
|
|
420
|
+
return (
|
|
421
|
+
(js && hasRootJavascriptProject()) ||
|
|
422
|
+
(python && hasRootPythonProject()) ||
|
|
423
|
+
(rust && hasRootRustProject()) ||
|
|
424
|
+
(hasLanguage('solidity') && hasRootJavascriptProject())
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
return false
|
|
428
|
+
}
|
|
429
|
+
return true
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** @param {string} ecosystem */
|
|
433
|
+
function hasDependencyManifest(ecosystem) {
|
|
434
|
+
if (ecosystem === 'javascript') {
|
|
435
|
+
const packageJson = readPackage()
|
|
436
|
+
return (
|
|
437
|
+
['bun.lock', 'bun.lockb', 'pnpm-lock.yaml', 'yarn.lock', 'package-lock.json'].some((file) =>
|
|
438
|
+
existsSync(resolve(root, file))
|
|
439
|
+
) ||
|
|
440
|
+
Boolean(
|
|
441
|
+
packageJson &&
|
|
442
|
+
['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'].some(
|
|
443
|
+
(group) => Object.keys(packageJson[group] ?? {}).length
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
if (ecosystem === 'rust') return existsSync(resolve(root, 'Cargo.toml'))
|
|
449
|
+
if (ecosystem === 'python')
|
|
450
|
+
return (
|
|
451
|
+
existsSync(resolve(root, 'pyproject.toml')) ||
|
|
452
|
+
existsSync(resolve(root, 'uv.lock')) ||
|
|
453
|
+
capture('git', ['ls-files', '*requirements*.txt']) !== ''
|
|
454
|
+
)
|
|
455
|
+
return false
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Detect repository-owned Oxlint setup: an oxlint dependency (or script
|
|
460
|
+
* reference) or a tracked `.oxlintrc.json` / `oxlint.config.*` file. Oxlint
|
|
461
|
+
* is the baseline's preferred JavaScript linter; repositories without one
|
|
462
|
+
* fall back to their own `lint` script if defined.
|
|
463
|
+
* @returns {boolean}
|
|
464
|
+
*/
|
|
465
|
+
function hasOxlintSetup() {
|
|
466
|
+
const pkg = readPackage() ?? {}
|
|
467
|
+
const scripts = pkg.scripts ?? {}
|
|
468
|
+
const dependencies = {
|
|
469
|
+
...pkg.dependencies,
|
|
470
|
+
...pkg.devDependencies,
|
|
471
|
+
...pkg.optionalDependencies,
|
|
472
|
+
...pkg.peerDependencies,
|
|
473
|
+
}
|
|
474
|
+
if (dependencies.oxlint) return true
|
|
475
|
+
if (Object.values(scripts).some((value) => /\boxlint\b/.test(String(value)))) return true
|
|
476
|
+
return repositoryTestFiles().some((file) =>
|
|
477
|
+
/(^|\/)(\.oxlintrc\.json|oxlint\.config\.[^/]*)$/.test(file)
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Detect repository-owned Oxfmt setup: an oxfmt dependency (or script
|
|
483
|
+
* reference) or a tracked `.oxfmtrc.json` / `oxfmt.config.*` file. Oxfmt is
|
|
484
|
+
* the baseline's formatter; repositories using a different formatter opt out
|
|
485
|
+
* through their own `format`/`fmt` scripts, which the script fallback honors.
|
|
486
|
+
* @returns {boolean}
|
|
487
|
+
*/
|
|
488
|
+
function hasOxfmtSetup() {
|
|
489
|
+
const pkg = readPackage() ?? {}
|
|
490
|
+
const scripts = pkg.scripts ?? {}
|
|
491
|
+
const dependencies = {
|
|
492
|
+
...pkg.dependencies,
|
|
493
|
+
...pkg.devDependencies,
|
|
494
|
+
...pkg.optionalDependencies,
|
|
495
|
+
...pkg.peerDependencies,
|
|
496
|
+
}
|
|
497
|
+
if (dependencies.oxfmt) return true
|
|
498
|
+
if (Object.values(scripts).some((value) => /\boxfmt\b/.test(String(value)))) return true
|
|
499
|
+
return repositoryTestFiles().some((file) =>
|
|
500
|
+
/(^|\/)(\.oxfmtrc\.json|oxfmt\.config\.[^/]*)$/.test(file)
|
|
501
|
+
)
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** @param {string} task */
|
|
505
|
+
function ci(task) {
|
|
506
|
+
if (task === 'install') return install()
|
|
507
|
+
if (task === 'should_run' || task === 'task_profile') {
|
|
508
|
+
const selected = process.argv[4]
|
|
509
|
+
writeOutput('applicable', relevant(selected) ? 'true' : 'false')
|
|
510
|
+
writeOutput(
|
|
511
|
+
'javascript',
|
|
512
|
+
hasLanguage('typescript') || hasLanguage('javascript') ? 'true' : 'false'
|
|
513
|
+
)
|
|
514
|
+
writeOutput('python', hasLanguage('python') ? 'true' : 'false')
|
|
515
|
+
writeOutput('rust', hasLanguage('rust') ? 'true' : 'false')
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
if (task === 'format') {
|
|
519
|
+
const scripted = runScript(['format:check', 'format', 'fmt'])
|
|
520
|
+
if (
|
|
521
|
+
!scripted &&
|
|
522
|
+
(hasLanguage('typescript') || hasLanguage('javascript')) &&
|
|
523
|
+
hasRootJavascriptProject() &&
|
|
524
|
+
hasOxfmtSetup()
|
|
525
|
+
) {
|
|
526
|
+
// Oxfmt is the baseline's formatter. Repositories that use a different
|
|
527
|
+
// formatter keep full control through their own `format`/`fmt` scripts,
|
|
528
|
+
// which the runScript fallback above already honors.
|
|
529
|
+
runTool('oxfmt', ['--check', '.'])
|
|
530
|
+
}
|
|
531
|
+
if (hasLanguage('python') && hasRootPythonProject()) runTool('ruff', ['format', '--check', '.'])
|
|
532
|
+
if (hasLanguage('rust') && hasRootRustProject()) run('cargo', ['fmt', '--check'])
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if (task === 'lint') {
|
|
536
|
+
const scripted = runScript(['lint'])
|
|
537
|
+
if (
|
|
538
|
+
!scripted &&
|
|
539
|
+
(hasLanguage('typescript') || hasLanguage('javascript')) &&
|
|
540
|
+
hasRootJavascriptProject() &&
|
|
541
|
+
hasOxlintSetup()
|
|
542
|
+
) {
|
|
543
|
+
// Oxlint is the baseline's linter. Repositories that use a different
|
|
544
|
+
// linter keep full control through their own `lint` script, which the
|
|
545
|
+
// runScript fallback above already honors.
|
|
546
|
+
runTool('oxlint', [])
|
|
547
|
+
}
|
|
548
|
+
if (hasLanguage('python') && hasRootPythonProject()) runTool('ruff', ['check', '.'])
|
|
549
|
+
if (hasLanguage('rust') && hasRootRustProject())
|
|
550
|
+
run('cargo', ['clippy', '--all-targets', '--', '-D', 'warnings'])
|
|
551
|
+
return
|
|
552
|
+
}
|
|
553
|
+
if (task === 'type_check') {
|
|
554
|
+
const scripted = runScript(['type-check', 'typecheck', 'type:check'])
|
|
555
|
+
if (!scripted && existsSync(resolve(root, 'tsconfig.json'))) runTool('tsc', ['--noEmit'])
|
|
556
|
+
if (hasLanguage('rust') && hasRootRustProject()) run('cargo', ['check', '--all-targets'])
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (task === 'build') {
|
|
560
|
+
const scripted = runScript(['build'])
|
|
561
|
+
if (!scripted && hasLanguage('rust') && hasRootRustProject())
|
|
562
|
+
run('cargo', ['build', '--all-targets'])
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
if (task === 'performance') {
|
|
566
|
+
return runPerformance()
|
|
567
|
+
}
|
|
568
|
+
/** @type {Record<string, string[]>} */
|
|
569
|
+
const scriptsByTask = {
|
|
570
|
+
unit: ['test:unit', 'test:coverage', 'test'],
|
|
571
|
+
integration: ['test:integration'],
|
|
572
|
+
e2e: ['test:e2e', 'e2e'],
|
|
573
|
+
smoke: ['test:smoke', 'smoke'],
|
|
574
|
+
}
|
|
575
|
+
const scripts = scriptsByTask[task]
|
|
576
|
+
const scripted = scripts ? runScript(scripts) : false
|
|
577
|
+
const testFiles = taskTestFiles(/** @type {'unit'|'integration'|'e2e'|'smoke'} */ (task))
|
|
578
|
+
const javascriptTests = testFiles.filter((file) => /\.(?:[cm]?[jt]sx?)$/i.test(file))
|
|
579
|
+
const pythonTests = testFiles.filter((file) => /\.py$/i.test(file))
|
|
580
|
+
const rustTests = testFiles.filter((file) => /\.rs$/i.test(file))
|
|
581
|
+
|
|
582
|
+
// A repository script is authoritative for that package. When it is absent,
|
|
583
|
+
// use Bun's native runner with only the discovered files for the requested
|
|
584
|
+
// category; this prevents smoke/integration files from being re-run as unit
|
|
585
|
+
// tests and avoids no-op category jobs.
|
|
586
|
+
if (
|
|
587
|
+
!scripted &&
|
|
588
|
+
(hasLanguage('typescript') || hasLanguage('javascript') || hasLanguage('solidity')) &&
|
|
589
|
+
hasRootJavascriptProject()
|
|
590
|
+
) {
|
|
591
|
+
if (javascriptTests.length > 0 || task === 'unit') run('bun', ['test', ...javascriptTests])
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (hasLanguage('python') && hasRootPythonProject()) {
|
|
595
|
+
const python = existsSync(resolve(root, '.venv/bin/python'))
|
|
596
|
+
? resolve(root, '.venv/bin/python')
|
|
597
|
+
: 'python'
|
|
598
|
+
if (pythonTests.length > 0 || task === 'unit') run(python, ['-m', 'pytest', ...pythonTests])
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (hasLanguage('rust') && hasRootRustProject()) {
|
|
602
|
+
if (task === 'unit') {
|
|
603
|
+
if (existsSync(resolve(root, 'src/lib.rs'))) run('cargo', ['test', '--lib'])
|
|
604
|
+
if (existsSync(resolve(root, 'src/main.rs'))) run('cargo', ['test', '--bin', packageName()])
|
|
605
|
+
if (!existsSync(resolve(root, 'src/lib.rs')) && !existsSync(resolve(root, 'src/main.rs')))
|
|
606
|
+
run('cargo', ['test'])
|
|
607
|
+
} else {
|
|
608
|
+
for (const file of rustTests) {
|
|
609
|
+
const match = file.match(/^tests\/(.+)\.rs$/)
|
|
610
|
+
if (match && !match[1].includes('/')) run('cargo', ['test', '--test', match[1]])
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function packageName() {
|
|
617
|
+
const cargo = readFileSafe(resolve(root, 'Cargo.toml'))
|
|
618
|
+
return cargo?.match(/^name\s*=\s*"([^"]+)"/m)?.[1] ?? 'app'
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** @param {string} file */
|
|
622
|
+
function readFileSafe(file) {
|
|
623
|
+
try {
|
|
624
|
+
return readFileSync(file, 'utf8')
|
|
625
|
+
} catch {
|
|
626
|
+
return null
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** @param {string} task @param {string} ecosystem */
|
|
631
|
+
function security(task, ecosystem) {
|
|
632
|
+
if (task === 'profile') {
|
|
633
|
+
writeOutput('javascript', hasDependencyManifest('javascript') ? 'true' : 'false')
|
|
634
|
+
writeOutput('rust', hasDependencyManifest('rust') ? 'true' : 'false')
|
|
635
|
+
writeOutput('python', hasDependencyManifest('python') ? 'true' : 'false')
|
|
636
|
+
const requirements = capture('git', ['ls-files', '*requirements*.txt'])
|
|
637
|
+
.split('\n')
|
|
638
|
+
.filter(Boolean)
|
|
639
|
+
writeOutput(
|
|
640
|
+
'python_requirements',
|
|
641
|
+
hasDependencyManifest('python')
|
|
642
|
+
? requirements.length
|
|
643
|
+
? requirements
|
|
644
|
+
: ['project']
|
|
645
|
+
: ['none']
|
|
646
|
+
)
|
|
647
|
+
writeOutput('dependency_review', featureEnabled('dependency_review') ? 'true' : 'false')
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
if (task === 'should_run') {
|
|
651
|
+
writeOutput('applicable', hasDependencyManifest(ecosystem) ? 'true' : 'false')
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
if (ecosystem === 'javascript' && existsSync(resolve(root, 'package.json'))) {
|
|
655
|
+
const ignores = existsSync(resolve(root, '.github/security-audit-allowlist.txt'))
|
|
656
|
+
? readFileSync(resolve(root, '.github/security-audit-allowlist.txt'), 'utf8')
|
|
657
|
+
.split(/\r?\n/)
|
|
658
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
659
|
+
: []
|
|
660
|
+
/** @type {Record<string, [string, string[]]>} */
|
|
661
|
+
const commands = {
|
|
662
|
+
bun: [
|
|
663
|
+
'bun',
|
|
664
|
+
['audit', '--audit-level=high', ...ignores.flatMap((advisory) => ['--ignore', advisory])],
|
|
665
|
+
],
|
|
666
|
+
pnpm: ['pnpm', ['audit', '--audit-level', 'high']],
|
|
667
|
+
yarn: ['yarn', ['npm', 'audit', '--all', '--recursive']],
|
|
668
|
+
npm: ['npm', ['audit', '--audit-level=high']],
|
|
669
|
+
}
|
|
670
|
+
const command = commands[packageManager]
|
|
671
|
+
if (command) run(command[0], command[1])
|
|
672
|
+
} else if (ecosystem === 'rust' && existsSync(resolve(root, 'Cargo.toml'))) {
|
|
673
|
+
if (!commandExists('cargo-audit'))
|
|
674
|
+
run('cargo', ['install', 'cargo-audit', '--locked', '--quiet'])
|
|
675
|
+
run('cargo', ['audit'])
|
|
676
|
+
} else if (ecosystem === 'python' && hasDependencyManifest('python')) {
|
|
677
|
+
const requirement = process.env.REPO_FOUNDRY_PYTHON_REQUIREMENT
|
|
678
|
+
const auditArgs =
|
|
679
|
+
requirement && !['project', 'none'].includes(requirement) ? ['-r', requirement] : []
|
|
680
|
+
if (commandExists('uv'))
|
|
681
|
+
run('uv', ['tool', 'run', '--from', 'pip-audit==2.10.1', 'pip-audit', ...auditArgs])
|
|
682
|
+
else {
|
|
683
|
+
run('python', ['-m', 'pip', 'install', '--quiet', 'pip-audit==2.10.1'])
|
|
684
|
+
run('python', ['-m', 'pip_audit', ...auditArgs])
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/** @param {string} key */
|
|
690
|
+
function featureEnabled(key) {
|
|
691
|
+
const policy = config[key] ?? 'auto'
|
|
692
|
+
if (policy === 'false') return false
|
|
693
|
+
if (policy === 'true') return true
|
|
694
|
+
return isPublicRepository()
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function isPublicRepository() {
|
|
698
|
+
const visibility = process.env.REPO_FOUNDRY_VISIBILITY
|
|
699
|
+
return visibility ? visibility === 'public' : process.env.REPO_FOUNDRY_PRIVATE !== 'true'
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function codeql() {
|
|
703
|
+
const enabled =
|
|
704
|
+
featureEnabled('codeql') &&
|
|
705
|
+
(isPublicRepository() || process.env.REPO_FOUNDRY_CODE_SECURITY === 'enabled')
|
|
706
|
+
writeOutput('enabled', enabled ? 'true' : 'false')
|
|
707
|
+
if (!enabled) {
|
|
708
|
+
writeOutput('languages', [])
|
|
709
|
+
for (const language of ['actions', 'javascript', 'python', 'rust']) {
|
|
710
|
+
writeOutput(`${language}_available`, 'false')
|
|
711
|
+
writeOutput(`${language}_changed`, 'false')
|
|
712
|
+
writeOutput(`${language}_build_mode`, 'none')
|
|
713
|
+
}
|
|
714
|
+
return
|
|
715
|
+
}
|
|
716
|
+
const available = []
|
|
717
|
+
if (existsSync(resolve(root, '.github/workflows'))) available.push('actions')
|
|
718
|
+
if (hasLanguage('typescript') || hasLanguage('javascript'))
|
|
719
|
+
available.push('javascript-typescript')
|
|
720
|
+
if (hasLanguage('python')) available.push('python')
|
|
721
|
+
if (hasLanguage('rust')) available.push('rust')
|
|
722
|
+
// Upload every configured language on every analysis run so GitHub can
|
|
723
|
+
// compare pull requests against the base branch's code-scanning config.
|
|
724
|
+
const languagesJson = available.map((language) => ({
|
|
725
|
+
language,
|
|
726
|
+
name:
|
|
727
|
+
language === 'javascript-typescript'
|
|
728
|
+
? 'TypeScript'
|
|
729
|
+
: language[0].toUpperCase() + language.slice(1),
|
|
730
|
+
'build-mode': 'none',
|
|
731
|
+
changed: true,
|
|
732
|
+
}))
|
|
733
|
+
writeOutput('languages', languagesJson)
|
|
734
|
+
for (const language of ['actions', 'javascript-typescript', 'python', 'rust']) {
|
|
735
|
+
const entry = languagesJson.find((item) => item.language === language)
|
|
736
|
+
const prefix = language === 'javascript-typescript' ? 'javascript' : language
|
|
737
|
+
writeOutput(`${prefix}_available`, entry ? 'true' : 'false')
|
|
738
|
+
writeOutput(`${prefix}_changed`, entry?.changed ? 'true' : 'false')
|
|
739
|
+
writeOutput(`${prefix}_build_mode`, entry?.['build-mode'] ?? 'none')
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** @param {string} command */
|
|
744
|
+
function printProfile(command) {
|
|
745
|
+
if (command === 'get') {
|
|
746
|
+
const key = process.argv[4]
|
|
747
|
+
console.log(
|
|
748
|
+
config[key] ??
|
|
749
|
+
(key === 'languages'
|
|
750
|
+
? languages.join(',')
|
|
751
|
+
: key === 'package_manager'
|
|
752
|
+
? packageManager
|
|
753
|
+
: '')
|
|
754
|
+
)
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
writeOutput('languages', languages.join(','))
|
|
758
|
+
writeOutput('package_manager', packageManager)
|
|
759
|
+
writeOutput('release_type', config.release_type ?? 'auto')
|
|
760
|
+
writeOutput('npm_publish', config.npm_publish ?? 'false')
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function preCommit() {
|
|
764
|
+
const changed = capture('git', ['diff', '--cached', '--name-only'])
|
|
765
|
+
if (!changed) return
|
|
766
|
+
const check = spawnSync('git', ['diff', '--cached', '--check'], { cwd: root, stdio: 'inherit' })
|
|
767
|
+
if (check.status !== 0) process.exit(check.status ?? 1)
|
|
768
|
+
if (/\.(js|jsx|ts|tsx|json|md|mdx|yml|yaml)$/.test(changed)) {
|
|
769
|
+
ci('format')
|
|
770
|
+
ci('lint')
|
|
771
|
+
}
|
|
772
|
+
if (/\.rs$|(^|\/)Cargo\.toml$/.test(changed)) {
|
|
773
|
+
run('cargo', ['fmt', '--check'])
|
|
774
|
+
run('cargo', ['clippy', '--all-targets', '--', '-D', 'warnings'])
|
|
775
|
+
}
|
|
776
|
+
if (/\.py$|(^|\/)(pyproject\.toml|requirements[^/]*\.txt)$/.test(changed)) {
|
|
777
|
+
runTool('ruff', ['format', '--check', '.'])
|
|
778
|
+
runTool('ruff', ['check', '.'])
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const [area, task, ecosystem] = process.argv.slice(2)
|
|
783
|
+
try {
|
|
784
|
+
if (area === 'ci') ci(task)
|
|
785
|
+
else if (area === 'security') security(task, ecosystem)
|
|
786
|
+
else if (area === 'codeql') codeql()
|
|
787
|
+
else if (area === 'profile') printProfile(task)
|
|
788
|
+
else if (area === 'validation') validation(task)
|
|
789
|
+
else if (area === 'pre-commit') preCommit()
|
|
790
|
+
else throw new Error(`Unknown runtime command: ${area || '(missing)'}`)
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error(error instanceof Error ? error.message : String(error))
|
|
793
|
+
process.exitCode = 1
|
|
794
|
+
}
|