code-foundry 1.8.0 → 1.9.1

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.
@@ -41,4 +41,6 @@ custom_workflows: preserve
41
41
  staging_validation_mode: fast
42
42
  performance: auto
43
43
  performance_command:
44
+ performance_profile:
45
+ performance_budget_file: performance-package-budgets.json
44
46
  opencode_security_model:
@@ -73,7 +73,7 @@ jobs:
73
73
  mode: ${{ needs.mode.outputs.mode }}
74
74
  runtime-repository: 0xPlayerOne/code-foundry
75
75
  # Pull requests validate the proposed runtime commit. Scheduled and
76
- # manually dispatched audits are isolated in validation_audit_self-ci.yml
76
+ # manually dispatched audits use a separate default-branch-pinned caller,
77
77
  # so this caller-selected ref never runs with default-branch cache access.
78
78
  runtime-ref: ${{ github.sha }}
79
79
  ci-runner: ubuntu-latest
package/.gitignore CHANGED
@@ -46,6 +46,7 @@ cache/
46
46
  !.github/actions/cache/action.yml
47
47
  typechain/
48
48
  coverage.json
49
+ performance-results/
49
50
  tests/.bin/
50
51
  **/.app/
51
52
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.9.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.9.0...v1.9.1) (2026-09-08)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** restore protected consumer audits ([#503](https://github.com/0xPlayerOne/code-foundry/issues/503)) ([d045cc4](https://github.com/0xPlayerOne/code-foundry/commit/d045cc4a5543d2096e4d6f33c5e74aa40604c887))
9
+
10
+ ## [1.9.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.8.0...v1.9.0) (2026-09-08)
11
+
12
+
13
+ ### Features
14
+
15
+ * **performance:** standardize package audit contracts ([#501](https://github.com/0xPlayerOne/code-foundry/issues/501)) ([ab30081](https://github.com/0xPlayerOne/code-foundry/commit/ab3008120772922a1affd1e5648a99b97b02e455))
16
+
3
17
  ## [1.8.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.7.0...v1.8.0) (2026-09-08)
4
18
 
5
19
 
package/README.md CHANGED
@@ -35,8 +35,10 @@ contract. For normal updates, edit that file and run `npx code-foundry sync`.
35
35
 
36
36
  ## What it installs
37
37
 
38
- - Short workflow callers for CI, Test, Security, CodeQL, Draft PR, Release PR,
39
- and Release.
38
+ - Short workflow callers for pull-request validation, protected scheduled/manual
39
+ audits, Draft PR, Release PR, and Release.
40
+ - A deterministic performance lane with ordered command support, stable result
41
+ artifacts, and an optional shared Node package budget profile.
40
42
  - A small `.githooks/pre-commit` launcher with language-aware formatting and
41
43
  linting.
42
44
  - An optional `.mise.toml`/`mise.lock`, repository profile configuration, and
@@ -37,7 +37,9 @@ repository manifests and source
37
37
  | `toolchain` | `auto`, `native`, `mise` | Environment setup policy; defaults to `auto` |
38
38
  | `staging_validation_mode` | `fast`, `audit` | Validation tier for pull requests targeting `staging`; defaults to `fast` |
39
39
  | `performance` | `auto`, `true`, `false` | Run a deterministic performance task when a supported entrypoint exists; defaults to `auto` |
40
- | `performance_command` | JSON string array | Command argv for non-package performance harnesses; package scripts take precedence |
40
+ | `performance_command` | JSON argv array or array of argv arrays | One or more ordered commands for non-package harnesses; package scripts take precedence |
41
+ | `performance_profile` | empty, `node-package` | Optional shared package import, memory, archive, and dependency budget harness |
42
+ | `performance_budget_file` | repository path | Budget policy for the shared package harness; defaults to `performance-package-budgets.json` |
41
43
  | `features` | `all` or a list | Standard workflow callers |
42
44
  | `codeql` | `auto`, `true`, `false` | CodeQL policy; public repositories default to enabled, non-public repositories default to disabled |
43
45
  | `codeql_rust_shards` | JSON array of paths | Rust scan scopes; `["all"]` keeps the safe single full scan |
@@ -62,7 +64,8 @@ Supported features are `ci`, `codeql`, `security`, `test`, `draft-pr`,
62
64
 
63
65
  The shared Test workflow exposes a deterministic `Performance` job. In JavaScript
64
66
  repositories it discovers `performance:check` first and `perf:check` second. Other
65
- repositories can declare an argv array without shell interpolation:
67
+ repositories can declare one argv array, or an ordered array of argv arrays, without
68
+ shell interpolation:
66
69
 
67
70
  ```yaml
68
71
  performance: true
@@ -70,6 +73,37 @@ performance_command: '["python3","scripts/performance_audit.py","--check"]'
70
73
  performance_runner: ubuntu-latest
71
74
  ```
72
75
 
76
+ The `node-package` profile adds a shared, repository-configured audit:
77
+
78
+ ```yaml
79
+ performance: true
80
+ performance_profile: node-package
81
+ performance_budget_file: performance-package-budgets.json
82
+ ```
83
+
84
+ ```json
85
+ {
86
+ "schemaVersion": 1,
87
+ "importTarget": "./dist/index.js",
88
+ "controlImport": "typebox",
89
+ "samples": 7,
90
+ "budgets": {
91
+ "coldImportP95Ms": 150,
92
+ "coldImportRssMaxBytes": 25000000,
93
+ "packedBytes": 500000,
94
+ "productionDependencyCount": 20
95
+ }
96
+ }
97
+ ```
98
+
99
+ Supported budgets are `coldImportP50Ms`, `coldImportP95Ms`,
100
+ `coldImportRssMaxBytes`, `coldImportRelativeP50`, `packedBytes`,
101
+ `unpackedBytes`, `packageFileCount`, `packageMapFileCount`, and
102
+ `productionDependencyCount`. The profile writes
103
+ `performance-results/node-package.json`. Every performance run also writes
104
+ `performance-results/summary.json`, including repository-owned scripts and
105
+ configured commands, so artifact consumers have one stable status contract.
106
+
73
107
  `performance: false` disables discovery. Performance budgets, fixtures, mock
74
108
  providers, and live endpoint credentials remain repository-owned. The shared job
75
109
  owns checkout, pinned setup, billing controls, concurrency, and artifact upload;
package/docs/WORKFLOWS.md CHANGED
@@ -2,13 +2,20 @@
2
2
 
3
3
  ## Standard triggers
4
4
 
5
- The canonical validation caller uses pull requests plus bounded audit entry
6
- points; it does not run the same suites again on branch pushes:
5
+ Validation uses two callers so pull-request code and default-branch-capable
6
+ audit events never share a caller-selected runtime ref. The canonical
7
+ `validation.yml` caller handles pull requests only:
7
8
 
8
9
  ```yaml
9
10
  pull_request:
10
11
  branches: [main, staging] # staging-release topology
11
12
  # direct topology: branches: [main]
13
+ ```
14
+
15
+ The separate `validation-audit.yml` caller is pinned to the configured released
16
+ runtime and handles scheduled and manual audits:
17
+
18
+ ```yaml
12
19
  schedule:
13
20
  - cron: '31 6 * * 1'
14
21
  workflow_dispatch:
@@ -76,15 +83,15 @@ repositories can opt in or out without a code change.
76
83
 
77
84
  ## Standard workflow responsibilities
78
85
 
79
- | Workflow | Responsibility |
80
- | ---------- | ------------------------------------------------------------- |
81
- | CI | Format, lint, type-check, and build |
82
- | Test | Unit, performance, integration, E2E, and smoke tests |
83
- | Security | Profile, audits, and public-only Dependency Review |
84
- | CodeQL | GitHub-native code scanning, kept separate from CI |
85
- | Draft PR | Create/update development pull requests |
86
- | Release PR | Promote `staging` into `main` (staging-release topology only) |
87
- | Release | Release Please, GitHub release, and optional npm publication |
86
+ | Workflow | Responsibility |
87
+ | ---------- | ----------------------------------------------------------------- |
88
+ | CI | Format, lint, type-check, and build |
89
+ | Test | Unit, standardized performance, integration, E2E, and smoke tests |
90
+ | Security | Profile, audits, and public-only Dependency Review |
91
+ | CodeQL | GitHub-native code scanning, kept separate from CI |
92
+ | Draft PR | Create/update development pull requests |
93
+ | Release PR | Promote `staging` into `main` (staging-release topology only) |
94
+ | Release | Release Please, GitHub release, and optional npm publication |
88
95
 
89
96
  Use concise job names such as `CI / Format`, `Test / Unit`, and
90
97
  `CodeQL / Analyze (Python)`. Per-language CodeQL analyzers (Rust shards
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
6
6
  "bugs": {
@@ -42,6 +42,7 @@ const standardFiles = [
42
42
  '.github/ISSUE_TEMPLATE/config.yml',
43
43
  '.github/ISSUE_TEMPLATE/feature_request.yml',
44
44
  '.github/workflows/validation.yml',
45
+ '.github/workflows/validation-audit.yml',
45
46
  '.github/workflows/draft-pr.yml',
46
47
  '.github/workflows/release-pr.yml',
47
48
  '.github/workflows/release.yml',
@@ -473,6 +474,9 @@ function sourcePath(source, file) {
473
474
  return existsSync(rootTemplate) ? rootTemplate : join(source, 'src/templates/gitignore')
474
475
  }
475
476
  if (file.startsWith('.github/workflows/')) {
477
+ if (file === '.github/workflows/validation-audit.yml') {
478
+ return join(source, 'src/templates/workflows/validation-audit.yml')
479
+ }
476
480
  const name = file.slice('.github/workflows/'.length, -4)
477
481
  return join(source, '.github/workflows', `${name}_self-ci.yml`)
478
482
  }
@@ -535,7 +539,7 @@ function renderWorkflow(content, config, repository, ref, rustCodeql) {
535
539
  if (workflow === 'test' && config.unit_runner) {
536
540
  rendered = rendered.replace(/^(\s+unit-runner:)\s+.*$/m, `$1 ${config.unit_runner}`)
537
541
  }
538
- if (workflow === 'validation') {
542
+ if (workflow === 'validation' || workflow === 'validation-audit') {
539
543
  // The mode classifier is a normal runner job in the caller rather than a
540
544
  // reusable-workflow input. Keep it on the configured default runner so
541
545
  // consumers that cannot use ubuntu-slim do not fail before validation.
@@ -1092,6 +1096,8 @@ function createDefaultConfig(root, source, configuredWorkflow) {
1092
1096
  staging_validation_mode: 'fast',
1093
1097
  performance: 'auto',
1094
1098
  performance_command: '',
1099
+ performance_profile: '',
1100
+ performance_budget_file: 'performance-package-budgets.json',
1095
1101
  prune_standard: 'false',
1096
1102
  cache_packages: 'auto',
1097
1103
  cache_build: 'auto',
@@ -0,0 +1,224 @@
1
+ // @ts-check
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
4
+ import { dirname, resolve } from 'node:path'
5
+ import { pathToFileURL } from 'node:url'
6
+ import { spawnSync } from 'node:child_process'
7
+
8
+ const METRICS = new Set([
9
+ 'coldImportP50Ms',
10
+ 'coldImportP95Ms',
11
+ 'coldImportRssMaxBytes',
12
+ 'coldImportRelativeP50',
13
+ 'packedBytes',
14
+ 'unpackedBytes',
15
+ 'packageFileCount',
16
+ 'packageMapFileCount',
17
+ 'productionDependencyCount',
18
+ ])
19
+
20
+ /** @param {number[]} values @param {number} percent */
21
+ export function percentile(values, percent) {
22
+ if (values.length === 0) throw new Error('percentile requires at least one value')
23
+ // oxlint-disable-next-line unicorn/no-array-sort -- The project typechecks against ES2022.
24
+ const ordered = [...values].sort((a, b) => a - b)
25
+ const position = ((ordered.length - 1) * percent) / 100
26
+ const lower = Math.floor(position)
27
+ const upper = Math.min(lower + 1, ordered.length - 1)
28
+ const fraction = position - lower
29
+ return ordered[lower] + (ordered[upper] - ordered[lower]) * fraction
30
+ }
31
+
32
+ /** @param {string} root @param {string} budgetFile @param {string} resultsDirectory */
33
+ export function runNodePackagePerformance(
34
+ root,
35
+ budgetFile = 'performance-package-budgets.json',
36
+ resultsDirectory = 'performance-results'
37
+ ) {
38
+ const packageJson = readJson(resolve(root, 'package.json'), 'package.json')
39
+ const policy = readJson(resolve(root, budgetFile), budgetFile)
40
+ if (policy.schemaVersion !== 1) throw new Error(`${budgetFile} must declare schemaVersion 1.`)
41
+
42
+ const samples = policy.samples ?? 7
43
+ if (!Number.isInteger(samples) || samples < 3 || samples > 50)
44
+ throw new Error(`${budgetFile} samples must be an integer from 3 through 50.`)
45
+ const budgets = policy.budgets
46
+ if (!budgets || typeof budgets !== 'object' || Array.isArray(budgets))
47
+ throw new Error(`${budgetFile} budgets must be an object.`)
48
+ for (const [name, maximum] of Object.entries(budgets)) {
49
+ if (!METRICS.has(name)) throw new Error(`${budgetFile} contains unknown metric ${name}.`)
50
+ if (typeof maximum !== 'number' || !Number.isFinite(maximum) || maximum < 0)
51
+ throw new Error(`${budgetFile} budget ${name} must be a non-negative finite number.`)
52
+ }
53
+
54
+ const importTarget = resolve(root, policy.importTarget ?? packageEntry(packageJson))
55
+ if (!existsSync(importTarget))
56
+ throw new Error(`Node package performance import target does not exist: ${importTarget}`)
57
+ const imports = measureImports(root, pathToFileURL(importTarget).href, samples)
58
+ const control = policy.controlImport
59
+ ? measureImports(root, String(policy.controlImport), samples)
60
+ : null
61
+ const pack = npmJson(root, ['pack', '--dry-run', '--json', '--ignore-scripts'], 'npm pack')
62
+ const packed = Array.isArray(pack) ? pack[0] : pack
63
+ if (!packed || typeof packed !== 'object') throw new Error('npm pack returned no package result.')
64
+ /** @type {Array<{path?: string}>} */
65
+ const files = Array.isArray(packed.files) ? packed.files : []
66
+ const productionDependencyCount = npmProductionDependencyCount(root)
67
+ const metrics = {
68
+ coldImportP50Ms: round(
69
+ percentile(
70
+ imports.map((sample) => sample.durationMs),
71
+ 50
72
+ )
73
+ ),
74
+ coldImportP95Ms: round(
75
+ percentile(
76
+ imports.map((sample) => sample.durationMs),
77
+ 95
78
+ )
79
+ ),
80
+ coldImportRssMaxBytes: Math.max(...imports.map((sample) => sample.rssDeltaBytes)),
81
+ ...(control
82
+ ? {
83
+ coldImportRelativeP50: round(
84
+ percentile(
85
+ imports.map((sample) => sample.durationMs),
86
+ 50
87
+ ) /
88
+ percentile(
89
+ control.map((sample) => sample.durationMs),
90
+ 50
91
+ )
92
+ ),
93
+ }
94
+ : {}),
95
+ packedBytes: numberField(packed, 'size'),
96
+ unpackedBytes: numberField(packed, 'unpackedSize'),
97
+ packageFileCount: files.length,
98
+ packageMapFileCount: files.filter(
99
+ (file) => file && typeof file === 'object' && String(file.path ?? '').endsWith('.map')
100
+ ).length,
101
+ productionDependencyCount,
102
+ }
103
+ const failures = Object.entries(budgets).flatMap(([name, maximum]) => {
104
+ const value = metrics[/** @type {keyof typeof metrics} */ (name)]
105
+ if (typeof value !== 'number' || !Number.isFinite(value)) return [`${name}: metric unavailable`]
106
+ return value > maximum ? [`${name}: ${value} > ${maximum}`] : []
107
+ })
108
+ const result = {
109
+ schemaVersion: 1,
110
+ kind: 'code-foundry-node-package-performance',
111
+ recordedAt: new Date().toISOString(),
112
+ package: typeof packageJson.name === 'string' ? packageJson.name : null,
113
+ importTarget: String(policy.importTarget ?? packageEntry(packageJson)),
114
+ controlImport: policy.controlImport ?? null,
115
+ samples,
116
+ metrics,
117
+ budgets,
118
+ failures,
119
+ passed: failures.length === 0,
120
+ }
121
+ const output = resolve(root, resultsDirectory, 'node-package.json')
122
+ mkdirSync(dirname(output), { recursive: true })
123
+ writeFileSync(output, `${JSON.stringify(result, null, 2)}\n`)
124
+ return result
125
+ }
126
+
127
+ /** @param {string} root @param {string} specifier @param {number} samples */
128
+ function measureImports(root, specifier, samples) {
129
+ return Array.from({ length: samples }, () => {
130
+ const source = [
131
+ 'const before = process.memoryUsage().rss',
132
+ 'const started = performance.now()',
133
+ `await import(${JSON.stringify(specifier)})`,
134
+ 'const durationMs = performance.now() - started',
135
+ 'const rssDeltaBytes = Math.max(0, process.memoryUsage().rss - before)',
136
+ 'process.stdout.write(JSON.stringify({ durationMs, rssDeltaBytes }))',
137
+ ].join(';')
138
+ const result = spawnSync(process.execPath, ['--input-type=module', '--eval', source], {
139
+ cwd: root,
140
+ encoding: 'utf8',
141
+ env: process.env,
142
+ })
143
+ if (result.error) throw result.error
144
+ if (result.status !== 0)
145
+ throw new Error(`cold import failed for ${specifier}: ${result.stderr.trim()}`)
146
+ const value = JSON.parse(result.stdout)
147
+ if (
148
+ typeof value.durationMs !== 'number' ||
149
+ !Number.isFinite(value.durationMs) ||
150
+ typeof value.rssDeltaBytes !== 'number' ||
151
+ !Number.isFinite(value.rssDeltaBytes)
152
+ )
153
+ throw new Error(`cold import returned invalid metrics for ${specifier}.`)
154
+ return value
155
+ })
156
+ }
157
+
158
+ /** @param {Record<string, any>} packageJson */
159
+ function packageEntry(packageJson) {
160
+ const rootExport = packageJson.exports?.['.'] ?? packageJson.exports
161
+ if (typeof rootExport === 'string') return rootExport
162
+ if (rootExport && typeof rootExport === 'object') {
163
+ for (const key of ['import', 'default', 'node', 'require']) {
164
+ if (typeof rootExport[key] === 'string') return rootExport[key]
165
+ }
166
+ }
167
+ for (const key of ['module', 'main']) {
168
+ if (typeof packageJson[key] === 'string') return packageJson[key]
169
+ }
170
+ throw new Error('Unable to infer a package import target; set importTarget in the budget file.')
171
+ }
172
+
173
+ /** @param {string} root */
174
+ function npmProductionDependencyCount(root) {
175
+ const result = spawnSync('npm', ['ls', '--omit=dev', '--all', '--parseable', '--silent'], {
176
+ cwd: root,
177
+ encoding: 'utf8',
178
+ env: process.env,
179
+ })
180
+ if (result.error || result.status !== 0) return null
181
+ return Math.max(
182
+ 0,
183
+ result.stdout
184
+ .split(/\r?\n/)
185
+ .map((line) => line.trim())
186
+ .filter(Boolean).length - 1
187
+ )
188
+ }
189
+
190
+ /** @param {string} root @param {string[]} args @param {string} label */
191
+ function npmJson(root, args, label) {
192
+ const result = spawnSync('npm', args, { cwd: root, encoding: 'utf8', env: process.env })
193
+ if (result.error) throw result.error
194
+ if (result.status !== 0) throw new Error(`${label} failed: ${result.stderr.trim()}`)
195
+ try {
196
+ return JSON.parse(result.stdout)
197
+ } catch {
198
+ throw new Error(`${label} did not return valid JSON.`)
199
+ }
200
+ }
201
+
202
+ /** @param {string} file @param {string} label */
203
+ function readJson(file, label) {
204
+ try {
205
+ return JSON.parse(readFileSync(file, 'utf8'))
206
+ } catch (error) {
207
+ throw new Error(
208
+ `${label} must contain valid JSON: ${error instanceof Error ? error.message : error}`,
209
+ { cause: error }
210
+ )
211
+ }
212
+ }
213
+
214
+ /** @param {Record<string, any>} value @param {string} field */
215
+ function numberField(value, field) {
216
+ if (typeof value[field] !== 'number' || !Number.isFinite(value[field]))
217
+ throw new Error(`npm pack result is missing numeric ${field}.`)
218
+ return value[field]
219
+ }
220
+
221
+ /** @param {number} value */
222
+ function round(value) {
223
+ return Math.round(value * 1000) / 1000
224
+ }
package/src/runtime.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
 
4
- import { appendFileSync, existsSync, readFileSync } from 'node:fs'
4
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
5
5
  import { resolve } from 'node:path'
6
6
  import { spawnSync } from 'node:child_process'
7
7
  import { detectPackageManager, resolveProfile } from './lib/profile.mjs'
@@ -9,6 +9,7 @@ import { configured, readConfig } from './lib/config.mjs'
9
9
  import { classifyTestFiles } from './lib/test-discovery.mjs'
10
10
  import { classifyValidationMode, evaluateValidationGate } from './lib/validation-policy.mjs'
11
11
  import { readReleaseConfig, validateGeneratedReleaseDiff } from './lib/release-policy.mjs'
12
+ import { runNodePackagePerformance } from './lib/node-package-performance.mjs'
12
13
 
13
14
  const root = process.cwd()
14
15
  const config = readConfig(resolve(root, '.github/code-foundry.yml'))
@@ -56,23 +57,118 @@ function performanceEnabled() {
56
57
  return configured(config.performance, 'auto') !== 'false'
57
58
  }
58
59
 
59
- function performanceCommand() {
60
+ function performanceCommands() {
60
61
  const raw = configured(config.performance_command, '').trim()
61
- if (!raw) return null
62
+ if (!raw) return []
62
63
  let command
63
64
  try {
64
65
  command = JSON.parse(raw)
65
66
  } catch {
66
- throw new Error('performance_command must be a JSON array of command arguments.')
67
+ throw new Error('performance_command must be a JSON argv array or an array of argv arrays.')
67
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
68
72
  if (
69
- !Array.isArray(command) ||
70
- command.length === 0 ||
71
- command.some((argument) => typeof argument !== 'string' || argument.length === 0)
72
- ) {
73
- throw new Error('performance_command must be a non-empty JSON array of non-empty strings.')
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
74
171
  }
75
- return command
76
172
  }
77
173
 
78
174
  /** @param {string} task */
@@ -299,7 +395,9 @@ function relevant(task) {
299
395
  if (task === 'performance') {
300
396
  if (!performanceEnabled()) return false
301
397
  return Boolean(
302
- (scripted && scripted.some((candidate) => hasScript(candidate))) || performanceCommand()
398
+ (scripted && scripted.some((candidate) => hasScript(candidate))) ||
399
+ performanceCommands().length > 0 ||
400
+ performanceProfile()
303
401
  )
304
402
  }
305
403
  if (scripted && scripted.some((candidate) => hasScript(candidate)))
@@ -465,11 +563,7 @@ function ci(task) {
465
563
  return
466
564
  }
467
565
  if (task === 'performance') {
468
- if (!performanceEnabled()) return
469
- if (runScript(['performance:check', 'perf:check'])) return
470
- const command = performanceCommand()
471
- if (command) run(command[0], command.slice(1))
472
- return
566
+ return runPerformance()
473
567
  }
474
568
  /** @type {Record<string, string[]>} */
475
569
  const scriptsByTask = {
@@ -46,6 +46,7 @@ cache/
46
46
  !.github/actions/cache/action.yml
47
47
  typechain/
48
48
  coverage.json
49
+ performance-results/
49
50
  tests/.bin/
50
51
  **/.app/
51
52
 
@@ -0,0 +1,43 @@
1
+ name: Code Foundry Audit
2
+
3
+ on:
4
+ schedule:
5
+ - cron: '31 6 * * 1'
6
+ workflow_dispatch:
7
+
8
+ permissions: {}
9
+
10
+ concurrency:
11
+ group: code-foundry-audit-${{ github.event_name }}
12
+ cancel-in-progress: true
13
+
14
+ jobs:
15
+ # Default-branch-capable events use an immutable released runtime for both
16
+ # reusable workflow code and runtime code. They never accept a caller-
17
+ # controlled runtime ref and cannot execute pull-request code with access to
18
+ # the protected default-branch cache.
19
+ validation:
20
+ name: Validation
21
+ if: vars.CI_BILLING_PAUSED != 'true'
22
+ permissions:
23
+ actions: read
24
+ contents: read
25
+ packages: read
26
+ security-events: write
27
+ uses: ./.github/workflows/validation.yml
28
+ with:
29
+ mode: audit
30
+ runtime-repository: 0xPlayerOne/code-foundry
31
+ runtime-ref: main
32
+ ci-runner: ubuntu-latest
33
+ test-runner: ubuntu-latest
34
+ unit-runner: ubuntu-slim
35
+ performance-runner: ubuntu-latest
36
+ security-runner: ubuntu-slim
37
+ codeql-runner: ubuntu-latest
38
+ rust-shards: '["all"]'
39
+ rust-threads: '1'
40
+ rust-max-parallel: 1
41
+ secrets:
42
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
43
+ NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}