code-foundry 1.28.4 → 1.28.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.28.6](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.5...v1.28.6) (2026-09-10)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **sync:** preserve non-CodeQL workflow triggers ([#605](https://github.com/0xPlayerOne/code-foundry/issues/605)) ([e215207](https://github.com/0xPlayerOne/code-foundry/commit/e215207d596361f283072d4f43f8ab58676e13c8))
9
+
10
+ ## [1.28.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.4...v1.28.5) (2026-09-10)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **fleet:** require explicit inventory manifest ([#603](https://github.com/0xPlayerOne/code-foundry/issues/603)) ([1345331](https://github.com/0xPlayerOne/code-foundry/commit/13453318ba4fb21e6df6d978f1d51119148300ba))
16
+
3
17
  ## [1.28.4](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.3...v1.28.4) (2026-09-10)
4
18
 
5
19
 
@@ -1,9 +1,9 @@
1
1
  # Declarative fleet inventory and staged rollouts
2
2
 
3
- Place `code-foundry-fleet.json` in the directory passed to `--root`. Its presence
4
- opts that fleet into inventory-based discovery and controlled upgrades. The
5
- runtime has no organization-specific fleet inventory; legacy discovery is limited
6
- to the explicitly supplied root and its immediate children.
3
+ Place `code-foundry-fleet.json` in the directory passed to `--root`. Fleet
4
+ discovery and controlled upgrades require this explicit inventory. The runtime
5
+ has no organization-specific fleet inventory and never scans arbitrary
6
+ repositories from the filesystem.
7
7
 
8
8
  ```json
9
9
  {
@@ -107,8 +107,8 @@ branches or commits with mismatched tree markers are preserved and blocked for
107
107
  manual review. Local managed refs preserve committed work after a failed push.
108
108
  The marker is an ownership/recovery guard, not a cryptographic signature.
109
109
 
110
- PR creation is draft-first in both manifest and legacy discovery modes. A PR that
111
- a human has already made ready is not silently converted back or modified.
110
+ PR creation is draft-first in manifest mode. A PR that a human has already made
111
+ ready is not silently converted back or modified.
112
112
 
113
113
  An entry may include a reviewed time-limited exception:
114
114
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.28.4",
3
+ "version": "1.28.6",
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": {
@@ -1,9 +1,5 @@
1
1
  // @ts-check
2
2
 
3
- import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
4
- import { join } from 'node:path'
5
- import { tmpdir } from 'node:os'
6
- import { spawnSync } from 'node:child_process'
7
3
  import { readPackageVersion, syncRepository } from './sync.mjs'
8
4
  import {
9
5
  discoverManifestRepositories,
@@ -13,31 +9,19 @@ import {
13
9
 
14
10
  /** @typedef {{ path: string, repository: string, runtimeRef: string, dirty: boolean, configured: boolean, gitWorkflow: string }} FleetRepository */
15
11
 
12
+ /** @param {string} root */
13
+ function requireFleetManifest(root) {
14
+ if (!hasFleetManifest(root)) {
15
+ throw new Error(
16
+ 'Missing code-foundry-fleet.json; fleet discovery requires an explicit manifest.'
17
+ )
18
+ }
19
+ }
20
+
16
21
  /** @param {string} root @returns {FleetRepository[]} */
17
22
  export function discoverRepositories(root) {
18
- if (hasFleetManifest(root)) return discoverManifestRepositories(root)
19
- /** @type {FleetRepository[]} */
20
- const result = []
21
- // Legacy discovery is intentionally limited to the explicitly supplied root
22
- // and its immediate children. Consumer-fleet inventories belong in an
23
- // explicit code-foundry-fleet.json manifest; the runtime must not know about
24
- // any organization's repositories or directory names.
25
- const candidates = [root, ...children(root)]
26
- for (const candidate of new Set(candidates)) {
27
- if (!existsSync(join(candidate, '.git'))) continue
28
- const configured = existsSync(join(candidate, '.github/code-foundry.yml'))
29
- const config = readConfig(join(candidate, '.github/code-foundry.yml'))
30
- const remote = git(candidate, ['remote', 'get-url', 'origin'])
31
- result.push({
32
- path: candidate,
33
- repository: normalizeRemote(remote),
34
- runtimeRef: config.runtime_ref ?? '',
35
- dirty: Boolean(git(candidate, ['status', '--porcelain'])),
36
- configured,
37
- gitWorkflow: config.git_workflow ?? 'direct',
38
- })
39
- }
40
- return result.toSorted((a, b) => a.path.localeCompare(b.path))
23
+ requireFleetManifest(root)
24
+ return discoverManifestRepositories(root)
41
25
  }
42
26
 
43
27
  /** @param {string} root @param {string} source @param {{ createPr?: boolean, dryRun?: boolean, force?: boolean, version: string, exclude?: string[] }} options */
@@ -49,523 +33,6 @@ export function upgradeFleet(root, source, options) {
49
33
  `fleet upgrade target ${options.version} does not match the runtime source checkout (${sourceVersion}); refresh the code-foundry checkout to ${options.version} before upgrading so the rendered callers and the declared runtime agree.`
50
34
  )
51
35
  }
52
- if (hasFleetManifest(root))
53
- return upgradeManifestFleet(root, source, { ...options, version }, syncRepository)
54
- const repositories = discoverRepositories(root)
55
- const report = []
56
- for (const repository of repositories) {
57
- if (
58
- (options.exclude ?? []).some(
59
- (value) =>
60
- repository.path === value ||
61
- repository.repository === value ||
62
- repository.path.endsWith(`/${value}`)
63
- )
64
- ) {
65
- report.push({ path: repository.path, status: 'skipped', reason: 'excluded by fleet policy' })
66
- continue
67
- }
68
- if (!repository.configured) {
69
- report.push({
70
- path: repository.path,
71
- status: 'skipped',
72
- reason: 'missing .github/code-foundry.yml',
73
- })
74
- continue
75
- }
76
- if (repository.dirty && !options.force) {
77
- report.push({ path: repository.path, status: 'skipped', reason: 'working tree is dirty' })
78
- continue
79
- }
80
- if (!options.createPr || options.dryRun) {
81
- const result = syncRepository({
82
- target: repository.path,
83
- source,
84
- dryRun: options.dryRun,
85
- force: false,
86
- })
87
- report.push({
88
- path: repository.path,
89
- status: options.dryRun ? 'preview' : 'synced',
90
- changed: result.changed,
91
- })
92
- continue
93
- }
94
- if (!repository.repository) {
95
- report.push({
96
- path: repository.path,
97
- status: 'skipped',
98
- reason: 'origin is not a GitHub remote',
99
- })
100
- continue
101
- }
102
- report.push(upgradeRepository(repository, source, version))
103
- }
104
- console.log(JSON.stringify(report, null, 2))
105
- return report
106
- }
107
-
108
- /** @param {FleetRepository} repository @param {string} source @param {string} version */
109
- function upgradeRepository(repository, source, version) {
110
- const branch = `codex/code-foundry-upgrade-${version.replace(/^v/, '')}`
111
- const base = repository.gitWorkflow === 'staging-release' ? 'staging' : 'main'
112
- const temporary = mkdtempSync(join(tmpdir(), 'code-foundry-fleet-'))
113
- let attached = false
114
- try {
115
- const fetch = spawnSync('git', ['-C', repository.path, 'fetch', 'origin', base, '--quiet'], {
116
- encoding: 'utf8',
117
- })
118
- if (fetch.status !== 0)
119
- return {
120
- path: repository.path,
121
- status: 'skipped',
122
- reason: fetch.stderr.trim() || `unable to refresh ${base} baseline`,
123
- }
124
- const marker = `<!-- code-foundry-legacy-rollout:${repository.repository}:${version}:${base} -->`
125
- const commitMarker = `Code-Foundry-Legacy-Rollout: ${repository.repository}:${version}:${base}`
126
- const remoteBranch = spawnSync(
127
- 'git',
128
- ['-C', repository.path, 'ls-remote', '--heads', 'origin', `refs/heads/${branch}`],
129
- { encoding: 'utf8' }
130
- )
131
- if (remoteBranch.status !== 0)
132
- return {
133
- path: repository.path,
134
- status: 'failed',
135
- branch,
136
- reason: remoteBranch.stderr.trim() || 'unable to inspect existing upgrade branch',
137
- }
138
- const remote = remoteBranch.stdout.trim().split(/\s/)[0] ?? ''
139
- const local = spawnSync(
140
- 'git',
141
- ['-C', repository.path, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`],
142
- { encoding: 'utf8' }
143
- )
144
- if (local.status !== 0 && local.status !== 1)
145
- return {
146
- path: repository.path,
147
- status: 'failed',
148
- branch,
149
- reason: 'unable to inspect existing local upgrade branch',
150
- }
151
- const existing = remote || (local.status === 0 ? local.stdout.trim() : '')
152
- if (remote) {
153
- const branchFetch = spawnSync(
154
- 'git',
155
- ['-C', repository.path, 'fetch', 'origin', branch, '--quiet'],
156
- { encoding: 'utf8' }
157
- )
158
- if (branchFetch.status !== 0)
159
- return {
160
- path: repository.path,
161
- status: 'failed',
162
- branch,
163
- reason: branchFetch.stderr.trim() || 'unable to fetch existing upgrade branch',
164
- }
165
- }
166
- const baseline = spawnSync('git', ['-C', repository.path, 'rev-parse', `origin/${base}`], {
167
- encoding: 'utf8',
168
- })
169
- if (baseline.status !== 0)
170
- return {
171
- path: repository.path,
172
- status: 'skipped',
173
- reason: `remote ${base} branch is unavailable`,
174
- }
175
- if (existing) {
176
- const message = spawnSync(
177
- 'git',
178
- ['-C', repository.path, 'show', '-s', '--format=%B', existing],
179
- { encoding: 'utf8' }
180
- )
181
- const tree = spawnSync('git', ['-C', repository.path, 'rev-parse', `${existing}^{tree}`], {
182
- encoding: 'utf8',
183
- })
184
- const parent = spawnSync('git', ['-C', repository.path, 'rev-parse', `${existing}^`], {
185
- encoding: 'utf8',
186
- })
187
- const subject = message.stdout.split(/\r?\n/, 1)[0]
188
- const managed =
189
- message.status === 0 &&
190
- tree.status === 0 &&
191
- message.stdout.includes(commitMarker) &&
192
- message.stdout.includes(`Code-Foundry-Tree: ${tree.stdout.trim()}`)
193
- const legacyManaged =
194
- subject === `chore(code-foundry): upgrade runtime to ${version}` &&
195
- parent.status === 0 &&
196
- parent.stdout.trim() === baseline.stdout.trim()
197
- if (!managed && !legacyManaged)
198
- return {
199
- path: repository.path,
200
- status: 'failed',
201
- branch,
202
- reason: 'Existing upgrade branch is not a managed rollout; reconcile it manually',
203
- }
204
- }
205
- const add = spawnSync(
206
- 'git',
207
- [
208
- '-C',
209
- repository.path,
210
- 'worktree',
211
- 'add',
212
- '--detach',
213
- temporary,
214
- existing || baseline.stdout.trim(),
215
- ],
216
- { encoding: 'utf8' }
217
- )
218
- if (add.status !== 0)
219
- return {
220
- path: repository.path,
221
- status: 'skipped',
222
- reason: add.stderr.trim() || 'unable to create isolated worktree',
223
- }
224
- attached = true
225
- let validatedHead = existing
226
- if (!existing) {
227
- const result = syncRepository({
228
- target: temporary,
229
- source,
230
- force: false,
231
- runtimeRef: version,
232
- })
233
- const configFile = join(temporary, '.github/code-foundry.yml')
234
- if (existsSync(configFile)) {
235
- const current = readFileSync(configFile, 'utf8')
236
- const updated = current.match(/^runtime_ref:\s*.*$/m)
237
- ? current.replace(/^runtime_ref:\s*.*$/m, `runtime_ref: ${version}`)
238
- : `${current.trimEnd()}\nruntime_ref: ${version}\n`
239
- if (updated !== current) {
240
- writeFileSync(configFile, updated)
241
- if (!result.changed.includes('.github/code-foundry.yml'))
242
- result.changed.push('.github/code-foundry.yml')
243
- }
244
- }
245
- if (!result.changed.length) return { path: repository.path, status: 'unchanged', branch }
246
- const stage = spawnSync('git', ['-C', temporary, 'add', '--', ...result.changed], {
247
- encoding: 'utf8',
248
- })
249
- if (stage.status !== 0)
250
- return {
251
- path: repository.path,
252
- status: 'failed',
253
- branch,
254
- reason: stage.stderr.trim() || 'unable to stage generated upgrade files',
255
- }
256
- const staged = spawnSync('git', ['-C', temporary, 'diff', '--cached', '--name-only'], {
257
- encoding: 'utf8',
258
- })
259
- if (staged.status !== 0 || !staged.stdout.trim())
260
- return { path: repository.path, status: 'unchanged', branch }
261
- const tree = spawnSync('git', ['-C', temporary, 'write-tree'], { encoding: 'utf8' })
262
- if (tree.status !== 0)
263
- return {
264
- path: repository.path,
265
- status: 'failed',
266
- branch,
267
- reason: tree.stderr.trim() || 'unable to record generated upgrade tree',
268
- }
269
- const commit = spawnSync(
270
- 'git',
271
- [
272
- '-C',
273
- temporary,
274
- '-c',
275
- 'core.hooksPath=.githooks',
276
- 'commit',
277
- '-m',
278
- `chore(code-foundry): upgrade runtime to ${version}`,
279
- '-m',
280
- `${commitMarker}\nCode-Foundry-Tree: ${tree.stdout.trim()}`,
281
- ],
282
- { encoding: 'utf8' }
283
- )
284
- if (commit.status !== 0)
285
- return {
286
- path: repository.path,
287
- status: 'failed',
288
- branch,
289
- reason: commit.stderr.trim() || 'commit failed',
290
- }
291
- const committedTree = spawnSync('git', ['-C', temporary, 'rev-parse', 'HEAD^{tree}'], {
292
- encoding: 'utf8',
293
- })
294
- if (committedTree.status !== 0 || committedTree.stdout.trim() !== tree.stdout.trim())
295
- return {
296
- path: repository.path,
297
- status: 'failed',
298
- branch,
299
- reason: 'Commit hook changed the generated upgrade tree',
300
- }
301
- const sha = spawnSync('git', ['-C', temporary, 'rev-parse', 'HEAD'], { encoding: 'utf8' })
302
- if (sha.status !== 0)
303
- return {
304
- path: repository.path,
305
- status: 'failed',
306
- branch,
307
- reason: 'unable to inspect generated upgrade commit',
308
- }
309
- const update = spawnSync(
310
- 'git',
311
- [
312
- '-C',
313
- repository.path,
314
- 'update-ref',
315
- `refs/heads/${branch}`,
316
- sha.stdout.trim(),
317
- '0'.repeat(40),
318
- ],
319
- { encoding: 'utf8' }
320
- )
321
- if (update.status !== 0)
322
- return {
323
- path: repository.path,
324
- status: 'failed',
325
- branch,
326
- reason: update.stderr.trim() || 'unable to retain generated upgrade commit',
327
- }
328
- validatedHead = sha.stdout.trim()
329
- }
330
- if (remote && remote !== validatedHead)
331
- return {
332
- path: repository.path,
333
- status: 'failed',
334
- branch,
335
- reason: 'Upgrade branch changed during validation; refusing to create a pull request',
336
- }
337
- if (!remote) {
338
- const push = spawnSync(
339
- 'git',
340
- ['-C', temporary, 'push', '-u', 'origin', `HEAD:refs/heads/${branch}`],
341
- { encoding: 'utf8' }
342
- )
343
- if (push.status !== 0)
344
- return {
345
- path: repository.path,
346
- status: 'failed',
347
- branch,
348
- reason: push.stderr.trim() || 'push failed',
349
- }
350
- const pushed = spawnSync(
351
- 'git',
352
- ['-C', repository.path, 'ls-remote', '--heads', 'origin', `refs/heads/${branch}`],
353
- { encoding: 'utf8' }
354
- )
355
- if (pushed.status !== 0 || pushed.stdout.trim().split(/\s/)[0] !== validatedHead)
356
- return {
357
- path: repository.path,
358
- status: 'failed',
359
- branch,
360
- reason: 'Upgrade branch did not retain the validated commit',
361
- }
362
- }
363
- const listed = spawnSync(
364
- 'gh',
365
- [
366
- 'pr',
367
- 'list',
368
- '--repo',
369
- repository.repository,
370
- '--head',
371
- branch,
372
- '--base',
373
- base,
374
- '--state',
375
- 'all',
376
- '--limit',
377
- '100',
378
- '--json',
379
- 'url,body,state,headRefName,headRefOid,baseRefName,isCrossRepository,isDraft',
380
- ],
381
- { encoding: 'utf8' }
382
- )
383
- if (listed.status !== 0)
384
- return {
385
- path: repository.path,
386
- status: 'failed',
387
- branch,
388
- reason: listed.stderr.trim() || 'unable to inspect upgrade pull requests',
389
- }
390
- let pullRequests
391
- try {
392
- pullRequests = JSON.parse(listed.stdout)
393
- } catch {
394
- return {
395
- path: repository.path,
396
- status: 'failed',
397
- branch,
398
- reason: 'invalid pull request list',
399
- }
400
- }
401
- if (!Array.isArray(pullRequests) || pullRequests.length > 1)
402
- return {
403
- path: repository.path,
404
- status: 'failed',
405
- branch,
406
- reason: 'Ambiguous upgrade pull requests',
407
- }
408
- const existingPr = pullRequests[0]
409
- if (existingPr) {
410
- if (
411
- !existingPr.body?.includes(marker) ||
412
- existingPr.headRefName !== branch ||
413
- existingPr.baseRefName !== base ||
414
- existingPr.headRefOid !== validatedHead ||
415
- existingPr.isCrossRepository !== false
416
- )
417
- return {
418
- path: repository.path,
419
- status: 'failed',
420
- branch,
421
- reason: 'Existing pull request does not match this rollout policy',
422
- }
423
- return {
424
- path: repository.path,
425
- status: 'pr-existing',
426
- branch,
427
- pullRequest: existingPr.url,
428
- draft: existingPr.isDraft,
429
- }
430
- }
431
- const pr = spawnSync(
432
- 'gh',
433
- [
434
- 'pr',
435
- 'create',
436
- '--draft',
437
- '--repo',
438
- repository.repository,
439
- '--base',
440
- base,
441
- '--head',
442
- branch,
443
- '--title',
444
- `chore(code-foundry): upgrade to ${version}`,
445
- '--body',
446
- `${marker}\n\nAutomated isolated Code Foundry runtime upgrade to ${version}.\n\nThe sync preserved protected repository-owned documents and custom workflows.`,
447
- ],
448
- { encoding: 'utf8' }
449
- )
450
- if (pr.status !== 0)
451
- return {
452
- path: repository.path,
453
- status: 'failed',
454
- branch,
455
- reason: pr.stderr.trim() || 'pull request creation failed',
456
- }
457
- const created = spawnSync(
458
- 'gh',
459
- [
460
- 'pr',
461
- 'view',
462
- pr.stdout.trim(),
463
- '--repo',
464
- repository.repository,
465
- '--json',
466
- 'headRefOid,headRefName,baseRefName,isCrossRepository,isDraft',
467
- ],
468
- { encoding: 'utf8' }
469
- )
470
- if (created.status !== 0)
471
- return {
472
- path: repository.path,
473
- status: 'failed',
474
- branch,
475
- reason: created.stderr.trim() || 'unable to verify created pull request',
476
- }
477
- let createdPr
478
- try {
479
- createdPr = JSON.parse(created.stdout)
480
- } catch {
481
- return {
482
- path: repository.path,
483
- status: 'failed',
484
- branch,
485
- reason: 'invalid created pull request',
486
- }
487
- }
488
- if (
489
- createdPr.headRefOid !== validatedHead ||
490
- createdPr.headRefName !== branch ||
491
- createdPr.baseRefName !== base ||
492
- createdPr.isCrossRepository !== false ||
493
- createdPr.isDraft !== true
494
- )
495
- return {
496
- path: repository.path,
497
- status: 'failed',
498
- branch,
499
- reason: 'Created pull request does not reference the validated draft rollout branch',
500
- }
501
- return {
502
- path: repository.path,
503
- status: existing ? 'pr-resumed' : 'pr-created',
504
- branch,
505
- pullRequest: pr.stdout.trim(),
506
- }
507
- } finally {
508
- if (attached) {
509
- const cleanup = spawnSync(
510
- 'git',
511
- ['-C', repository.path, 'worktree', 'remove', '--force', temporary],
512
- { encoding: 'utf8' }
513
- )
514
- if (cleanup.status !== 0)
515
- console.error(`Preserved isolated worktree after cleanup failure: ${temporary}`)
516
- else rmSync(temporary, { recursive: true, force: true })
517
- } else rmSync(temporary, { recursive: true, force: true })
518
- }
519
- }
520
-
521
- /** @param {string} root @returns {string[]} */
522
- function children(root) {
523
- try {
524
- return readdirSync(root, { withFileTypes: true })
525
- .filter(
526
- (entry) =>
527
- entry.isDirectory() &&
528
- !entry.name.startsWith('.') &&
529
- !['node_modules', 'target'].includes(entry.name)
530
- )
531
- .map((entry) => join(root, entry.name))
532
- } catch {
533
- return []
534
- }
535
- }
536
-
537
- /** @param {string} file @returns {Record<string, string>} */
538
- function readConfig(file) {
539
- try {
540
- return Object.fromEntries(
541
- requireText(file)
542
- .split(/\r?\n/)
543
- .flatMap((line) => {
544
- const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/)
545
- return match
546
- ? [[match[1], match[2].replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '')]]
547
- : []
548
- })
549
- )
550
- } catch {
551
- return {}
552
- }
553
- }
554
-
555
- /** @param {string} file */
556
- function requireText(file) {
557
- return readFileSync(file, 'utf8')
558
- }
559
-
560
- /** @param {string} root @param {string[]} args */
561
- function git(root, args) {
562
- return spawnSync('git', args, { cwd: root, encoding: 'utf8' }).stdout?.trim() ?? ''
563
- }
564
-
565
- /** @param {string} remote */
566
- function normalizeRemote(remote) {
567
- return remote
568
- .replace(/^git@github\.com:/, '')
569
- .replace(/^https?:\/\/github\.com\//, '')
570
- .replace(/\.git$/, '')
36
+ requireFleetManifest(root)
37
+ return upgradeManifestFleet(root, source, { ...options, version }, syncRepository)
571
38
  }
@@ -613,7 +613,10 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
613
613
  `${remotePrefix}validation-no-codeql.yml`
614
614
  )
615
615
  rendered = removeWorkflowBlock(rendered, 'default-branch-codeql')
616
- rendered = removeWorkflowBlock(rendered, 'push')
616
+ // Only the validation caller's default-branch trigger is tied to CodeQL.
617
+ // Draft-PR, release, and other callers still need their own push triggers.
618
+ if (file === '.github/workflows/validation.yml')
619
+ rendered = removeWorkflowBlock(rendered, 'push')
617
620
  }
618
621
  rendered = rendered.replace(
619
622
  new RegExp(`${escapeRegExp(remotePrefix)}([^\\s@]+)`, 'g'),