code-foundry 1.28.4 → 1.28.5
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 +7 -0
- package/docs/fleet-rollouts.md +6 -6
- package/package.json +1 -1
- package/src/commands/fleet-core.mjs +13 -546
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.28.5](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.4...v1.28.5) (2026-09-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **fleet:** require explicit inventory manifest ([#603](https://github.com/0xPlayerOne/code-foundry/issues/603)) ([1345331](https://github.com/0xPlayerOne/code-foundry/commit/13453318ba4fb21e6df6d978f1d51119148300ba))
|
|
9
|
+
|
|
3
10
|
## [1.28.4](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.3...v1.28.4) (2026-09-10)
|
|
4
11
|
|
|
5
12
|
|
package/docs/fleet-rollouts.md
CHANGED
|
@@ -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`.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
111
|
-
|
|
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.
|
|
3
|
+
"version": "1.28.5",
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
}
|