code-foundry 1.2.1 → 1.3.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/workflows/ci.yml +1 -1
- package/.oxfmtrc.json +7 -0
- package/.oxlintrc.json +5 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +7 -0
- package/README.md +3 -2
- package/docs/CONFIGURATION.md +24 -24
- package/docs/WORKFLOWS.md +16 -16
- package/package.json +20 -17
- package/src/cli.mjs +60 -14
- package/src/commands/ci.mjs +166 -57
- package/src/commands/doctor.mjs +98 -31
- package/src/commands/fleet.mjs +139 -29
- package/src/commands/release.mjs +378 -96
- package/src/commands/sync.mjs +332 -104
- package/src/lib/config.mjs +8 -2
- package/src/lib/github-doctor.mjs +153 -51
- package/src/lib/overlay.mjs +6 -2
- package/src/lib/profile.mjs +26 -8
- package/src/lib/release-hook.mjs +17 -5
- package/src/lib/release-manifest.mjs +81 -19
- package/src/lib/release-policy.mjs +157 -55
- package/src/lib/test-discovery.mjs +7 -2
- package/src/runtime.mjs +259 -61
- package/.prettierignore +0 -9
- package/.prettierrc +0 -6
package/src/commands/fleet.mjs
CHANGED
|
@@ -13,7 +13,7 @@ export function discoverRepositories(root) {
|
|
|
13
13
|
/** @type {FleetRepository[]} */
|
|
14
14
|
const result = []
|
|
15
15
|
const candidates = [root, ...children(root), ...children(join(root, 'NiftyLeague'))]
|
|
16
|
-
for (const candidate of
|
|
16
|
+
for (const candidate of new Set(candidates)) {
|
|
17
17
|
if (!existsSync(join(candidate, '.git'))) continue
|
|
18
18
|
const configured = existsSync(join(candidate, '.github/code-foundry.yml'))
|
|
19
19
|
const config = readConfig(join(candidate, '.github/code-foundry.yml'))
|
|
@@ -35,12 +35,23 @@ export function upgradeFleet(root, source, options) {
|
|
|
35
35
|
const repositories = discoverRepositories(root)
|
|
36
36
|
const report = []
|
|
37
37
|
for (const repository of repositories) {
|
|
38
|
-
if (
|
|
38
|
+
if (
|
|
39
|
+
(options.exclude ?? []).some(
|
|
40
|
+
(value) =>
|
|
41
|
+
repository.path === value ||
|
|
42
|
+
repository.repository === value ||
|
|
43
|
+
repository.path.endsWith(`/${value}`)
|
|
44
|
+
)
|
|
45
|
+
) {
|
|
39
46
|
report.push({ path: repository.path, status: 'skipped', reason: 'excluded by fleet policy' })
|
|
40
47
|
continue
|
|
41
48
|
}
|
|
42
49
|
if (!repository.configured) {
|
|
43
|
-
report.push({
|
|
50
|
+
report.push({
|
|
51
|
+
path: repository.path,
|
|
52
|
+
status: 'skipped',
|
|
53
|
+
reason: 'missing .github/code-foundry.yml',
|
|
54
|
+
})
|
|
44
55
|
continue
|
|
45
56
|
}
|
|
46
57
|
if (repository.dirty && !options.force) {
|
|
@@ -48,12 +59,25 @@ export function upgradeFleet(root, source, options) {
|
|
|
48
59
|
continue
|
|
49
60
|
}
|
|
50
61
|
if (!options.createPr || options.dryRun) {
|
|
51
|
-
const result = syncRepository({
|
|
52
|
-
|
|
62
|
+
const result = syncRepository({
|
|
63
|
+
target: repository.path,
|
|
64
|
+
source,
|
|
65
|
+
dryRun: options.dryRun,
|
|
66
|
+
force: false,
|
|
67
|
+
})
|
|
68
|
+
report.push({
|
|
69
|
+
path: repository.path,
|
|
70
|
+
status: options.dryRun ? 'preview' : 'synced',
|
|
71
|
+
changed: result.changed,
|
|
72
|
+
})
|
|
53
73
|
continue
|
|
54
74
|
}
|
|
55
75
|
if (!repository.repository) {
|
|
56
|
-
report.push({
|
|
76
|
+
report.push({
|
|
77
|
+
path: repository.path,
|
|
78
|
+
status: 'skipped',
|
|
79
|
+
reason: 'origin is not a GitHub remote',
|
|
80
|
+
})
|
|
57
81
|
continue
|
|
58
82
|
}
|
|
59
83
|
report.push(upgradeRepository(repository, source, options.version))
|
|
@@ -68,12 +92,35 @@ function upgradeRepository(repository, source, version) {
|
|
|
68
92
|
const base = repository.gitWorkflow === 'staging-release' ? 'staging' : 'main'
|
|
69
93
|
const temporary = mkdtempSync(join(tmpdir(), 'code-foundry-fleet-'))
|
|
70
94
|
try {
|
|
71
|
-
const fetch = spawnSync('git', ['-C', repository.path, 'fetch', 'origin', base, '--quiet'], {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
const fetch = spawnSync('git', ['-C', repository.path, 'fetch', 'origin', base, '--quiet'], {
|
|
96
|
+
encoding: 'utf8',
|
|
97
|
+
})
|
|
98
|
+
if (fetch.status !== 0)
|
|
99
|
+
return {
|
|
100
|
+
path: repository.path,
|
|
101
|
+
status: 'skipped',
|
|
102
|
+
reason: fetch.stderr.trim() || `unable to refresh ${base} baseline`,
|
|
103
|
+
}
|
|
104
|
+
const baseline = spawnSync('git', ['-C', repository.path, 'rev-parse', `origin/${base}`], {
|
|
105
|
+
encoding: 'utf8',
|
|
106
|
+
})
|
|
107
|
+
if (baseline.status !== 0)
|
|
108
|
+
return {
|
|
109
|
+
path: repository.path,
|
|
110
|
+
status: 'skipped',
|
|
111
|
+
reason: `remote ${base} branch is unavailable`,
|
|
112
|
+
}
|
|
113
|
+
const add = spawnSync(
|
|
114
|
+
'git',
|
|
115
|
+
['-C', repository.path, 'worktree', 'add', '-b', branch, temporary, baseline.stdout.trim()],
|
|
116
|
+
{ encoding: 'utf8' }
|
|
117
|
+
)
|
|
118
|
+
if (add.status !== 0)
|
|
119
|
+
return {
|
|
120
|
+
path: repository.path,
|
|
121
|
+
status: 'skipped',
|
|
122
|
+
reason: add.stderr.trim() || 'unable to create isolated worktree',
|
|
123
|
+
}
|
|
77
124
|
const result = syncRepository({ target: temporary, source, force: false })
|
|
78
125
|
const configFile = join(temporary, '.github/code-foundry.yml')
|
|
79
126
|
if (existsSync(configFile)) {
|
|
@@ -83,21 +130,62 @@ function upgradeRepository(repository, source, version) {
|
|
|
83
130
|
: `${current.trimEnd()}\nruntime_ref: ${version}\n`
|
|
84
131
|
if (updated !== current) {
|
|
85
132
|
writeFileSync(configFile, updated)
|
|
86
|
-
if (!result.changed.includes('.github/code-foundry.yml'))
|
|
133
|
+
if (!result.changed.includes('.github/code-foundry.yml'))
|
|
134
|
+
result.changed.push('.github/code-foundry.yml')
|
|
87
135
|
}
|
|
88
136
|
}
|
|
89
137
|
if (!result.changed.length) return { path: repository.path, status: 'unchanged', branch }
|
|
90
138
|
spawnSync('git', ['-C', temporary, 'add', '-A'], { stdio: 'ignore' })
|
|
91
|
-
const commit = spawnSync(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
139
|
+
const commit = spawnSync(
|
|
140
|
+
'git',
|
|
141
|
+
['-C', temporary, 'commit', '-m', `chore(code-foundry): upgrade runtime to ${version}`],
|
|
142
|
+
{ encoding: 'utf8' }
|
|
143
|
+
)
|
|
144
|
+
if (commit.status !== 0)
|
|
145
|
+
return {
|
|
146
|
+
path: repository.path,
|
|
147
|
+
status: 'failed',
|
|
148
|
+
reason: commit.stderr.trim() || 'commit failed',
|
|
149
|
+
}
|
|
150
|
+
const push = spawnSync('git', ['-C', temporary, 'push', '-u', 'origin', branch], {
|
|
151
|
+
encoding: 'utf8',
|
|
152
|
+
})
|
|
153
|
+
if (push.status !== 0)
|
|
154
|
+
return {
|
|
155
|
+
path: repository.path,
|
|
156
|
+
status: 'failed',
|
|
157
|
+
reason: push.stderr.trim() || 'push failed',
|
|
158
|
+
}
|
|
159
|
+
const pr = spawnSync(
|
|
160
|
+
'gh',
|
|
161
|
+
[
|
|
162
|
+
'pr',
|
|
163
|
+
'create',
|
|
164
|
+
'--repo',
|
|
165
|
+
repository.repository,
|
|
166
|
+
'--base',
|
|
167
|
+
base,
|
|
168
|
+
'--head',
|
|
169
|
+
branch,
|
|
170
|
+
'--title',
|
|
171
|
+
`chore(code-foundry): upgrade to ${version}`,
|
|
172
|
+
'--body',
|
|
173
|
+
`Automated isolated Code Foundry runtime upgrade to ${version}.\n\nThe sync preserved protected repository-owned documents and custom workflows.`,
|
|
174
|
+
],
|
|
175
|
+
{ encoding: 'utf8' }
|
|
176
|
+
)
|
|
96
177
|
return pr.status === 0
|
|
97
178
|
? { path: repository.path, status: 'pr-created', branch, pullRequest: pr.stdout.trim() }
|
|
98
|
-
: {
|
|
179
|
+
: {
|
|
180
|
+
path: repository.path,
|
|
181
|
+
status: 'failed',
|
|
182
|
+
branch,
|
|
183
|
+
reason: pr.stderr.trim() || 'pull request creation failed',
|
|
184
|
+
}
|
|
99
185
|
} finally {
|
|
100
|
-
spawnSync('git', ['-C', repository.path, 'worktree', 'remove', '--force', temporary], {
|
|
186
|
+
spawnSync('git', ['-C', repository.path, 'worktree', 'remove', '--force', temporary], {
|
|
187
|
+
stdio: 'ignore',
|
|
188
|
+
})
|
|
101
189
|
}
|
|
102
190
|
}
|
|
103
191
|
|
|
@@ -105,19 +193,34 @@ function upgradeRepository(repository, source, version) {
|
|
|
105
193
|
function children(root) {
|
|
106
194
|
try {
|
|
107
195
|
return readdirSync(root, { withFileTypes: true })
|
|
108
|
-
.filter(
|
|
196
|
+
.filter(
|
|
197
|
+
(entry) =>
|
|
198
|
+
entry.isDirectory() &&
|
|
199
|
+
!entry.name.startsWith('.') &&
|
|
200
|
+
!['node_modules', 'target'].includes(entry.name)
|
|
201
|
+
)
|
|
109
202
|
.map((entry) => join(root, entry.name))
|
|
110
|
-
} catch {
|
|
203
|
+
} catch {
|
|
204
|
+
return []
|
|
205
|
+
}
|
|
111
206
|
}
|
|
112
207
|
|
|
113
208
|
/** @param {string} file @returns {Record<string, string>} */
|
|
114
209
|
function readConfig(file) {
|
|
115
210
|
try {
|
|
116
|
-
return Object.fromEntries(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
211
|
+
return Object.fromEntries(
|
|
212
|
+
requireText(file)
|
|
213
|
+
.split(/\r?\n/)
|
|
214
|
+
.flatMap((line) => {
|
|
215
|
+
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*?)\s*$/)
|
|
216
|
+
return match
|
|
217
|
+
? [[match[1], match[2].replace(/\s+#.*$/, '').replace(/^['"]|['"]$/g, '')]]
|
|
218
|
+
: []
|
|
219
|
+
})
|
|
220
|
+
)
|
|
221
|
+
} catch {
|
|
222
|
+
return {}
|
|
223
|
+
}
|
|
121
224
|
}
|
|
122
225
|
|
|
123
226
|
/** @param {string} file */
|
|
@@ -126,7 +229,14 @@ function requireText(file) {
|
|
|
126
229
|
}
|
|
127
230
|
|
|
128
231
|
/** @param {string} root @param {string[]} args */
|
|
129
|
-
function git(root, args) {
|
|
232
|
+
function git(root, args) {
|
|
233
|
+
return spawnSync('git', args, { cwd: root, encoding: 'utf8' }).stdout?.trim() ?? ''
|
|
234
|
+
}
|
|
130
235
|
|
|
131
236
|
/** @param {string} remote */
|
|
132
|
-
function normalizeRemote(remote) {
|
|
237
|
+
function normalizeRemote(remote) {
|
|
238
|
+
return remote
|
|
239
|
+
.replace(/^git@github\.com:/, '')
|
|
240
|
+
.replace(/^https?:\/\/github\.com\//, '')
|
|
241
|
+
.replace(/\.git$/, '')
|
|
242
|
+
}
|