code-foundry 1.12.0 → 1.13.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 +7 -6
- package/.github/workflows/draft-enforcement_self-ci.yml +3 -4
- package/.github/workflows/opencode-security_self-ci.yml +2 -1
- package/.github/workflows/validation_self-ci.yml +6 -4
- package/AGENTS.md +6 -5
- package/CHANGELOG.md +7 -0
- package/README.md +7 -6
- package/docs/PERFORMANCE.md +3 -3
- package/docs/README.md +1 -0
- package/docs/WORKFLOWS.md +14 -14
- package/docs/fleet-rollouts.md +139 -0
- package/package.json +1 -1
- package/src/commands/doctor.mjs +8 -3
- package/src/commands/fleet.mjs +355 -42
- package/src/commands/sync.mjs +11 -6
- package/src/lib/fleet-manifest.mjs +654 -0
package/src/commands/fleet.mjs
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { existsSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { spawnSync } from 'node:child_process'
|
|
7
7
|
import { readPackageVersion, syncRepository } from './sync.mjs'
|
|
8
|
+
import {
|
|
9
|
+
discoverManifestRepositories,
|
|
10
|
+
hasFleetManifest,
|
|
11
|
+
upgradeManifestFleet,
|
|
12
|
+
} from '../lib/fleet-manifest.mjs'
|
|
8
13
|
|
|
9
14
|
/** @typedef {{ path: string, repository: string, runtimeRef: string, dirty: boolean, configured: boolean, gitWorkflow: string }} FleetRepository */
|
|
10
15
|
|
|
11
16
|
/** @param {string} root @returns {FleetRepository[]} */
|
|
12
17
|
export function discoverRepositories(root) {
|
|
18
|
+
if (hasFleetManifest(root)) return discoverManifestRepositories(root)
|
|
13
19
|
/** @type {FleetRepository[]} */
|
|
14
20
|
const result = []
|
|
15
21
|
const candidates = [root, ...children(root), ...children(join(root, 'NiftyLeague'))]
|
|
@@ -39,6 +45,8 @@ export function upgradeFleet(root, source, options) {
|
|
|
39
45
|
`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.`
|
|
40
46
|
)
|
|
41
47
|
}
|
|
48
|
+
if (hasFleetManifest(root))
|
|
49
|
+
return upgradeManifestFleet(root, source, { ...options, version }, syncRepository)
|
|
42
50
|
const repositories = discoverRepositories(root)
|
|
43
51
|
const report = []
|
|
44
52
|
for (const repository of repositories) {
|
|
@@ -98,6 +106,7 @@ function upgradeRepository(repository, source, version) {
|
|
|
98
106
|
const branch = `codex/code-foundry-upgrade-${version.replace(/^v/, '')}`
|
|
99
107
|
const base = repository.gitWorkflow === 'staging-release' ? 'staging' : 'main'
|
|
100
108
|
const temporary = mkdtempSync(join(tmpdir(), 'code-foundry-fleet-'))
|
|
109
|
+
let attached = false
|
|
101
110
|
try {
|
|
102
111
|
const fetch = spawnSync('git', ['-C', repository.path, 'fetch', 'origin', base, '--quiet'], {
|
|
103
112
|
encoding: 'utf8',
|
|
@@ -108,6 +117,48 @@ function upgradeRepository(repository, source, version) {
|
|
|
108
117
|
status: 'skipped',
|
|
109
118
|
reason: fetch.stderr.trim() || `unable to refresh ${base} baseline`,
|
|
110
119
|
}
|
|
120
|
+
const marker = `<!-- code-foundry-legacy-rollout:${repository.repository}:${version}:${base} -->`
|
|
121
|
+
const commitMarker = `Code-Foundry-Legacy-Rollout: ${repository.repository}:${version}:${base}`
|
|
122
|
+
const remoteBranch = spawnSync(
|
|
123
|
+
'git',
|
|
124
|
+
['-C', repository.path, 'ls-remote', '--heads', 'origin', `refs/heads/${branch}`],
|
|
125
|
+
{ encoding: 'utf8' }
|
|
126
|
+
)
|
|
127
|
+
if (remoteBranch.status !== 0)
|
|
128
|
+
return {
|
|
129
|
+
path: repository.path,
|
|
130
|
+
status: 'failed',
|
|
131
|
+
branch,
|
|
132
|
+
reason: remoteBranch.stderr.trim() || 'unable to inspect existing upgrade branch',
|
|
133
|
+
}
|
|
134
|
+
const remote = remoteBranch.stdout.trim().split(/\s/)[0] ?? ''
|
|
135
|
+
const local = spawnSync(
|
|
136
|
+
'git',
|
|
137
|
+
['-C', repository.path, 'rev-parse', '--verify', '--quiet', `refs/heads/${branch}`],
|
|
138
|
+
{ encoding: 'utf8' }
|
|
139
|
+
)
|
|
140
|
+
if (local.status !== 0 && local.status !== 1)
|
|
141
|
+
return {
|
|
142
|
+
path: repository.path,
|
|
143
|
+
status: 'failed',
|
|
144
|
+
branch,
|
|
145
|
+
reason: 'unable to inspect existing local upgrade branch',
|
|
146
|
+
}
|
|
147
|
+
const existing = remote || (local.status === 0 ? local.stdout.trim() : '')
|
|
148
|
+
if (remote) {
|
|
149
|
+
const branchFetch = spawnSync(
|
|
150
|
+
'git',
|
|
151
|
+
['-C', repository.path, 'fetch', 'origin', branch, '--quiet'],
|
|
152
|
+
{ encoding: 'utf8' }
|
|
153
|
+
)
|
|
154
|
+
if (branchFetch.status !== 0)
|
|
155
|
+
return {
|
|
156
|
+
path: repository.path,
|
|
157
|
+
status: 'failed',
|
|
158
|
+
branch,
|
|
159
|
+
reason: branchFetch.stderr.trim() || 'unable to fetch existing upgrade branch',
|
|
160
|
+
}
|
|
161
|
+
}
|
|
111
162
|
const baseline = spawnSync('git', ['-C', repository.path, 'rev-parse', `origin/${base}`], {
|
|
112
163
|
encoding: 'utf8',
|
|
113
164
|
})
|
|
@@ -117,9 +168,47 @@ function upgradeRepository(repository, source, version) {
|
|
|
117
168
|
status: 'skipped',
|
|
118
169
|
reason: `remote ${base} branch is unavailable`,
|
|
119
170
|
}
|
|
171
|
+
if (existing) {
|
|
172
|
+
const message = spawnSync(
|
|
173
|
+
'git',
|
|
174
|
+
['-C', repository.path, 'show', '-s', '--format=%B', existing],
|
|
175
|
+
{ encoding: 'utf8' }
|
|
176
|
+
)
|
|
177
|
+
const tree = spawnSync('git', ['-C', repository.path, 'rev-parse', `${existing}^{tree}`], {
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
})
|
|
180
|
+
const parent = spawnSync('git', ['-C', repository.path, 'rev-parse', `${existing}^`], {
|
|
181
|
+
encoding: 'utf8',
|
|
182
|
+
})
|
|
183
|
+
const subject = message.stdout.split(/\r?\n/, 1)[0]
|
|
184
|
+
const managed =
|
|
185
|
+
message.status === 0 &&
|
|
186
|
+
tree.status === 0 &&
|
|
187
|
+
message.stdout.includes(commitMarker) &&
|
|
188
|
+
message.stdout.includes(`Code-Foundry-Tree: ${tree.stdout.trim()}`)
|
|
189
|
+
const legacyManaged =
|
|
190
|
+
subject === `chore(code-foundry): upgrade runtime to ${version}` &&
|
|
191
|
+
parent.status === 0 &&
|
|
192
|
+
parent.stdout.trim() === baseline.stdout.trim()
|
|
193
|
+
if (!managed && !legacyManaged)
|
|
194
|
+
return {
|
|
195
|
+
path: repository.path,
|
|
196
|
+
status: 'failed',
|
|
197
|
+
branch,
|
|
198
|
+
reason: 'Existing upgrade branch is not a managed rollout; reconcile it manually',
|
|
199
|
+
}
|
|
200
|
+
}
|
|
120
201
|
const add = spawnSync(
|
|
121
202
|
'git',
|
|
122
|
-
[
|
|
203
|
+
[
|
|
204
|
+
'-C',
|
|
205
|
+
repository.path,
|
|
206
|
+
'worktree',
|
|
207
|
+
'add',
|
|
208
|
+
'--detach',
|
|
209
|
+
temporary,
|
|
210
|
+
existing || baseline.stdout.trim(),
|
|
211
|
+
],
|
|
123
212
|
{ encoding: 'utf8' }
|
|
124
213
|
)
|
|
125
214
|
if (add.status !== 0)
|
|
@@ -128,51 +217,219 @@ function upgradeRepository(repository, source, version) {
|
|
|
128
217
|
status: 'skipped',
|
|
129
218
|
reason: add.stderr.trim() || 'unable to create isolated worktree',
|
|
130
219
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
220
|
+
attached = true
|
|
221
|
+
let validatedHead = existing
|
|
222
|
+
if (!existing) {
|
|
223
|
+
const result = syncRepository({
|
|
224
|
+
target: temporary,
|
|
225
|
+
source,
|
|
226
|
+
force: false,
|
|
227
|
+
runtimeRef: version,
|
|
228
|
+
})
|
|
229
|
+
const configFile = join(temporary, '.github/code-foundry.yml')
|
|
230
|
+
if (existsSync(configFile)) {
|
|
231
|
+
const current = readFileSync(configFile, 'utf8')
|
|
232
|
+
const updated = current.match(/^runtime_ref:\s*.*$/m)
|
|
233
|
+
? current.replace(/^runtime_ref:\s*.*$/m, `runtime_ref: ${version}`)
|
|
234
|
+
: `${current.trimEnd()}\nruntime_ref: ${version}\n`
|
|
235
|
+
if (updated !== current) {
|
|
236
|
+
writeFileSync(configFile, updated)
|
|
237
|
+
if (!result.changed.includes('.github/code-foundry.yml'))
|
|
238
|
+
result.changed.push('.github/code-foundry.yml')
|
|
239
|
+
}
|
|
147
240
|
}
|
|
241
|
+
if (!result.changed.length) return { path: repository.path, status: 'unchanged', branch }
|
|
242
|
+
const stage = spawnSync('git', ['-C', temporary, 'add', '--', ...result.changed], {
|
|
243
|
+
encoding: 'utf8',
|
|
244
|
+
})
|
|
245
|
+
if (stage.status !== 0)
|
|
246
|
+
return {
|
|
247
|
+
path: repository.path,
|
|
248
|
+
status: 'failed',
|
|
249
|
+
branch,
|
|
250
|
+
reason: stage.stderr.trim() || 'unable to stage generated upgrade files',
|
|
251
|
+
}
|
|
252
|
+
const staged = spawnSync('git', ['-C', temporary, 'diff', '--cached', '--name-only'], {
|
|
253
|
+
encoding: 'utf8',
|
|
254
|
+
})
|
|
255
|
+
if (staged.status !== 0 || !staged.stdout.trim())
|
|
256
|
+
return { path: repository.path, status: 'unchanged', branch }
|
|
257
|
+
const tree = spawnSync('git', ['-C', temporary, 'write-tree'], { encoding: 'utf8' })
|
|
258
|
+
if (tree.status !== 0)
|
|
259
|
+
return {
|
|
260
|
+
path: repository.path,
|
|
261
|
+
status: 'failed',
|
|
262
|
+
branch,
|
|
263
|
+
reason: tree.stderr.trim() || 'unable to record generated upgrade tree',
|
|
264
|
+
}
|
|
265
|
+
const commit = spawnSync(
|
|
266
|
+
'git',
|
|
267
|
+
[
|
|
268
|
+
'-C',
|
|
269
|
+
temporary,
|
|
270
|
+
'-c',
|
|
271
|
+
'core.hooksPath=.githooks',
|
|
272
|
+
'commit',
|
|
273
|
+
'-m',
|
|
274
|
+
`chore(code-foundry): upgrade runtime to ${version}`,
|
|
275
|
+
'-m',
|
|
276
|
+
`${commitMarker}\nCode-Foundry-Tree: ${tree.stdout.trim()}`,
|
|
277
|
+
],
|
|
278
|
+
{ encoding: 'utf8' }
|
|
279
|
+
)
|
|
280
|
+
if (commit.status !== 0)
|
|
281
|
+
return {
|
|
282
|
+
path: repository.path,
|
|
283
|
+
status: 'failed',
|
|
284
|
+
branch,
|
|
285
|
+
reason: commit.stderr.trim() || 'commit failed',
|
|
286
|
+
}
|
|
287
|
+
const committedTree = spawnSync('git', ['-C', temporary, 'rev-parse', 'HEAD^{tree}'], {
|
|
288
|
+
encoding: 'utf8',
|
|
289
|
+
})
|
|
290
|
+
if (committedTree.status !== 0 || committedTree.stdout.trim() !== tree.stdout.trim())
|
|
291
|
+
return {
|
|
292
|
+
path: repository.path,
|
|
293
|
+
status: 'failed',
|
|
294
|
+
branch,
|
|
295
|
+
reason: 'Commit hook changed the generated upgrade tree',
|
|
296
|
+
}
|
|
297
|
+
const sha = spawnSync('git', ['-C', temporary, 'rev-parse', 'HEAD'], { encoding: 'utf8' })
|
|
298
|
+
if (sha.status !== 0)
|
|
299
|
+
return {
|
|
300
|
+
path: repository.path,
|
|
301
|
+
status: 'failed',
|
|
302
|
+
branch,
|
|
303
|
+
reason: 'unable to inspect generated upgrade commit',
|
|
304
|
+
}
|
|
305
|
+
const update = spawnSync(
|
|
306
|
+
'git',
|
|
307
|
+
[
|
|
308
|
+
'-C',
|
|
309
|
+
repository.path,
|
|
310
|
+
'update-ref',
|
|
311
|
+
`refs/heads/${branch}`,
|
|
312
|
+
sha.stdout.trim(),
|
|
313
|
+
'0'.repeat(40),
|
|
314
|
+
],
|
|
315
|
+
{ encoding: 'utf8' }
|
|
316
|
+
)
|
|
317
|
+
if (update.status !== 0)
|
|
318
|
+
return {
|
|
319
|
+
path: repository.path,
|
|
320
|
+
status: 'failed',
|
|
321
|
+
branch,
|
|
322
|
+
reason: update.stderr.trim() || 'unable to retain generated upgrade commit',
|
|
323
|
+
}
|
|
324
|
+
validatedHead = sha.stdout.trim()
|
|
148
325
|
}
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
326
|
+
if (remote && remote !== validatedHead)
|
|
327
|
+
return {
|
|
328
|
+
path: repository.path,
|
|
329
|
+
status: 'failed',
|
|
330
|
+
branch,
|
|
331
|
+
reason: 'Upgrade branch changed during validation; refusing to create a pull request',
|
|
332
|
+
}
|
|
333
|
+
if (!remote) {
|
|
334
|
+
const push = spawnSync(
|
|
335
|
+
'git',
|
|
336
|
+
['-C', temporary, 'push', '-u', 'origin', `HEAD:refs/heads/${branch}`],
|
|
337
|
+
{ encoding: 'utf8' }
|
|
338
|
+
)
|
|
339
|
+
if (push.status !== 0)
|
|
340
|
+
return {
|
|
341
|
+
path: repository.path,
|
|
342
|
+
status: 'failed',
|
|
343
|
+
branch,
|
|
344
|
+
reason: push.stderr.trim() || 'push failed',
|
|
345
|
+
}
|
|
346
|
+
const pushed = spawnSync(
|
|
347
|
+
'git',
|
|
348
|
+
['-C', repository.path, 'ls-remote', '--heads', 'origin', `refs/heads/${branch}`],
|
|
349
|
+
{ encoding: 'utf8' }
|
|
350
|
+
)
|
|
351
|
+
if (pushed.status !== 0 || pushed.stdout.trim().split(/\s/)[0] !== validatedHead)
|
|
352
|
+
return {
|
|
353
|
+
path: repository.path,
|
|
354
|
+
status: 'failed',
|
|
355
|
+
branch,
|
|
356
|
+
reason: 'Upgrade branch did not retain the validated commit',
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const listed = spawnSync(
|
|
360
|
+
'gh',
|
|
361
|
+
[
|
|
362
|
+
'pr',
|
|
363
|
+
'list',
|
|
364
|
+
'--repo',
|
|
365
|
+
repository.repository,
|
|
366
|
+
'--head',
|
|
367
|
+
branch,
|
|
368
|
+
'--base',
|
|
369
|
+
base,
|
|
370
|
+
'--state',
|
|
371
|
+
'all',
|
|
372
|
+
'--limit',
|
|
373
|
+
'100',
|
|
374
|
+
'--json',
|
|
375
|
+
'url,body,state,headRefName,headRefOid,baseRefName,isCrossRepository,isDraft',
|
|
376
|
+
],
|
|
154
377
|
{ encoding: 'utf8' }
|
|
155
378
|
)
|
|
156
|
-
if (
|
|
379
|
+
if (listed.status !== 0)
|
|
380
|
+
return {
|
|
381
|
+
path: repository.path,
|
|
382
|
+
status: 'failed',
|
|
383
|
+
branch,
|
|
384
|
+
reason: listed.stderr.trim() || 'unable to inspect upgrade pull requests',
|
|
385
|
+
}
|
|
386
|
+
let pullRequests
|
|
387
|
+
try {
|
|
388
|
+
pullRequests = JSON.parse(listed.stdout)
|
|
389
|
+
} catch {
|
|
157
390
|
return {
|
|
158
391
|
path: repository.path,
|
|
159
392
|
status: 'failed',
|
|
160
|
-
|
|
393
|
+
branch,
|
|
394
|
+
reason: 'invalid pull request list',
|
|
161
395
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
})
|
|
165
|
-
if (push.status !== 0)
|
|
396
|
+
}
|
|
397
|
+
if (!Array.isArray(pullRequests) || pullRequests.length > 1)
|
|
166
398
|
return {
|
|
167
399
|
path: repository.path,
|
|
168
400
|
status: 'failed',
|
|
169
|
-
|
|
401
|
+
branch,
|
|
402
|
+
reason: 'Ambiguous upgrade pull requests',
|
|
170
403
|
}
|
|
404
|
+
const existingPr = pullRequests[0]
|
|
405
|
+
if (existingPr) {
|
|
406
|
+
if (
|
|
407
|
+
!existingPr.body?.includes(marker) ||
|
|
408
|
+
existingPr.headRefName !== branch ||
|
|
409
|
+
existingPr.baseRefName !== base ||
|
|
410
|
+
existingPr.headRefOid !== validatedHead ||
|
|
411
|
+
existingPr.isCrossRepository !== false
|
|
412
|
+
)
|
|
413
|
+
return {
|
|
414
|
+
path: repository.path,
|
|
415
|
+
status: 'failed',
|
|
416
|
+
branch,
|
|
417
|
+
reason: 'Existing pull request does not match this rollout policy',
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
path: repository.path,
|
|
421
|
+
status: 'pr-existing',
|
|
422
|
+
branch,
|
|
423
|
+
pullRequest: existingPr.url,
|
|
424
|
+
draft: existingPr.isDraft,
|
|
425
|
+
}
|
|
426
|
+
}
|
|
171
427
|
const pr = spawnSync(
|
|
172
428
|
'gh',
|
|
173
429
|
[
|
|
174
430
|
'pr',
|
|
175
431
|
'create',
|
|
432
|
+
'--draft',
|
|
176
433
|
'--repo',
|
|
177
434
|
repository.repository,
|
|
178
435
|
'--base',
|
|
@@ -182,22 +439,78 @@ function upgradeRepository(repository, source, version) {
|
|
|
182
439
|
'--title',
|
|
183
440
|
`chore(code-foundry): upgrade to ${version}`,
|
|
184
441
|
'--body',
|
|
185
|
-
|
|
442
|
+
`${marker}\n\nAutomated isolated Code Foundry runtime upgrade to ${version}.\n\nThe sync preserved protected repository-owned documents and custom workflows.`,
|
|
186
443
|
],
|
|
187
444
|
{ encoding: 'utf8' }
|
|
188
445
|
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
446
|
+
if (pr.status !== 0)
|
|
447
|
+
return {
|
|
448
|
+
path: repository.path,
|
|
449
|
+
status: 'failed',
|
|
450
|
+
branch,
|
|
451
|
+
reason: pr.stderr.trim() || 'pull request creation failed',
|
|
452
|
+
}
|
|
453
|
+
const created = spawnSync(
|
|
454
|
+
'gh',
|
|
455
|
+
[
|
|
456
|
+
'pr',
|
|
457
|
+
'view',
|
|
458
|
+
pr.stdout.trim(),
|
|
459
|
+
'--repo',
|
|
460
|
+
repository.repository,
|
|
461
|
+
'--json',
|
|
462
|
+
'headRefOid,headRefName,baseRefName,isCrossRepository,isDraft',
|
|
463
|
+
],
|
|
464
|
+
{ encoding: 'utf8' }
|
|
465
|
+
)
|
|
466
|
+
if (created.status !== 0)
|
|
467
|
+
return {
|
|
468
|
+
path: repository.path,
|
|
469
|
+
status: 'failed',
|
|
470
|
+
branch,
|
|
471
|
+
reason: created.stderr.trim() || 'unable to verify created pull request',
|
|
472
|
+
}
|
|
473
|
+
let createdPr
|
|
474
|
+
try {
|
|
475
|
+
createdPr = JSON.parse(created.stdout)
|
|
476
|
+
} catch {
|
|
477
|
+
return {
|
|
478
|
+
path: repository.path,
|
|
479
|
+
status: 'failed',
|
|
480
|
+
branch,
|
|
481
|
+
reason: 'invalid created pull request',
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (
|
|
485
|
+
createdPr.headRefOid !== validatedHead ||
|
|
486
|
+
createdPr.headRefName !== branch ||
|
|
487
|
+
createdPr.baseRefName !== base ||
|
|
488
|
+
createdPr.isCrossRepository !== false ||
|
|
489
|
+
createdPr.isDraft !== true
|
|
490
|
+
)
|
|
491
|
+
return {
|
|
492
|
+
path: repository.path,
|
|
493
|
+
status: 'failed',
|
|
494
|
+
branch,
|
|
495
|
+
reason: 'Created pull request does not reference the validated draft rollout branch',
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
path: repository.path,
|
|
499
|
+
status: existing ? 'pr-resumed' : 'pr-created',
|
|
500
|
+
branch,
|
|
501
|
+
pullRequest: pr.stdout.trim(),
|
|
502
|
+
}
|
|
197
503
|
} finally {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
504
|
+
if (attached) {
|
|
505
|
+
const cleanup = spawnSync(
|
|
506
|
+
'git',
|
|
507
|
+
['-C', repository.path, 'worktree', 'remove', '--force', temporary],
|
|
508
|
+
{ encoding: 'utf8' }
|
|
509
|
+
)
|
|
510
|
+
if (cleanup.status !== 0)
|
|
511
|
+
console.error(`Preserved isolated worktree after cleanup failure: ${temporary}`)
|
|
512
|
+
else rmSync(temporary, { recursive: true, force: true })
|
|
513
|
+
} else rmSync(temporary, { recursive: true, force: true })
|
|
201
514
|
}
|
|
202
515
|
}
|
|
203
516
|
|
package/src/commands/sync.mjs
CHANGED
|
@@ -105,7 +105,7 @@ const legacyFiles = [
|
|
|
105
105
|
'.github/licenses/AGPL-3.0-or-later.txt',
|
|
106
106
|
]
|
|
107
107
|
|
|
108
|
-
/** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean, runtimeRef?: string }} SyncOptions */
|
|
108
|
+
/** @typedef {{ target: string, source: string, dryRun?: boolean, force?: boolean, init?: boolean, runtimeRef?: string, configureHooks?: boolean }} SyncOptions */
|
|
109
109
|
|
|
110
110
|
/** @param {SyncOptions} options */
|
|
111
111
|
export function syncRepository(options) {
|
|
@@ -115,6 +115,7 @@ export function syncRepository(options) {
|
|
|
115
115
|
const force = options.force ?? false
|
|
116
116
|
const configPath = join(target, '.github/code-foundry.yml')
|
|
117
117
|
const existingConfig = readConfig(configPath)
|
|
118
|
+
const changed = []
|
|
118
119
|
if (!Object.keys(existingConfig).length && !options.init)
|
|
119
120
|
throw new Error('Missing .github/code-foundry.yml; run init first.')
|
|
120
121
|
const defaults = createDefaultConfig(target, source, existingConfig.git_workflow)
|
|
@@ -141,12 +142,14 @@ export function syncRepository(options) {
|
|
|
141
142
|
throw new Error(`Unsupported license: ${license}; use ${supported}.`)
|
|
142
143
|
}
|
|
143
144
|
if (!Object.keys(existingConfig).length) {
|
|
145
|
+
changed.push('.github/code-foundry.yml')
|
|
144
146
|
writeOrReport(configPath, renderConfig(config), dryRun)
|
|
145
147
|
} else {
|
|
146
148
|
const missing = Object.keys(defaults).filter((key) => !(key in existingConfig))
|
|
147
149
|
const original = readFileSync(configPath, 'utf8')
|
|
148
150
|
const normalized = removeConfigKeys(original, obsoleteConfigKeys).trimEnd()
|
|
149
151
|
if (missing.length || normalized !== original.trimEnd()) {
|
|
152
|
+
changed.push('.github/code-foundry.yml')
|
|
150
153
|
const additions = missing.map((key) => renderConfigLine(key, defaults[key])).join('\n')
|
|
151
154
|
writeOrReport(configPath, `${normalized}${additions ? `\n${additions}` : ''}\n`, dryRun)
|
|
152
155
|
}
|
|
@@ -197,8 +200,6 @@ export function syncRepository(options) {
|
|
|
197
200
|
)
|
|
198
201
|
}
|
|
199
202
|
}
|
|
200
|
-
const changed = []
|
|
201
|
-
|
|
202
203
|
// Keep normal semver pins current during sync while preserving intentional
|
|
203
204
|
// refs such as `main`, `staging`, or a custom immutable SHA. An explicit
|
|
204
205
|
// runtime ref (fleet upgrade) is authoritative and overrides even those so
|
|
@@ -415,7 +416,11 @@ export function syncRepository(options) {
|
|
|
415
416
|
}
|
|
416
417
|
}
|
|
417
418
|
}
|
|
418
|
-
if (
|
|
419
|
+
if (
|
|
420
|
+
!dryRun &&
|
|
421
|
+
options.configureHooks !== false &&
|
|
422
|
+
existsSync(join(target, '.githooks/pre-commit'))
|
|
423
|
+
) {
|
|
419
424
|
chmodSync(join(target, '.githooks/pre-commit'), 0o755)
|
|
420
425
|
git(target, ['config', 'core.hooksPath', '.githooks'])
|
|
421
426
|
}
|
|
@@ -809,8 +814,8 @@ const DIRECT_DOC_REPLACEMENTS = {
|
|
|
809
814
|
'| Change | Target | Merge method | Merge gate |\n| ------------------------- | ------ | --------------------------------- | --------------------------------------- |\n| Working branch | `main` | Squash | All applicable required checks pass |\n| Release Please version PR | `main` | Squash (`release_merge_strategy`) | Validation gate and release policy pass |\n',
|
|
810
815
|
],
|
|
811
816
|
[
|
|
812
|
-
'Draft pull requests do not start runner-heavy validation. The lightweight Draft Guard
|
|
813
|
-
'Draft pull requests do not start validation. The lightweight Draft Guard
|
|
817
|
+
'Draft pull requests do not start runner-heavy validation. The lightweight Draft Guard converts ordinary pull requests opened or reopened while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier, and each new commit on a ready pull request reruns that tier for the current head. Draft updates allocate no validation runner. Converting a pull request to draft cancels in-flight validation through the lightweight cancellation control.',
|
|
818
|
+
'Draft pull requests do not start validation. The lightweight Draft Guard converts ordinary pull requests opened or reopened while ready back to draft; it never checks out pull-request code and it excludes Release Please version heads, whose release workflow owns their state. Marking a pull request ready for review starts the applicable validation tier, and each new commit on a ready pull request reruns that tier for the current head. Draft updates allocate no validation runner. Converting a pull request to draft runs only the lightweight cancellation control.',
|
|
814
819
|
],
|
|
815
820
|
['1. Create a focused branch from `staging`.', '1. Create a focused branch from `main`.'],
|
|
816
821
|
],
|