code-foundry 1.48.8 → 1.50.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.
- package/.github/CONTRIBUTING.md +8 -8
- package/.github/code-foundry.schema.json +10 -0
- package/.github/code-foundry.yml +2 -0
- package/.github/workflows/ci.yml +41 -5
- package/.github/workflows/draft-enforcement_self-ci.yml +12 -8
- package/.github/workflows/qualification-receipt.yml +201 -0
- package/.github/workflows/qualified-foundry-publish.yml +0 -12
- package/.github/workflows/release.yml +58 -20
- package/.github/workflows/release_self-ci.yml +0 -2
- package/.github/workflows/security.yml +3 -1
- package/.github/workflows/test.yml +90 -1
- package/.github/workflows/validation-billing.yml +159 -23
- package/.github/workflows/validation-no-codeql.yml +91 -15
- package/.github/workflows/validation.yml +96 -15
- package/.github/workflows/validation_self-ci.yml +7 -4
- package/CHANGELOG.md +50 -50
- package/docs/CONFIGURATION.md +11 -0
- package/docs/PUBLISHING.md +13 -5
- package/docs/WORKFLOWS.md +25 -12
- package/docs/qualified-publication.md +22 -14
- package/package.json +4 -1
- package/src/commands/qualified-publication.mjs +27 -0
- package/src/commands/sync.mjs +76 -9
- package/src/lib/github-doctor.mjs +23 -2
- package/src/lib/node-package-performance.mjs +21 -1
- package/src/lib/validation-policy.mjs +11 -11
- package/src/runtime-core.mjs +2 -0
package/src/commands/sync.mjs
CHANGED
|
@@ -164,6 +164,16 @@ function synchronize(options) {
|
|
|
164
164
|
if (!['true', 'false'].includes(draftProtection)) {
|
|
165
165
|
throw new Error(`Unsupported draft_protection: ${draftProtection}; use true or false.`)
|
|
166
166
|
}
|
|
167
|
+
const qualificationReceipts = configured(config.release_qualification_receipts, 'false')
|
|
168
|
+
if (!['true', 'false'].includes(qualificationReceipts)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Unsupported release_qualification_receipts: ${qualificationReceipts}; use true or false.`
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
const npmPublishAuth = configured(config.npm_publish_auth, 'auto')
|
|
174
|
+
if (!['auto', 'oidc', 'token'].includes(npmPublishAuth)) {
|
|
175
|
+
throw new Error(`Unsupported npm_publish_auth: ${npmPublishAuth}; use auto, oidc, or token.`)
|
|
176
|
+
}
|
|
167
177
|
// Validate the dependency-update bot policy before any sync writes so a
|
|
168
178
|
// typo fails fast instead of leaving a half-switched updater behind.
|
|
169
179
|
const dependencyUpdater = configured(config.dependency_updater, 'dependabot')
|
|
@@ -1084,6 +1094,26 @@ export function renderDevChannel(config) {
|
|
|
1084
1094
|
)
|
|
1085
1095
|
}
|
|
1086
1096
|
|
|
1097
|
+
/**
|
|
1098
|
+
* Pin consumer runtime refs without retaining the self-CI expression used by
|
|
1099
|
+
* the Code Foundry repository. Self callers keep their event-relative refs.
|
|
1100
|
+
*
|
|
1101
|
+
* @param {string} content
|
|
1102
|
+
* @param {string} ref
|
|
1103
|
+
* @param {boolean} selfRepository
|
|
1104
|
+
*/
|
|
1105
|
+
export function pinRuntimeRefs(content, ref, selfRepository) {
|
|
1106
|
+
if (selfRepository) return content
|
|
1107
|
+
const foldedRuntimeRef =
|
|
1108
|
+
/^([ \t]*runtime-ref:)[ \t]*>-[ \t]*\r?\n(?:[ \t]+\$\{\{[^\r\n]*\}\}[ \t]*(?:\r?\n|$))+/gm
|
|
1109
|
+
return content
|
|
1110
|
+
.replace(
|
|
1111
|
+
foldedRuntimeRef,
|
|
1112
|
+
(match, key) => `${key} ${ref}${match.endsWith('\r\n') ? '\r\n' : '\n'}`
|
|
1113
|
+
)
|
|
1114
|
+
.replace(/^([ \t]*runtime-ref:)[ \t]+.*$/gm, (_, key) => `${key} ${ref}`)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1087
1117
|
/**
|
|
1088
1118
|
* @param {string} content
|
|
1089
1119
|
* @param {Record<string,string>} config
|
|
@@ -1094,6 +1124,7 @@ export function renderDevChannel(config) {
|
|
|
1094
1124
|
* @param {boolean} selfRepository
|
|
1095
1125
|
*/
|
|
1096
1126
|
function renderWorkflow(content, config, repository, ref, rustCodeql, file, selfRepository) {
|
|
1127
|
+
const npmPublishAuth = configured(config.npm_publish_auth, 'auto')
|
|
1097
1128
|
const localPrefix = 'uses: ./.github/workflows/'
|
|
1098
1129
|
const remotePrefix = `uses: ${repository}/.github/workflows/`
|
|
1099
1130
|
let rendered = content.replaceAll(localPrefix, remotePrefix)
|
|
@@ -1138,11 +1169,12 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1138
1169
|
new RegExp(`${escapeRegExp(remotePrefix)}([^\\s@]+)`, 'g'),
|
|
1139
1170
|
`$&@${ref}`
|
|
1140
1171
|
)
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1172
|
+
// Replace complete folded self-CI runtime expressions when rendering a
|
|
1173
|
+
// consumer. The source repository keeps its event-relative runtime refs.
|
|
1174
|
+
rendered = pinRuntimeRefs(rendered, ref, selfRepository)
|
|
1175
|
+
if (!selfRepository) {
|
|
1176
|
+
rendered = rendered.replace(/^(\s+ref:)\s+\$\{\{\s*github\.sha\s*\}\}\s*$/gm, `$1 ${ref}`)
|
|
1177
|
+
}
|
|
1146
1178
|
rendered = rendered.replace(/^(\s+runtime-repository:)\s+.*$/gm, `$1 ${repository}`)
|
|
1147
1179
|
rendered = rendered.replace(
|
|
1148
1180
|
new RegExp(`^(\\s+repository:)\\s+0xPlayerOne\\/code-foundry\\s*$`, 'gm'),
|
|
@@ -1162,7 +1194,7 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1162
1194
|
if (workflow === 'release' && !selfRepository) {
|
|
1163
1195
|
rendered = rendered.replace(
|
|
1164
1196
|
/^(\s+runtime-ref:)\s+.*$/m,
|
|
1165
|
-
`$1 ${ref}\n git-workflow: ${isStagingRelease(config.git_workflow) ? 'staging-release' : 'direct'}`
|
|
1197
|
+
`$1 ${ref}\n git-workflow: ${isStagingRelease(config.git_workflow) ? 'staging-release' : 'direct'}\n npm-publish-auth: ${npmPublishAuth}`
|
|
1166
1198
|
)
|
|
1167
1199
|
}
|
|
1168
1200
|
// Generated PR callers protect drafts by default. Consumers that intentionally
|
|
@@ -1170,6 +1202,22 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1170
1202
|
if (configured(config.draft_protection, 'true') === 'false') {
|
|
1171
1203
|
rendered = rendered.replaceAll(' && github.event.pull_request.draft == false', '')
|
|
1172
1204
|
}
|
|
1205
|
+
const integrationShards =
|
|
1206
|
+
config.integration_shards === undefined || config.integration_shards === ''
|
|
1207
|
+
? 1
|
|
1208
|
+
: Number(config.integration_shards)
|
|
1209
|
+
if (!Number.isInteger(integrationShards) || integrationShards < 1 || integrationShards > 2) {
|
|
1210
|
+
throw new Error(`Unsupported integration_shards: ${config.integration_shards}; use 1 or 2.`)
|
|
1211
|
+
}
|
|
1212
|
+
if (integrationShards > 1 && configured(config.billing_lanes, 'false') === 'true') {
|
|
1213
|
+
throw new Error('integration_shards cannot be combined with billing_lanes.')
|
|
1214
|
+
}
|
|
1215
|
+
if (integrationShards > 1 && file === '.github/workflows/validation.yml') {
|
|
1216
|
+
rendered = rendered.replace(
|
|
1217
|
+
/^(\s+integration-total-shards:)\s+.*$/gm,
|
|
1218
|
+
`$1 ${integrationShards}`
|
|
1219
|
+
)
|
|
1220
|
+
}
|
|
1173
1221
|
// E2E sharding fans the managed E2E job into one runner per shard; shards
|
|
1174
1222
|
// stay isolated because each job owns a runner-local test database. Fail
|
|
1175
1223
|
// closed on a malformed count rather than silently rendering a single shard.
|
|
@@ -1225,9 +1273,13 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1225
1273
|
)
|
|
1226
1274
|
}
|
|
1227
1275
|
}
|
|
1276
|
+
const npmSecrets =
|
|
1277
|
+
configured(config.npm_publish, 'false') !== 'true'
|
|
1278
|
+
? ''
|
|
1279
|
+
: `${npmPublishAuth === 'oidc' ? '' : ' NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n'} NPM_READ_TOKEN: \${{ secrets.NPM_READ_TOKEN }}\n`
|
|
1228
1280
|
rendered = rendered.replace(
|
|
1229
1281
|
/(\n secrets:\n CODE_FOUNDRY_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN \}\}\n)/,
|
|
1230
|
-
|
|
1282
|
+
`$1${npmSecrets}`
|
|
1231
1283
|
)
|
|
1232
1284
|
}
|
|
1233
1285
|
// The staging-release topology validates and scans pull requests against
|
|
@@ -1286,6 +1338,19 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1286
1338
|
if (!value) continue
|
|
1287
1339
|
rendered = rendered.replace(new RegExp(`^(\\s+${input}:)\\s+.*$`, 'm'), `$1 ${value}`)
|
|
1288
1340
|
}
|
|
1341
|
+
if (
|
|
1342
|
+
workflow === 'validation' &&
|
|
1343
|
+
configured(config.release_qualification_receipts, 'false') === 'true'
|
|
1344
|
+
) {
|
|
1345
|
+
const qualified = rendered.replace(
|
|
1346
|
+
/^(\s+mode:)\s+(\$\{\{\s*needs\.mode\.outputs\.mode\s*\}\})\s*$/m,
|
|
1347
|
+
'$1 $2\n release-qualification-receipts: true'
|
|
1348
|
+
)
|
|
1349
|
+
if (qualified === rendered) {
|
|
1350
|
+
throw new Error('Validation caller cannot render release-qualification-receipts opt-in.')
|
|
1351
|
+
}
|
|
1352
|
+
rendered = qualified
|
|
1353
|
+
}
|
|
1289
1354
|
// Every Rust CodeQL lane in the caller must carry the configured shard
|
|
1290
1355
|
// list. A SARIF category is derived from the shard string, so a caller that
|
|
1291
1356
|
// renders the configuration into the pull-request lane while a second lane
|
|
@@ -1525,8 +1590,8 @@ const DIRECT_DOC_REPLACEMENTS = {
|
|
|
1525
1590
|
'7. Push to the fork and open a pull request targeting `main`.',
|
|
1526
1591
|
],
|
|
1527
1592
|
[
|
|
1528
|
-
'| Event | Expected automation
|
|
1529
|
-
'| Event
|
|
1593
|
+
'| Event | Expected automation |\n| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| Draft pull request targeting `staging` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `staging` | Fast validation: CI plus unit tests, ending in `Validation / Gate` |\n| Draft ordinary pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready ordinary pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please PR targeting `main` | Release diff, configured CodeQL, build/package check, and either a verified receipt or full CI/tests |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `staging` | Promotion PR workflow; canonical validation waits for the PR event |\n| Push to `main` | Release workflow; CodeQL skips release-only merges (audit covers drift) |\n',
|
|
1594
|
+
'| Event | Expected automation |\n| ---------------------------------------- | ---------------------------------------------------------------------------------------------------- |\n| Draft pull request targeting `main` | No runner-heavy validation; run local checks before requesting review |\n| Ready pull request targeting `main` | Audit validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\n| Exact Release Please PR targeting `main` | Release diff, configured CodeQL, build/package check, and either a verified receipt or full CI/tests |\n| Scheduled or manual validation | Full audit tier |\n| Push to a working branch | Draft PR workflow |\n| Push to `main` | Release workflow; CodeQL skips release-only merges (audit covers drift) |\n',
|
|
1530
1595
|
],
|
|
1531
1596
|
[
|
|
1532
1597
|
'| Change | Target | Merge method | Merge gate |\n| ---------------------------- | --------- | ----------------------------------------------- | --------------------------------------------------------- |\n| Working branch | `staging` | Squash | All applicable required checks pass |\n| `staging` → `main` promotion | `main` | Rebase (`merge_strategy`) | Current staging checks, release review, and rollout notes |\n| Release Please version PR | `main` | Rebase (`release_merge_strategy`, fails closed) | Validation gate and release policy pass |\n',
|
|
@@ -2149,6 +2214,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
|
|
|
2149
2214
|
languages,
|
|
2150
2215
|
features: 'all',
|
|
2151
2216
|
codeql: 'auto',
|
|
2217
|
+
release_qualification_receipts: 'false',
|
|
2152
2218
|
dependency_review: 'auto',
|
|
2153
2219
|
package_manager: packageManager,
|
|
2154
2220
|
codeql_rust_shards: '["all"]',
|
|
@@ -2176,6 +2242,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
|
|
|
2176
2242
|
monorepo_tool: 'auto',
|
|
2177
2243
|
release_type: detectPackageManager(root) === 'none' ? 'auto' : 'node',
|
|
2178
2244
|
npm_publish: 'false',
|
|
2245
|
+
npm_publish_auth: 'auto',
|
|
2179
2246
|
post_release: 'false',
|
|
2180
2247
|
post_release_workflow: '',
|
|
2181
2248
|
post_release_mode: 'auto',
|
|
@@ -144,6 +144,7 @@ export function doctorGithub(root) {
|
|
|
144
144
|
: undefined
|
|
145
145
|
const credentialChecks = {
|
|
146
146
|
npmTokenPresent: secretNames.includes('NPM_TOKEN'),
|
|
147
|
+
npmReadTokenPresent: secretNames.includes('NPM_READ_TOKEN'),
|
|
147
148
|
turboTokenPresent: secretNames.includes('TURBO_TOKEN'),
|
|
148
149
|
turboTeamPresent: Boolean(
|
|
149
150
|
repositoryVariables.some(
|
|
@@ -153,10 +154,30 @@ export function doctorGithub(root) {
|
|
|
153
154
|
opencodeApiKeyPresent: secretNames.includes('OPENCODE_API_KEY'),
|
|
154
155
|
}
|
|
155
156
|
details.credentials = credentialChecks
|
|
156
|
-
|
|
157
|
+
const npmPublishAuth = config.npm_publish_auth ?? 'auto'
|
|
158
|
+
details.credentials.npmPublishAuth = npmPublishAuth
|
|
159
|
+
if (!['auto', 'oidc', 'token'].includes(npmPublishAuth)) {
|
|
160
|
+
errors.push('npm_publish_auth must be auto, oidc, or token.')
|
|
161
|
+
} else if (config.npm_publish === 'true' && npmPublishAuth === 'token') {
|
|
162
|
+
if (!credentialChecks.npmTokenPresent)
|
|
163
|
+
errors.push('npm_publish_auth is token but NPM_TOKEN is not configured.')
|
|
164
|
+
} else if (
|
|
165
|
+
config.npm_publish === 'true' &&
|
|
166
|
+
npmPublishAuth === 'auto' &&
|
|
167
|
+
!credentialChecks.npmTokenPresent
|
|
168
|
+
) {
|
|
157
169
|
warnings.push(
|
|
158
|
-
'
|
|
170
|
+
'npm_publish_auth is auto and NPM_TOKEN is absent; publication uses OIDC, so npm trust must match this repository and the generated release.yml caller.'
|
|
159
171
|
)
|
|
172
|
+
} else if (
|
|
173
|
+
config.npm_publish === 'true' &&
|
|
174
|
+
npmPublishAuth === 'oidc' &&
|
|
175
|
+
credentialChecks.npmTokenPresent
|
|
176
|
+
) {
|
|
177
|
+
warnings.push(
|
|
178
|
+
'npm_publish_auth is oidc; NPM_TOKEN is not used for publishing or install. Use NPM_READ_TOKEN only when private dependencies require read access.'
|
|
179
|
+
)
|
|
180
|
+
}
|
|
160
181
|
if (
|
|
161
182
|
['true', 'auto'].includes(config.turbo_remote ?? 'false') &&
|
|
162
183
|
(!credentialChecks.turboTokenPresent || !credentialChecks.turboTeamPresent)
|
|
@@ -21,7 +21,7 @@ const METRICS = new Set([
|
|
|
21
21
|
* npm 12 keys pack reports by package name; older npm versions return an array.
|
|
22
22
|
* Count reports before validating their shape so an extra malformed entry cannot be ignored.
|
|
23
23
|
* @param {unknown} value
|
|
24
|
-
* @returns {{filename: string, size: number, unpackedSize: number, files: Array<{path: string}
|
|
24
|
+
* @returns {{filename: string, size: number, unpackedSize: number, files: Array<{path: string}>, name?: string, version?: string}}
|
|
25
25
|
*/
|
|
26
26
|
export function singleNpmPackReport(value) {
|
|
27
27
|
let reports
|
|
@@ -53,6 +53,26 @@ export function singleNpmPackReport(value) {
|
|
|
53
53
|
return packed
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Verify the single npm pack report used to inspect a versioned root package.
|
|
58
|
+
* Supports both npm's legacy array and npm 12's package-name keyed object.
|
|
59
|
+
* @param {unknown} value
|
|
60
|
+
* @param {{name?:unknown,version?:unknown}} manifest
|
|
61
|
+
*/
|
|
62
|
+
export function verifyNpmPackPackage(value, manifest) {
|
|
63
|
+
const packed = singleNpmPackReport(value)
|
|
64
|
+
if (
|
|
65
|
+
typeof manifest?.name !== 'string' ||
|
|
66
|
+
typeof manifest.version !== 'string' ||
|
|
67
|
+
packed.name !== manifest.name ||
|
|
68
|
+
packed.version !== manifest.version ||
|
|
69
|
+
!packed.files.some((file) => file.path === 'package.json')
|
|
70
|
+
) {
|
|
71
|
+
throw new Error('npm pack result does not match the root package manifest.')
|
|
72
|
+
}
|
|
73
|
+
return packed
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
/** @param {number[]} values @param {number} percent */
|
|
57
77
|
export function percentile(values, percent) {
|
|
58
78
|
if (values.length === 0) throw new Error('percentile requires at least one value')
|
|
@@ -82,13 +82,9 @@ export function classifyValidationMode(input) {
|
|
|
82
82
|
const REQUIRED_JOBS_BY_MODE = {
|
|
83
83
|
fast: ['ci', 'test'],
|
|
84
84
|
audit: ['ci', 'test', 'security', 'codeql', 'eval'],
|
|
85
|
-
// Release Please
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// merged into main, and the scheduled audit lane re-covers drift. The
|
|
89
|
-
// release tier therefore requires the fast suite plus CodeQL, whose
|
|
90
|
-
// per-pull-request code scanning results some repository rulesets require
|
|
91
|
-
// to keep the release from deadlocking.
|
|
85
|
+
// Release Please uses the full CI and unit-test fallback unless the
|
|
86
|
+
// orchestrator has verified an exact short-lived source receipt. CodeQL
|
|
87
|
+
// remains required because some rulesets require a PR analysis to merge.
|
|
92
88
|
release: ['ci', 'test', 'codeql'],
|
|
93
89
|
}
|
|
94
90
|
|
|
@@ -96,9 +92,13 @@ const REQUIRED_JOBS_BY_MODE = {
|
|
|
96
92
|
* Job ids that the aggregate gate requires for a mode. Expected skips never
|
|
97
93
|
* appear here: anything outside the returned list is not part of the gate.
|
|
98
94
|
* @param {string} mode
|
|
95
|
+
* @param {{sourceQualification?:boolean}} [options]
|
|
99
96
|
* @returns {string[]}
|
|
100
97
|
*/
|
|
101
|
-
export function requiredValidationJobs(mode) {
|
|
98
|
+
export function requiredValidationJobs(mode, options = {}) {
|
|
99
|
+
if (mode === 'release' && options.sourceQualification === true) {
|
|
100
|
+
return ['ci', 'codeql', 'qualification']
|
|
101
|
+
}
|
|
102
102
|
const jobs = REQUIRED_JOBS_BY_MODE[/** @type {'fast'|'audit'|'release'} */ (mode)]
|
|
103
103
|
if (!jobs) {
|
|
104
104
|
throw new Error(
|
|
@@ -114,14 +114,14 @@ export function requiredValidationJobs(mode) {
|
|
|
114
114
|
* non-required jobs never fail the gate. Every required job must report
|
|
115
115
|
* exactly `success`; failure, cancellation, an unexpected skip, a missing
|
|
116
116
|
* result, or an unknown result fails the gate (fail closed).
|
|
117
|
-
* @param {{ mode: string, results?: Record<string, string | null | undefined
|
|
117
|
+
* @param {{ mode: string, results?: Record<string, string | null | undefined>, sourceQualification?:boolean }} input
|
|
118
118
|
* @returns {{ valid: boolean, required: string[], failures: Array<{ job: string, result: string }> }}
|
|
119
119
|
*/
|
|
120
120
|
export function evaluateValidationGate(input) {
|
|
121
|
-
const { mode, results = {} } = input
|
|
121
|
+
const { mode, results = {}, sourceQualification = false } = input
|
|
122
122
|
let required
|
|
123
123
|
try {
|
|
124
|
-
required = requiredValidationJobs(mode)
|
|
124
|
+
required = requiredValidationJobs(mode, { sourceQualification })
|
|
125
125
|
} catch (error) {
|
|
126
126
|
return {
|
|
127
127
|
valid: false,
|
package/src/runtime-core.mjs
CHANGED
|
@@ -380,7 +380,9 @@ function validation(task) {
|
|
|
380
380
|
security: process.env.FOUNDRY_SECURITY,
|
|
381
381
|
codeql: process.env.FOUNDRY_CODEQL,
|
|
382
382
|
eval: process.env.FOUNDRY_EVAL,
|
|
383
|
+
qualification: process.env.FOUNDRY_QUALIFICATION,
|
|
383
384
|
},
|
|
385
|
+
sourceQualification: process.env.FOUNDRY_SOURCE_QUALIFICATION === 'true',
|
|
384
386
|
})
|
|
385
387
|
if (gate.valid) {
|
|
386
388
|
console.log(`Validation gate passed for ${mode} mode.`)
|