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.
@@ -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
- // Pin every runtime reference in the rendered caller: the orchestrator input
1142
- // in the `with:` block and the mode job's runtime checkout. Self templates
1143
- // use ${{ github.sha }} and are only rewritten when rendered for consumers.
1144
- rendered = rendered.replace(/^(\s+runtime-ref:)\s+.*$/gm, `$1 ${ref}`)
1145
- rendered = rendered.replace(/^(\s+ref:)\s+\$\{\{\s*github\.sha\s*\}\}\s*$/gm, `$1 ${ref}`)
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
- '$1 NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n'
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 |\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 pull request targeting `main` | Full validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\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',
1529
- '| 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 pull request targeting `main` | Full validation: CI, full tests, Security, and CodeQL, ending in `Validation / Gate` |\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',
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
- if (config.npm_publish === 'true' && !credentialChecks.npmTokenPresent)
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
- 'npm_publish is enabled but NPM_TOKEN is not configured; npm trusted publishing must be configured for tokenless publication.'
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 pull requests change version metadata only, and the gate
86
- // validates that diff against the release policy before anything publishes.
87
- // The content tree was already fully audited by the pull requests that
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> }} input
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,
@@ -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.`)