code-foundry 1.28.9 → 1.28.11
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/workflows/ci.yml +4 -0
- package/.github/workflows/draft-pr_self-ci.yml +1 -1
- package/.github/workflows/eval.yml +1 -0
- package/.github/workflows/opencode-security_self-ci.yml +2 -1
- package/.github/workflows/release-pr_self-ci.yml +1 -1
- package/.github/workflows/release_self-ci.yml +1 -1
- package/.github/workflows/validation_self-ci.yml +1 -1
- package/CHANGELOG.md +14 -0
- package/docs/CONFIGURATION.md +18 -0
- package/package.json +1 -1
- package/src/commands/sync.mjs +46 -2
- package/src/runtime-core.mjs +80 -1
package/.github/workflows/ci.yml
CHANGED
|
@@ -53,6 +53,7 @@ jobs:
|
|
|
53
53
|
name: Format
|
|
54
54
|
if: vars.CI_BILLING_PAUSED != 'true'
|
|
55
55
|
runs-on: ${{ inputs.runner }}
|
|
56
|
+
timeout-minutes: 30
|
|
56
57
|
steps:
|
|
57
58
|
- name: Checkout
|
|
58
59
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
@@ -112,6 +113,7 @@ jobs:
|
|
|
112
113
|
name: Lint
|
|
113
114
|
if: vars.CI_BILLING_PAUSED != 'true'
|
|
114
115
|
runs-on: ${{ inputs.runner }}
|
|
116
|
+
timeout-minutes: 30
|
|
115
117
|
steps:
|
|
116
118
|
- name: Checkout
|
|
117
119
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
@@ -166,6 +168,7 @@ jobs:
|
|
|
166
168
|
name: Type-Check
|
|
167
169
|
if: vars.CI_BILLING_PAUSED != 'true'
|
|
168
170
|
runs-on: ${{ inputs.runner }}
|
|
171
|
+
timeout-minutes: 30
|
|
169
172
|
steps:
|
|
170
173
|
- name: Checkout
|
|
171
174
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
@@ -220,6 +223,7 @@ jobs:
|
|
|
220
223
|
name: Build
|
|
221
224
|
if: vars.CI_BILLING_PAUSED != 'true'
|
|
222
225
|
runs-on: ${{ inputs.runner }}
|
|
226
|
+
timeout-minutes: 30
|
|
223
227
|
env:
|
|
224
228
|
# Framework builds may validate server-side configuration at build time.
|
|
225
229
|
# Consumers can provide the real value; the fallback is intentionally
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
name: Code Foundry
|
|
1
|
+
name: Code Foundry Security
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
pull_request:
|
|
@@ -16,6 +16,7 @@ jobs:
|
|
|
16
16
|
name: OpenCode Security / Detect
|
|
17
17
|
if: vars.CI_BILLING_PAUSED != 'true' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false))
|
|
18
18
|
runs-on: ubuntu-slim
|
|
19
|
+
timeout-minutes: 10
|
|
19
20
|
outputs:
|
|
20
21
|
enabled: ${{ steps.detect.outputs.enabled }}
|
|
21
22
|
token: ${{ steps.detect.outputs.token }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.28.11](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.10...v1.28.11) (2026-09-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **fleet:** bound rendered jobs with timeouts and refresh stale runtime pins ([#616](https://github.com/0xPlayerOne/code-foundry/issues/616)) ([f8d20b4](https://github.com/0xPlayerOne/code-foundry/commit/f8d20b4ad48cddd20630ff9b7c1c21d0c5ac9520))
|
|
9
|
+
|
|
10
|
+
## [1.28.10](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.9...v1.28.10) (2026-09-11)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **ci:** dedupe caller names and keep CodeQL Rust shards aligned ([#613](https://github.com/0xPlayerOne/code-foundry/issues/613)) ([8000e5e](https://github.com/0xPlayerOne/code-foundry/commit/8000e5e152ccc7e657933b55eb5b82a278762176))
|
|
16
|
+
|
|
3
17
|
## [1.28.9](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.8...v1.28.9) (2026-09-11)
|
|
4
18
|
|
|
5
19
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -215,6 +215,24 @@ Each shard must contain tracked Rust source. Absolute paths, parent traversal,
|
|
|
215
215
|
duplicates, empty scopes, and more than eight shards are rejected. Use
|
|
216
216
|
`["all"]` when complete, non-overlapping source scopes are not available.
|
|
217
217
|
|
|
218
|
+
Every lane rendered from this configuration (pull-request validation, the
|
|
219
|
+
default-branch scan, the scheduled audit, and merge queues) receives the same
|
|
220
|
+
shard list, and the CodeQL Detect job fails closed when caller workflows drift
|
|
221
|
+
apart. Keep them aligned: each shard uploads a SARIF category keyed by a hash
|
|
222
|
+
of the shard scope, GitHub code scanning baselines every category it has seen
|
|
223
|
+
on the default branch, and the code-scanning merge gate waits for results in
|
|
224
|
+
every tracked category. Two practical consequences:
|
|
225
|
+
|
|
226
|
+
- Treat `codeql_rust_shards` as add-only while the gate tracks categories.
|
|
227
|
+
Removing a shard orphans its category on the default branch, and pull
|
|
228
|
+
requests then stall on "Code scanning is still expecting N results" until
|
|
229
|
+
the category is covered again.
|
|
230
|
+
- To retire a category for real, delete its code-scanning analyses (Code
|
|
231
|
+
security → Code scanning → analyses, or the code-scanning analysis
|
|
232
|
+
deletion API) after removing the shard, then re-run `sync`. Covering retired
|
|
233
|
+
categories in the shard list keeps the gate green but re-runs their analysis
|
|
234
|
+
on every pull request and is not a substitute for cleanup.
|
|
235
|
+
|
|
218
236
|
## Cloudflare Workers
|
|
219
237
|
|
|
220
238
|
Repositories that deploy to Cloudflare Workers can use the opt-in verified
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.28.
|
|
3
|
+
"version": "1.28.11",
|
|
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": {
|
package/src/commands/sync.mjs
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs'
|
|
4
12
|
import { dirname, join, resolve } from 'node:path'
|
|
5
13
|
import { spawnSync } from 'node:child_process'
|
|
6
14
|
import {
|
|
@@ -321,6 +329,37 @@ function synchronize(options) {
|
|
|
321
329
|
}
|
|
322
330
|
}
|
|
323
331
|
|
|
332
|
+
// Preserved (repository-owned) workflows may reference the runtime directly,
|
|
333
|
+
// for example a hand-maintained Cloudflare preview caller. Advance their
|
|
334
|
+
// plain semver pins to the synced runtime ref so a stale hand-added pin
|
|
335
|
+
// cannot keep an old runtime alive after a fleet upgrade. Intentional refs
|
|
336
|
+
// (branch names, immutable SHAs) and other repositories' actions are left
|
|
337
|
+
// untouched, mirroring the config pin policy above.
|
|
338
|
+
if (!selfRepository && /^v\d+\.\d+\.\d+$/.test(runtimeRef)) {
|
|
339
|
+
const standard = new Set(standardFiles)
|
|
340
|
+
const workflowsDir = join(target, '.github/workflows')
|
|
341
|
+
if (existsSync(workflowsDir)) {
|
|
342
|
+
const runtimePin = new RegExp(
|
|
343
|
+
`(uses:\\s*${escapeRegExp(runtimeRepository)}/\\.github/(?:workflows|actions)/[^\\s@]+)@v\\d+\\.\\d+\\.\\d+`,
|
|
344
|
+
'g'
|
|
345
|
+
)
|
|
346
|
+
for (const entry of readdirSync(workflowsDir).toSorted()) {
|
|
347
|
+
if (!entry.endsWith('.yml') || standard.has(`.github/workflows/${entry}`)) continue
|
|
348
|
+
const destination = join(workflowsDir, entry)
|
|
349
|
+
const original = readFileSync(destination, 'utf8')
|
|
350
|
+
const updated = original.replace(runtimePin, `$1@${runtimeRef}`)
|
|
351
|
+
if (updated !== original) {
|
|
352
|
+
changed.push(`.github/workflows/${entry}`)
|
|
353
|
+
if (dryRun)
|
|
354
|
+
console.log(
|
|
355
|
+
`Would refresh the runtime pin in .github/workflows/${entry} to ${runtimeRef}.`
|
|
356
|
+
)
|
|
357
|
+
else writeOrReport(destination, updated, dryRun)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
324
363
|
// A repository that no longer opts into the staging-release topology must
|
|
325
364
|
// not keep a generated staging promotion caller that would otherwise
|
|
326
365
|
// linger dormant (it triggers on pushes to a branch that does not exist).
|
|
@@ -1003,7 +1042,12 @@ function escapeRegExp(value) {
|
|
|
1003
1042
|
*/
|
|
1004
1043
|
export function isGeneratedEventCaller(content, stem, runtimeRepository) {
|
|
1005
1044
|
const text = Buffer.isBuffer(content) ? content.toString('utf8') : String(content)
|
|
1006
|
-
|
|
1045
|
+
// Generated callers carried the shared display name "Code Foundry" before
|
|
1046
|
+
// issue 609 gave each caller a unique name; both renders must stay
|
|
1047
|
+
// recognizable so a topology switch can still prune stale promotion callers.
|
|
1048
|
+
const names = ['Code Foundry', ...(stem === 'release-pr' ? ['Code Foundry Promotion'] : [])]
|
|
1049
|
+
if (!names.some((name) => new RegExp(`^name:\\s*${escapeRegExp(name)}\\s*$`, 'm').test(text)))
|
|
1050
|
+
return false
|
|
1007
1051
|
if (/^\s*(runs-on|steps):/m.test(text)) return false
|
|
1008
1052
|
// Older generated staging-promotion callers predate the explicit runtime
|
|
1009
1053
|
// repository input. They are still safe to identify structurally by their
|
package/src/runtime-core.mjs
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
appendFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
mkdirSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs'
|
|
5
12
|
import { resolve } from 'node:path'
|
|
6
13
|
import { spawnSync } from 'node:child_process'
|
|
7
14
|
import { detectPackageManager, resolveProfile } from './lib/profile.mjs'
|
|
@@ -880,6 +887,75 @@ function isPublicRepository() {
|
|
|
880
887
|
return visibility ? visibility === 'public' : process.env.REPO_FOUNDRY_PRIVATE !== 'true'
|
|
881
888
|
}
|
|
882
889
|
|
|
890
|
+
/** Split a workflow file's `jobs:` entries into [job id, block text] pairs.
|
|
891
|
+
* Caller files are re-parsed here instead of trusting the sync renderer so
|
|
892
|
+
* hand-edited or stale generated callers are still validated at run time.
|
|
893
|
+
* @param {string} text @returns {Array<[string, string]>}
|
|
894
|
+
*/
|
|
895
|
+
function workflowJobBlocks(text) {
|
|
896
|
+
const lines = text.split('\n')
|
|
897
|
+
/** @type {Array<[string, string]>} */
|
|
898
|
+
const blocks = []
|
|
899
|
+
let index = 0
|
|
900
|
+
while (index < lines.length) {
|
|
901
|
+
const match = lines[index].match(/^ ([A-Za-z0-9_-]+):\s*$/)
|
|
902
|
+
if (!match) {
|
|
903
|
+
index += 1
|
|
904
|
+
continue
|
|
905
|
+
}
|
|
906
|
+
const start = index
|
|
907
|
+
index += 1
|
|
908
|
+
while (index < lines.length && !/^ [A-Za-z0-9_-]+:\s*$/.test(lines[index])) index += 1
|
|
909
|
+
blocks.push([match[1], lines.slice(start, index).join('\n')])
|
|
910
|
+
}
|
|
911
|
+
return blocks
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* The Rust SARIF category embeds a hash of each shard scope, and GitHub code
|
|
916
|
+
* scanning baselines every category it has seen on the default branch: when
|
|
917
|
+
* two lanes calling the CodeQL workflow report different shard sets, each
|
|
918
|
+
* lane uploads categories the other never produces and the code-scanning
|
|
919
|
+
* merge gate waits forever (issue 609). Fail closed before any analysis runs.
|
|
920
|
+
*/
|
|
921
|
+
function assertRustShardsAligned() {
|
|
922
|
+
const directory = resolve(root, '.github/workflows')
|
|
923
|
+
if (!existsSync(directory)) return
|
|
924
|
+
/** @type {Map<string, string>} */
|
|
925
|
+
const lanes = new Map()
|
|
926
|
+
for (const file of readdirSync(directory)
|
|
927
|
+
.filter((name) => name.endsWith('.yml'))
|
|
928
|
+
.toSorted()) {
|
|
929
|
+
for (const [job, block] of workflowJobBlocks(readFileSync(resolve(directory, file), 'utf8'))) {
|
|
930
|
+
// Every lane that ultimately runs CodeQL analysis counts: direct
|
|
931
|
+
// codeql.yml callers and callers of the validation orchestrators whose
|
|
932
|
+
// rust-shards input flows through to the analysis matrix.
|
|
933
|
+
if (!/^\s+uses:\s*\S*\/(codeql|validation|validation-no-codeql)\.yml(@\S+)?\s*$/m.test(block))
|
|
934
|
+
continue
|
|
935
|
+
const raw = block.match(/^\s+rust-shards:\s*(.+?)\s*$/m)?.[1] ?? '["all"]'
|
|
936
|
+
// Unresolved caller expressions cannot be evaluated here; the literal
|
|
937
|
+
// shard list of the workflow that passes them is what gets compared.
|
|
938
|
+
if (raw.includes('${{')) continue
|
|
939
|
+
let shards = raw.replace(/^['"]|['"]$/g, '')
|
|
940
|
+
try {
|
|
941
|
+
shards = JSON.stringify(JSON.parse(shards).toSorted())
|
|
942
|
+
} catch {
|
|
943
|
+
// Compare unparsable values verbatim; alignment still fails loudly.
|
|
944
|
+
}
|
|
945
|
+
lanes.set(`${file} (${job})`, shards)
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const distinct = new Set(lanes.values())
|
|
949
|
+
if (distinct.size <= 1) return
|
|
950
|
+
const detail = [...lanes].map(([lane, shards]) => `${lane}: ${shards}`).join(', ')
|
|
951
|
+
throw new Error(
|
|
952
|
+
`Rust CodeQL shard sets drift between lanes calling codeql.yml (${detail}). ` +
|
|
953
|
+
'GitHub code scanning tracks every uploaded category, so every lane must pass the same rust-shards list. ' +
|
|
954
|
+
'Re-render the callers with `npx code-foundry sync`, keep codeql_rust_shards aligned across callers, and retire ' +
|
|
955
|
+
'removed categories by deleting their code-scanning analyses (see docs/CONFIGURATION.md).'
|
|
956
|
+
)
|
|
957
|
+
}
|
|
958
|
+
|
|
883
959
|
function codeql() {
|
|
884
960
|
const enabled =
|
|
885
961
|
featureEnabled('codeql') &&
|
|
@@ -894,6 +970,9 @@ function codeql() {
|
|
|
894
970
|
}
|
|
895
971
|
return
|
|
896
972
|
}
|
|
973
|
+
// Detect lane drift before any analysis uploads a category the other lanes
|
|
974
|
+
// never produce; a stall surfaces only later as a blocked merge gate.
|
|
975
|
+
assertRustShardsAligned()
|
|
897
976
|
const available = []
|
|
898
977
|
if (existsSync(resolve(root, '.github/workflows'))) available.push('actions')
|
|
899
978
|
if (hasLanguage('typescript') || hasLanguage('javascript'))
|