code-foundry 1.48.7 → 1.50.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/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 +155 -107
- 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 +160 -24
- package/.github/workflows/validation-no-codeql.yml +92 -16
- package/.github/workflows/validation.yml +97 -16
- 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 +50 -4
- package/src/lib/github-doctor.mjs +23 -2
- package/src/lib/node-package-performance.mjs +57 -2
- package/src/lib/product-quality.mjs +11 -11
- package/src/lib/validation-policy.mjs +11 -11
- package/src/runtime-core.mjs +24 -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')
|
|
@@ -1094,6 +1104,7 @@ export function renderDevChannel(config) {
|
|
|
1094
1104
|
* @param {boolean} selfRepository
|
|
1095
1105
|
*/
|
|
1096
1106
|
function renderWorkflow(content, config, repository, ref, rustCodeql, file, selfRepository) {
|
|
1107
|
+
const npmPublishAuth = configured(config.npm_publish_auth, 'auto')
|
|
1097
1108
|
const localPrefix = 'uses: ./.github/workflows/'
|
|
1098
1109
|
const remotePrefix = `uses: ${repository}/.github/workflows/`
|
|
1099
1110
|
let rendered = content.replaceAll(localPrefix, remotePrefix)
|
|
@@ -1162,7 +1173,7 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1162
1173
|
if (workflow === 'release' && !selfRepository) {
|
|
1163
1174
|
rendered = rendered.replace(
|
|
1164
1175
|
/^(\s+runtime-ref:)\s+.*$/m,
|
|
1165
|
-
`$1 ${ref}\n git-workflow: ${isStagingRelease(config.git_workflow) ? 'staging-release' : 'direct'}`
|
|
1176
|
+
`$1 ${ref}\n git-workflow: ${isStagingRelease(config.git_workflow) ? 'staging-release' : 'direct'}\n npm-publish-auth: ${npmPublishAuth}`
|
|
1166
1177
|
)
|
|
1167
1178
|
}
|
|
1168
1179
|
// Generated PR callers protect drafts by default. Consumers that intentionally
|
|
@@ -1170,6 +1181,22 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1170
1181
|
if (configured(config.draft_protection, 'true') === 'false') {
|
|
1171
1182
|
rendered = rendered.replaceAll(' && github.event.pull_request.draft == false', '')
|
|
1172
1183
|
}
|
|
1184
|
+
const integrationShards =
|
|
1185
|
+
config.integration_shards === undefined || config.integration_shards === ''
|
|
1186
|
+
? 1
|
|
1187
|
+
: Number(config.integration_shards)
|
|
1188
|
+
if (!Number.isInteger(integrationShards) || integrationShards < 1 || integrationShards > 2) {
|
|
1189
|
+
throw new Error(`Unsupported integration_shards: ${config.integration_shards}; use 1 or 2.`)
|
|
1190
|
+
}
|
|
1191
|
+
if (integrationShards > 1 && configured(config.billing_lanes, 'false') === 'true') {
|
|
1192
|
+
throw new Error('integration_shards cannot be combined with billing_lanes.')
|
|
1193
|
+
}
|
|
1194
|
+
if (integrationShards > 1 && file === '.github/workflows/validation.yml') {
|
|
1195
|
+
rendered = rendered.replace(
|
|
1196
|
+
/^(\s+integration-total-shards:)\s+.*$/gm,
|
|
1197
|
+
`$1 ${integrationShards}`
|
|
1198
|
+
)
|
|
1199
|
+
}
|
|
1173
1200
|
// E2E sharding fans the managed E2E job into one runner per shard; shards
|
|
1174
1201
|
// stay isolated because each job owns a runner-local test database. Fail
|
|
1175
1202
|
// closed on a malformed count rather than silently rendering a single shard.
|
|
@@ -1225,9 +1252,13 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1225
1252
|
)
|
|
1226
1253
|
}
|
|
1227
1254
|
}
|
|
1255
|
+
const npmSecrets =
|
|
1256
|
+
configured(config.npm_publish, 'false') !== 'true'
|
|
1257
|
+
? ''
|
|
1258
|
+
: `${npmPublishAuth === 'oidc' ? '' : ' NPM_TOKEN: ${{ secrets.NPM_TOKEN }}\n'} NPM_READ_TOKEN: \${{ secrets.NPM_READ_TOKEN }}\n`
|
|
1228
1259
|
rendered = rendered.replace(
|
|
1229
1260
|
/(\n secrets:\n CODE_FOUNDRY_TOKEN: \$\{\{ secrets\.CODE_FOUNDRY_TOKEN \}\}\n)/,
|
|
1230
|
-
|
|
1261
|
+
`$1${npmSecrets}`
|
|
1231
1262
|
)
|
|
1232
1263
|
}
|
|
1233
1264
|
// The staging-release topology validates and scans pull requests against
|
|
@@ -1286,6 +1317,19 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
1286
1317
|
if (!value) continue
|
|
1287
1318
|
rendered = rendered.replace(new RegExp(`^(\\s+${input}:)\\s+.*$`, 'm'), `$1 ${value}`)
|
|
1288
1319
|
}
|
|
1320
|
+
if (
|
|
1321
|
+
workflow === 'validation' &&
|
|
1322
|
+
configured(config.release_qualification_receipts, 'false') === 'true'
|
|
1323
|
+
) {
|
|
1324
|
+
const qualified = rendered.replace(
|
|
1325
|
+
/^(\s+mode:)\s+(\$\{\{\s*needs\.mode\.outputs\.mode\s*\}\})\s*$/m,
|
|
1326
|
+
'$1 $2\n release-qualification-receipts: true'
|
|
1327
|
+
)
|
|
1328
|
+
if (qualified === rendered) {
|
|
1329
|
+
throw new Error('Validation caller cannot render release-qualification-receipts opt-in.')
|
|
1330
|
+
}
|
|
1331
|
+
rendered = qualified
|
|
1332
|
+
}
|
|
1289
1333
|
// Every Rust CodeQL lane in the caller must carry the configured shard
|
|
1290
1334
|
// list. A SARIF category is derived from the shard string, so a caller that
|
|
1291
1335
|
// renders the configuration into the pull-request lane while a second lane
|
|
@@ -1525,8 +1569,8 @@ const DIRECT_DOC_REPLACEMENTS = {
|
|
|
1525
1569
|
'7. Push to the fork and open a pull request targeting `main`.',
|
|
1526
1570
|
],
|
|
1527
1571
|
[
|
|
1528
|
-
'| Event | Expected automation
|
|
1529
|
-
'| Event
|
|
1572
|
+
'| 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',
|
|
1573
|
+
'| 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
1574
|
],
|
|
1531
1575
|
[
|
|
1532
1576
|
'| 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 +2193,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
|
|
|
2149
2193
|
languages,
|
|
2150
2194
|
features: 'all',
|
|
2151
2195
|
codeql: 'auto',
|
|
2196
|
+
release_qualification_receipts: 'false',
|
|
2152
2197
|
dependency_review: 'auto',
|
|
2153
2198
|
package_manager: packageManager,
|
|
2154
2199
|
codeql_rust_shards: '["all"]',
|
|
@@ -2176,6 +2221,7 @@ function createDefaultConfig(root, source, configuredWorkflow) {
|
|
|
2176
2221
|
monorepo_tool: 'auto',
|
|
2177
2222
|
release_type: detectPackageManager(root) === 'none' ? 'auto' : 'node',
|
|
2178
2223
|
npm_publish: 'false',
|
|
2224
|
+
npm_publish_auth: 'auto',
|
|
2179
2225
|
post_release: 'false',
|
|
2180
2226
|
post_release_workflow: '',
|
|
2181
2227
|
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)
|
|
@@ -17,6 +17,62 @@ const METRICS = new Set([
|
|
|
17
17
|
'productionDependencyCount',
|
|
18
18
|
])
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* npm 12 keys pack reports by package name; older npm versions return an array.
|
|
22
|
+
* Count reports before validating their shape so an extra malformed entry cannot be ignored.
|
|
23
|
+
* @param {unknown} value
|
|
24
|
+
* @returns {{filename: string, size: number, unpackedSize: number, files: Array<{path: string}>, name?: string, version?: string}}
|
|
25
|
+
*/
|
|
26
|
+
export function singleNpmPackReport(value) {
|
|
27
|
+
let reports
|
|
28
|
+
if (Array.isArray(value)) reports = value
|
|
29
|
+
else if (value && typeof value === 'object') {
|
|
30
|
+
const object = /** @type {Record<string, unknown>} */ (value)
|
|
31
|
+
reports = typeof object.filename === 'string' ? [object] : Object.values(object)
|
|
32
|
+
} else reports = []
|
|
33
|
+
if (reports.length !== 1) throw new Error('Expected one npm pack report.')
|
|
34
|
+
const packed = reports[0]
|
|
35
|
+
if (!packed || typeof packed !== 'object' || Array.isArray(packed))
|
|
36
|
+
throw new Error('npm pack returned no package result.')
|
|
37
|
+
if (typeof packed.filename !== 'string' || packed.filename.length === 0)
|
|
38
|
+
throw new Error('npm pack result is missing filename.')
|
|
39
|
+
for (const field of ['size', 'unpackedSize'])
|
|
40
|
+
if (typeof packed[field] !== 'number' || !Number.isFinite(packed[field]) || packed[field] < 0)
|
|
41
|
+
throw new Error(`npm pack result is missing non-negative numeric ${field}.`)
|
|
42
|
+
if (
|
|
43
|
+
!Array.isArray(packed.files) ||
|
|
44
|
+
!packed.files.every(
|
|
45
|
+
(/** @type {unknown} */ file) =>
|
|
46
|
+
file !== null &&
|
|
47
|
+
typeof file === 'object' &&
|
|
48
|
+
!Array.isArray(file) &&
|
|
49
|
+
typeof (/** @type {{path?: unknown}} */ (file).path) === 'string'
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
throw new Error('npm pack result is missing a valid files list.')
|
|
53
|
+
return packed
|
|
54
|
+
}
|
|
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
|
+
|
|
20
76
|
/** @param {number[]} values @param {number} percent */
|
|
21
77
|
export function percentile(values, percent) {
|
|
22
78
|
if (values.length === 0) throw new Error('percentile requires at least one value')
|
|
@@ -59,8 +115,7 @@ export function runNodePackagePerformance(
|
|
|
59
115
|
? measureImports(root, String(policy.controlImport), samples)
|
|
60
116
|
: null
|
|
61
117
|
const pack = npmJson(root, ['pack', '--dry-run', '--json', '--ignore-scripts'], 'npm pack')
|
|
62
|
-
const packed =
|
|
63
|
-
if (!packed || typeof packed !== 'object') throw new Error('npm pack returned no package result.')
|
|
118
|
+
const packed = singleNpmPackReport(pack)
|
|
64
119
|
/** @type {Array<{path?: string}>} */
|
|
65
120
|
const files = Array.isArray(packed.files) ? packed.files : []
|
|
66
121
|
const productionDependencyCount = npmProductionDependencyCount(root)
|
|
@@ -17,6 +17,7 @@ import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
|
17
17
|
import { pathToFileURL } from 'node:url'
|
|
18
18
|
import { tmpdir } from 'node:os'
|
|
19
19
|
import { gzipSync } from 'node:zlib'
|
|
20
|
+
import { singleNpmPackReport } from './node-package-performance.mjs'
|
|
20
21
|
|
|
21
22
|
/** @typedef {Record<string, any>} Profile */
|
|
22
23
|
/** @param {unknown} condition @param {string} message @returns {asserts condition} */
|
|
@@ -602,24 +603,23 @@ export function checkPackage(root, profile, output) {
|
|
|
602
603
|
// Pack outside the consumer repository so test installations cannot become package input.
|
|
603
604
|
const temporary = mkdtempSync(join(tmpdir(), 'foundry-package-quality-'))
|
|
604
605
|
try {
|
|
605
|
-
const pack =
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
606
|
+
const pack = singleNpmPackReport(
|
|
607
|
+
JSON.parse(
|
|
608
|
+
runQualityCommand(
|
|
609
|
+
['npm', 'pack', '--ignore-scripts', '--json', '--pack-destination', temporary],
|
|
610
|
+
root,
|
|
611
|
+
output
|
|
612
|
+
)
|
|
610
613
|
)
|
|
611
614
|
)
|
|
615
|
+
requireValue(/^[A-Za-z0-9._-]+\.tgz$/.test(pack.filename), 'Expected one package archive')
|
|
612
616
|
requireValue(
|
|
613
|
-
|
|
614
|
-
'Expected one package archive'
|
|
615
|
-
)
|
|
616
|
-
requireValue(
|
|
617
|
-
pack[0].files.every(
|
|
617
|
+
pack.files.every(
|
|
618
618
|
(/** @type {{path:string}} */ file) => !/^\.code-foundry(?:\/|$)/.test(file.path)
|
|
619
619
|
),
|
|
620
620
|
'Exclude .code-foundry quality evidence from the published package'
|
|
621
621
|
)
|
|
622
|
-
const archive = ownedPath(temporary, pack
|
|
622
|
+
const archive = ownedPath(temporary, pack.filename)
|
|
623
623
|
const archiveSha256 = createHash('sha256').update(readFileSync(archive)).digest('hex')
|
|
624
624
|
const consumer = join(temporary, 'consumer')
|
|
625
625
|
mkdirSync(consumer)
|
|
@@ -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.`)
|
|
@@ -404,6 +406,28 @@ function validation(task) {
|
|
|
404
406
|
{ cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }
|
|
405
407
|
)
|
|
406
408
|
: undefined
|
|
409
|
+
if (diff && diff.status !== 0) {
|
|
410
|
+
// Git can relay arbitrary remote messages, URLs and credentials on stderr. Emit only
|
|
411
|
+
// bounded, recognised failure facts; never replay remote text as workflow annotations.
|
|
412
|
+
const stderr = (diff.stderr ?? '').slice(0, 8192)
|
|
413
|
+
const missingBlob = /could not fetch ([0-9a-f]{40,64}) from promisor remote/i.exec(stderr)
|
|
414
|
+
const reason = missingBlob
|
|
415
|
+
? `historical blob ${missingBlob[1]} unavailable from promisor remote`
|
|
416
|
+
: /authentication failed|could not read (?:username|password)|terminal prompts disabled/i.test(
|
|
417
|
+
stderr
|
|
418
|
+
)
|
|
419
|
+
? 'authentication unavailable'
|
|
420
|
+
: /could not read from remote repository/i.test(stderr)
|
|
421
|
+
? 'remote repository unavailable'
|
|
422
|
+
: /bad object|unknown revision|invalid symmetric difference|ambiguous argument/i.test(
|
|
423
|
+
stderr
|
|
424
|
+
)
|
|
425
|
+
? 'revision unavailable'
|
|
426
|
+
: 'Git error details omitted'
|
|
427
|
+
console.error(
|
|
428
|
+
`::error::Git content diff failed (status ${diff.status ?? 'unavailable'}): ${reason}.`
|
|
429
|
+
)
|
|
430
|
+
}
|
|
407
431
|
const result = validateGeneratedReleaseDiff({
|
|
408
432
|
headRef: process.env.FOUNDRY_HEAD_REF ?? '',
|
|
409
433
|
headRepo: process.env.FOUNDRY_HEAD_REPO ?? '',
|