infra-kit 0.1.102 → 0.1.107
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/.eslintcache +1 -1
- package/.omc/state/agent-replay-0a58307d-2a37-4c69-851c-83a646502d62.jsonl +1 -0
- package/.omc/state/agent-replay-11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc.jsonl +16 -0
- package/.omc/state/agent-replay-4cf1c186-81b2-497c-b002-d7f84e7839f3.jsonl +9 -0
- package/.omc/state/agent-replay-5c4ab554-64f1-42ae-83e3-21e0237e955c.jsonl +11 -0
- package/.omc/state/agent-replay-a60ac2ec-afbd-449f-a540-6df287392fc2.jsonl +1 -0
- package/.omc/state/agent-replay-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
- package/.omc/state/agent-replay-be37e426-6fc8-47f4-8178-221c8494551c.jsonl +3 -0
- package/.omc/state/agent-replay-c967c819-3d1c-447b-ab48-56a8448ef9f8.jsonl +2 -0
- package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
- package/.omc/state/idle-notif-cooldown.json +3 -0
- package/.omc/state/last-tool-error.json +4 -4
- package/.omc/state/mission-state.json +53 -0
- package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/mission-state.json +117 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/pre-tool-advisory-throttle.json +42 -0
- package/.omc/state/sessions/11c41aa0-51fa-49e1-a1dc-26dcd6ac26cc/subagent-tracking-state.json +53 -0
- package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/4cf1c186-81b2-497c-b002-d7f84e7839f3/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/mission-state.json +117 -0
- package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/5c4ab554-64f1-42ae-83e3-21e0237e955c/subagent-tracking-state.json +17 -0
- package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +18 -0
- package/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/mission-state.json +89 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +34 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ralph-state.json +13 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/skill-active-state.json +15 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/subagent-tracking-state.json +35 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ultrawork-state.json +11 -0
- package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +10 -0
- package/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/subagent-tracking-state.json +7 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/pre-tool-advisory-throttle.json +10 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/subagent-tracking-state.json +26 -0
- package/.omc/state/subagent-tracking.json +14 -4
- package/.turbo/turbo-build.log +7 -0
- package/.turbo/turbo-check.log +14 -0
- package/.turbo/turbo-prettier-fix.log +2 -1
- package/.turbo/turbo-test.log +28 -5
- package/.turbo/turbo-validate.log +14 -0
- package/dist/cli.js +88 -74
- package/dist/cli.js.map +4 -4
- package/dist/entry/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/package-config/package-config.d.ts +71 -0
- package/dist/mcp.js +43 -41
- package/dist/mcp.js.map +4 -4
- package/eslint.config.js +1 -1
- package/infra-kit.config.ts +5 -0
- package/package.json +20 -13
- package/scripts/build.js +32 -3
- package/src/.omc/state/sessions/0a58307d-2a37-4c69-851c-83a646502d62/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
- package/src/commands/.omc/state/sessions/c967c819-3d1c-447b-ab48-56a8448ef9f8/pre-tool-advisory-throttle.json +18 -0
- package/src/commands/audit/__tests__/audit.test.ts +59 -0
- package/src/commands/audit/audit.ts +177 -0
- package/src/commands/audit/index.ts +1 -0
- package/src/commands/config/config.ts +49 -7
- package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
- package/src/commands/doctor/doctor.ts +69 -4
- package/src/commands/env-clear/env-clear.ts +1 -1
- package/src/commands/env-list/env-list.ts +3 -3
- package/src/commands/env-load/env-load.ts +1 -1
- package/src/commands/env-status/env-status.ts +1 -1
- package/src/commands/gh-merge-dev/gh-merge-dev.ts +3 -8
- package/src/commands/gh-release-deliver/gh-release-deliver.ts +47 -21
- package/src/commands/gh-release-deploy-all/gh-release-deploy-all.ts +13 -7
- package/src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts +12 -6
- package/src/commands/gh-release-list/gh-release-list.ts +19 -8
- package/src/commands/init/__tests__/agent-files.test.ts +147 -0
- package/src/commands/init/__tests__/migrate-config.test.ts +160 -0
- package/src/commands/init/agent-files.ts +199 -0
- package/src/commands/init/index.ts +7 -0
- package/src/commands/init/init.ts +82 -60
- package/src/commands/init/migrate-config.ts +146 -0
- package/src/commands/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/commands/release-create/__tests__/release-create.test.ts +55 -0
- package/src/commands/release-create/release-create.ts +142 -38
- package/src/commands/release-desc-edit/release-desc-edit.ts +28 -8
- package/src/commands/version/version.ts +1 -1
- package/src/commands/worktrees-add/worktrees-add.ts +7 -12
- package/src/commands/worktrees-list/worktrees-list.ts +13 -5
- package/src/commands/worktrees-open/worktrees-open.ts +1 -1
- package/src/commands/worktrees-remove/worktrees-remove.ts +6 -10
- package/src/commands/worktrees-sync/worktrees-sync.ts +3 -5
- package/src/entry/cli.ts +50 -7
- package/src/entry/index.ts +5 -0
- package/src/integrations/cmux/open-workspace-with-layout.ts +4 -4
- package/src/integrations/cmux/workspace-title.ts +10 -4
- package/src/integrations/doppler/doppler-project.ts +1 -1
- package/src/integrations/gh/gh-release-prs/__tests__/gh-release-prs.test.ts +115 -0
- package/src/integrations/gh/gh-release-prs/gh-release-prs.ts +49 -32
- package/src/lib/.omc/state/sessions/a60ac2ec-afbd-449f-a540-6df287392fc2/pre-tool-advisory-throttle.json +14 -0
- package/src/lib/constants/index.ts +15 -0
- package/src/lib/git-utils/__tests__/git-utils.test.ts +49 -0
- package/src/lib/git-utils/git-utils.ts +3 -1
- package/src/lib/infra-kit-config/__tests__/infra-kit-config.test.ts +270 -0
- package/src/lib/infra-kit-config/index.ts +7 -1
- package/src/lib/infra-kit-config/infra-kit-config.ts +46 -28
- package/src/lib/managed-block/__tests__/managed-block.test.ts +121 -0
- package/src/lib/managed-block/index.ts +8 -0
- package/src/lib/managed-block/managed-block.ts +145 -0
- package/src/lib/package-config/__tests__/package-config.test.ts +95 -0
- package/src/lib/package-config/index.ts +3 -0
- package/src/lib/package-config/package-config-schema.ts +19 -0
- package/src/lib/package-config/package-config.ts +99 -0
- package/src/lib/package-validator/__tests__/package-validator.test.ts +263 -0
- package/src/lib/package-validator/checks/__tests__/checks.test.ts +130 -0
- package/src/lib/package-validator/checks/config-check.ts +30 -0
- package/src/lib/package-validator/checks/files-check.ts +29 -0
- package/src/lib/package-validator/checks/index.ts +4 -0
- package/src/lib/package-validator/checks/scripts-check.ts +23 -0
- package/src/lib/package-validator/checks/turbo-check.ts +47 -0
- package/src/lib/package-validator/fs-utils.ts +18 -0
- package/src/lib/package-validator/index.ts +3 -0
- package/src/lib/package-validator/loader/config-loader.ts +77 -0
- package/src/lib/package-validator/loader/index.ts +2 -0
- package/src/lib/package-validator/loader/package-discovery.ts +98 -0
- package/src/lib/package-validator/package-validator.ts +48 -0
- package/src/lib/package-validator/types.ts +15 -0
- package/src/lib/release-id/__tests__/release-id.test.ts +351 -0
- package/src/lib/release-id/__tests__/versioned-regression.test.ts +69 -0
- package/src/lib/release-id/index.ts +15 -0
- package/src/lib/release-id/release-id.ts +257 -0
- package/src/lib/release-utils/__tests__/release-utils.test.ts +122 -0
- package/src/lib/release-utils/index.ts +4 -0
- package/src/lib/release-utils/release-utils.ts +85 -17
- package/src/lib/version-utils/__tests__/load-existing-versions.test.ts +37 -0
- package/src/lib/version-utils/__tests__/next-version.test.ts +119 -13
- package/src/lib/version-utils/index.ts +3 -0
- package/src/lib/version-utils/load-existing-versions.ts +29 -10
- package/src/lib/version-utils/next-version.ts +67 -12
- package/src/lib/version-utils/version-utils.ts +13 -4
- package/src/mcp/tools/index.ts +2 -0
- package/src/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/src/types.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/src/lib/__tests__/infra-kit-config.test.ts +0 -231
- /package/src/integrations/{clickup → linear}/.gitkeep +0 -0
- /package/src/lib/{__tests__ → constants/__tests__}/constants.test.ts +0 -0
- /package/src/lib/{constants.ts → constants/constants.ts} +0 -0
package/src/entry/cli.ts
CHANGED
|
@@ -2,6 +2,7 @@ import select, { Separator } from '@inquirer/select'
|
|
|
2
2
|
import { Command } from 'commander'
|
|
3
3
|
import process from 'node:process'
|
|
4
4
|
|
|
5
|
+
import { audit } from 'src/commands/audit'
|
|
5
6
|
import { configEdit, configPath } from 'src/commands/config'
|
|
6
7
|
import { doctor } from 'src/commands/doctor'
|
|
7
8
|
import { envClear } from 'src/commands/env-clear'
|
|
@@ -25,6 +26,7 @@ import { worktreesRemove } from 'src/commands/worktrees-remove'
|
|
|
25
26
|
import { worktreesSync } from 'src/commands/worktrees-sync'
|
|
26
27
|
import { logger } from 'src/lib/logger'
|
|
27
28
|
import { parseReleaseSpec } from 'src/lib/version-utils'
|
|
29
|
+
import type { ReleaseInput } from 'src/lib/version-utils'
|
|
28
30
|
|
|
29
31
|
const program = new Command()
|
|
30
32
|
|
|
@@ -92,10 +94,22 @@ program
|
|
|
92
94
|
collectReleaseSpec,
|
|
93
95
|
[],
|
|
94
96
|
)
|
|
97
|
+
.option(
|
|
98
|
+
'-n, --name <name>',
|
|
99
|
+
'Named release (repeatable). Bare kebab-case name, e.g. "checkout-redesign". Creates a regular release; set a description later via release-desc-edit. Can be combined with --release.',
|
|
100
|
+
collectReleaseSpec,
|
|
101
|
+
[],
|
|
102
|
+
)
|
|
95
103
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
96
104
|
.action(async (options) => {
|
|
97
105
|
const specs = options.release as string[]
|
|
98
|
-
const
|
|
106
|
+
const names = options.name as string[]
|
|
107
|
+
const versionInputs: ReleaseInput[] = specs.map(parseReleaseSpec)
|
|
108
|
+
const nameInputs: ReleaseInput[] = names.map((name) => {
|
|
109
|
+
return { name, type: 'regular' as const }
|
|
110
|
+
})
|
|
111
|
+
const combined = [...versionInputs, ...nameInputs]
|
|
112
|
+
const releases = combined.length > 0 ? combined : undefined
|
|
99
113
|
|
|
100
114
|
await releaseCreate({
|
|
101
115
|
releases,
|
|
@@ -106,7 +120,7 @@ program
|
|
|
106
120
|
program
|
|
107
121
|
.command('release-desc-edit')
|
|
108
122
|
.description("Edit a release's description in Jira and in the matching GitHub PR body")
|
|
109
|
-
.option('-v, --version <version>', 'Release version
|
|
123
|
+
.option('-v, --version <version>', 'Release version (e.g. 1.2.5) or release name (e.g. checkout-redesign)')
|
|
110
124
|
.option('-d, --description <description>', 'New description (use "" to clear)')
|
|
111
125
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
112
126
|
.action(async (options) => {
|
|
@@ -120,7 +134,10 @@ program
|
|
|
120
134
|
program
|
|
121
135
|
.command('release-deploy-all')
|
|
122
136
|
.description('Deploy any release branch to any environment')
|
|
123
|
-
.option(
|
|
137
|
+
.option(
|
|
138
|
+
'-v, --version <version>',
|
|
139
|
+
'Version (e.g. 1.2.5) or release name (e.g. checkout-redesign) to deploy; "dev" deploys from the dev branch',
|
|
140
|
+
)
|
|
124
141
|
.option('-e, --env <env>', 'Specify the environment to deploy to, e.g. dev')
|
|
125
142
|
.option('--skip-terraform', 'Skip terraform deployment step')
|
|
126
143
|
.action(async (options) => {
|
|
@@ -130,7 +147,10 @@ program
|
|
|
130
147
|
program
|
|
131
148
|
.command('release-deploy-selected')
|
|
132
149
|
.description('Deploy selected services from release branch to any environment')
|
|
133
|
-
.option(
|
|
150
|
+
.option(
|
|
151
|
+
'-v, --version <version>',
|
|
152
|
+
'Version (e.g. 1.2.5) or release name (e.g. checkout-redesign) to deploy; "dev" deploys from the dev branch',
|
|
153
|
+
)
|
|
134
154
|
.option('-e, --env <env>', 'Specify the environment to deploy to, e.g. dev')
|
|
135
155
|
.option('-s, --services <services...>', 'Specify services to deploy, e.g. client-be client-fe')
|
|
136
156
|
.option('--skip-terraform', 'Skip terraform deployment step')
|
|
@@ -146,7 +166,7 @@ program
|
|
|
146
166
|
program
|
|
147
167
|
.command('release-deliver')
|
|
148
168
|
.description('Release a new version to production')
|
|
149
|
-
.option('-v, --version <version>', '
|
|
169
|
+
.option('-v, --version <version>', 'Version (e.g. 1.2.5) or release name (e.g. checkout-redesign) to deliver')
|
|
150
170
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
151
171
|
.action(async (options) => {
|
|
152
172
|
await ghReleaseDeliver({ version: options.version, confirmedCommand: options.yes })
|
|
@@ -223,6 +243,19 @@ configCmd
|
|
|
223
243
|
await configEdit()
|
|
224
244
|
})
|
|
225
245
|
|
|
246
|
+
program
|
|
247
|
+
.command('audit')
|
|
248
|
+
.description('Audit against infra-kit.config.ts rules (--all for every package, --root for the monorepo root)')
|
|
249
|
+
.option('-a, --all', 'Audit every non-vendor workspace package')
|
|
250
|
+
.option('-r, --root', 'Audit the monorepo root (turbo pipeline + root commands)')
|
|
251
|
+
.action(async (options) => {
|
|
252
|
+
const result = await audit({ all: options.all, root: options.root })
|
|
253
|
+
|
|
254
|
+
if (!result.structuredContent.allPassed) {
|
|
255
|
+
process.exitCode = 1
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
226
259
|
program
|
|
227
260
|
.command('doctor')
|
|
228
261
|
.description('Check installation and authentication status of gh and doppler CLIs')
|
|
@@ -253,7 +286,7 @@ program
|
|
|
253
286
|
|
|
254
287
|
program
|
|
255
288
|
.command('init')
|
|
256
|
-
.description('Inject shell integration into
|
|
289
|
+
.description('Inject shell integration into .zshrc and sync repo agent-instruction files')
|
|
257
290
|
.action(async () => {
|
|
258
291
|
await init()
|
|
259
292
|
})
|
|
@@ -284,7 +317,17 @@ if (process.argv.length <= 2) {
|
|
|
284
317
|
'release-deliver',
|
|
285
318
|
]
|
|
286
319
|
const worktreeCommands = ['worktrees-add', 'worktrees-list', 'worktrees-open', 'worktrees-remove', 'worktrees-sync']
|
|
287
|
-
const envCommands = [
|
|
320
|
+
const envCommands = [
|
|
321
|
+
'audit',
|
|
322
|
+
'doctor',
|
|
323
|
+
'init',
|
|
324
|
+
'version',
|
|
325
|
+
'config',
|
|
326
|
+
'env-status',
|
|
327
|
+
'env-list',
|
|
328
|
+
'env-load',
|
|
329
|
+
'env-clear',
|
|
330
|
+
]
|
|
288
331
|
|
|
289
332
|
const commandMap = new Map(
|
|
290
333
|
program.commands.map((cmd) => {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Public library entry for `import { defineConfig } from 'infra-kit'`. Uses
|
|
2
|
+
// relative imports (no `src/*` alias) so the emitted .d.ts stays portable for
|
|
3
|
+
// external consumers. Keep this surface minimal — only the package-config API.
|
|
4
|
+
export { defineConfig } from '../lib/package-config/package-config'
|
|
5
|
+
export type { InfraKitPackageConfig, InfraKitPackageConfigInput } from '../lib/package-config/package-config'
|
|
@@ -14,7 +14,7 @@ interface OpenCmuxWorkspaceArgs {
|
|
|
14
14
|
export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs): Promise<void> => {
|
|
15
15
|
const { cwd, title } = args
|
|
16
16
|
|
|
17
|
-
const newWorkspaceOutput = (await $`cmux
|
|
17
|
+
const newWorkspaceOutput = (await $`cmux workspace create --cwd ${cwd}`).stdout
|
|
18
18
|
|
|
19
19
|
const workspaceRef = parseWorkspaceRef(newWorkspaceOutput)
|
|
20
20
|
|
|
@@ -26,7 +26,7 @@ export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs):
|
|
|
26
26
|
await $`cmux new-split down --workspace ${workspaceRef} --surface ${leftTopRef}`
|
|
27
27
|
|
|
28
28
|
if (title) {
|
|
29
|
-
await $`cmux
|
|
29
|
+
await $`cmux workspace rename --workspace ${workspaceRef} --title ${title}`
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -51,7 +51,7 @@ const parseFirstSurfaceRef = (output: string): string => {
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Extracts the `workspace:<id>` reference from the output of
|
|
54
|
-
* `cmux
|
|
54
|
+
* `cmux workspace create`. The returned ref is used to target the newly
|
|
55
55
|
* created workspace in follow-up `cmux` commands (splits, rename, etc.).
|
|
56
56
|
*
|
|
57
57
|
* @example
|
|
@@ -62,7 +62,7 @@ const parseWorkspaceRef = (output: string): string => {
|
|
|
62
62
|
const match = output.match(/workspace:\d+/)
|
|
63
63
|
|
|
64
64
|
if (!match) {
|
|
65
|
-
throw new Error('cmux: could not locate workspace ref in
|
|
65
|
+
throw new Error('cmux: could not locate workspace ref in workspace create output')
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
return match[0]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { displayLabel, parseBranchName } from 'src/lib/release-id'
|
|
2
|
+
|
|
1
3
|
interface BuildCmuxWorkspaceTitleArgs {
|
|
2
4
|
repoName: string
|
|
3
5
|
branch: string
|
|
@@ -5,13 +7,17 @@ interface BuildCmuxWorkspaceTitleArgs {
|
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* Builds the cmux workspace title used by `worktrees-add` and looked up by
|
|
8
|
-
* `worktrees-remove`.
|
|
9
|
-
* e.g. `"hulyo-monorepo
|
|
10
|
+
* `worktrees-remove`. Release branches are rendered via their release-id
|
|
11
|
+
* display label so the title reads e.g. `"hulyo-monorepo 1.48.0"` for
|
|
12
|
+
* `"release/v1.48.0"` and `"hulyo-monorepo checkout-redesign"` for
|
|
13
|
+
* `"release/n/checkout-redesign"`. Non-release branches (cmux titles them too)
|
|
14
|
+
* fall back to the raw branch string.
|
|
10
15
|
*/
|
|
11
16
|
export const buildCmuxWorkspaceTitle = (args: BuildCmuxWorkspaceTitleArgs): string => {
|
|
12
17
|
const { repoName, branch } = args
|
|
13
18
|
|
|
14
|
-
const
|
|
19
|
+
const id = parseBranchName(branch)
|
|
20
|
+
const label = id ? displayLabel(id) : branch
|
|
15
21
|
|
|
16
|
-
return `${repoName} ${
|
|
22
|
+
return `${repoName} ${label}`
|
|
17
23
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getInfraKitConfig } from 'src/lib/infra-kit-config'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Resolve Doppler project name from infra-kit.
|
|
4
|
+
* Resolve Doppler project name from infra-kit.json at the project root
|
|
5
5
|
*/
|
|
6
6
|
export const getDopplerProject = async (): Promise<string> => {
|
|
7
7
|
const { envManagement } = await getInfraKitConfig()
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { getReleasePRs, getReleasePRsWithInfo } from '../gh-release-prs'
|
|
4
|
+
|
|
5
|
+
interface FakePR {
|
|
6
|
+
number: number
|
|
7
|
+
title: string
|
|
8
|
+
headRefName: string
|
|
9
|
+
state: string
|
|
10
|
+
baseRefName: string
|
|
11
|
+
createdAt: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const responses = vi.hoisted(() => {
|
|
15
|
+
return { release: [] as FakePR[], hotfix: [] as FakePR[] }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Mock zx's tagged-template `$`: the gh pr list call for `--base dev` returns
|
|
19
|
+
// the "release" set, `--base main` returns the "hotfix" set. The command is
|
|
20
|
+
// reconstructed from the template strings so we can branch on the base flag.
|
|
21
|
+
vi.mock('zx', () => {
|
|
22
|
+
return {
|
|
23
|
+
$: vi.fn((strings: TemplateStringsArray) => {
|
|
24
|
+
const command = strings.join('')
|
|
25
|
+
|
|
26
|
+
if (command.includes('--base main')) {
|
|
27
|
+
return Promise.resolve({ stdout: JSON.stringify(responses.hotfix) })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Promise.resolve({ stdout: JSON.stringify(responses.release) })
|
|
31
|
+
}),
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const pr = (overrides: Partial<FakePR> & Pick<FakePR, 'headRefName' | 'createdAt'>): FakePR => {
|
|
36
|
+
return {
|
|
37
|
+
number: 1,
|
|
38
|
+
title: 'Release',
|
|
39
|
+
state: 'OPEN',
|
|
40
|
+
baseRefName: 'dev',
|
|
41
|
+
...overrides,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('getReleasePRs (discovery + sort)', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
responses.release = []
|
|
48
|
+
responses.hotfix = []
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('sorts version branches first by semver ascending (numeric, 1.9.0 < 1.10.0), then names by createdAt', async () => {
|
|
52
|
+
responses.release = [
|
|
53
|
+
pr({ headRefName: 'release/n/zeta-feature', createdAt: '2026-01-10T00:00:00Z', title: 'Release zeta-feature' }),
|
|
54
|
+
pr({ headRefName: 'release/v1.10.0', createdAt: '2026-01-02T00:00:00Z' }),
|
|
55
|
+
pr({ headRefName: 'release/n/alpha-feature', createdAt: '2026-01-05T00:00:00Z', title: 'Release alpha-feature' }),
|
|
56
|
+
pr({ headRefName: 'release/v1.9.0', createdAt: '2026-01-01T00:00:00Z' }),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
await expect(getReleasePRs()).resolves.toEqual([
|
|
60
|
+
'release/v1.9.0',
|
|
61
|
+
'release/v1.10.0',
|
|
62
|
+
'release/n/alpha-feature',
|
|
63
|
+
'release/n/zeta-feature',
|
|
64
|
+
])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('filters out unparseable junk branches instead of throwing or NaN-sorting', async () => {
|
|
68
|
+
responses.release = [
|
|
69
|
+
pr({ headRefName: 'release/garbage', createdAt: '2026-01-01T00:00:00Z' }),
|
|
70
|
+
pr({ headRefName: 'release/v1.2.3', createdAt: '2026-01-02T00:00:00Z' }),
|
|
71
|
+
pr({ headRefName: 'totally-not-a-release', createdAt: '2026-01-03T00:00:00Z' }),
|
|
72
|
+
pr({
|
|
73
|
+
headRefName: 'release/n/checkout-redesign',
|
|
74
|
+
createdAt: '2026-01-04T00:00:00Z',
|
|
75
|
+
title: 'Release checkout-redesign',
|
|
76
|
+
}),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
await expect(getReleasePRs()).resolves.toEqual(['release/v1.2.3', 'release/n/checkout-redesign'])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('merges hotfix (base main) and release (base dev) sets', async () => {
|
|
83
|
+
responses.release = [pr({ headRefName: 'release/v2.0.0', createdAt: '2026-01-01T00:00:00Z' })]
|
|
84
|
+
responses.hotfix = [
|
|
85
|
+
pr({
|
|
86
|
+
headRefName: 'release/v1.9.9',
|
|
87
|
+
createdAt: '2026-01-02T00:00:00Z',
|
|
88
|
+
baseRefName: 'main',
|
|
89
|
+
title: 'Hotfix v1.9.9',
|
|
90
|
+
}),
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
await expect(getReleasePRs()).resolves.toEqual(['release/v1.9.9', 'release/v2.0.0'])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('getReleasePRsWithInfo (discovery + sort)', () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
responses.release = []
|
|
100
|
+
responses.hotfix = []
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('returns branch/title/createdAt in the locked order with junk filtered', async () => {
|
|
104
|
+
responses.release = [
|
|
105
|
+
pr({ headRefName: 'release/n/beta-feature', createdAt: '2026-02-02T00:00:00Z', title: 'Release beta-feature' }),
|
|
106
|
+
pr({ headRefName: 'release/garbage', createdAt: '2026-02-03T00:00:00Z', title: 'Release garbage' }),
|
|
107
|
+
pr({ headRefName: 'release/v3.1.0', createdAt: '2026-02-01T00:00:00Z', title: 'Release v3.1.0' }),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
await expect(getReleasePRsWithInfo()).resolves.toEqual([
|
|
111
|
+
{ branch: 'release/v3.1.0', title: 'Release v3.1.0', createdAt: '2026-02-01T00:00:00Z' },
|
|
112
|
+
{ branch: 'release/n/beta-feature', title: 'Release beta-feature', createdAt: '2026-02-02T00:00:00Z' },
|
|
113
|
+
])
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -2,9 +2,10 @@ import process from 'node:process'
|
|
|
2
2
|
import { $ } from 'zx'
|
|
3
3
|
|
|
4
4
|
import { logger } from 'src/lib/logger'
|
|
5
|
+
import { compareReleaseIds, formatBranchName, formatPrTitle, parseBranchName } from 'src/lib/release-id'
|
|
6
|
+
import type { ReleaseId } from 'src/lib/release-id'
|
|
5
7
|
import { getBaseBranch } from 'src/lib/release-utils'
|
|
6
8
|
import type { ReleaseType } from 'src/lib/release-utils'
|
|
7
|
-
import { sortVersions } from 'src/lib/version-utils'
|
|
8
9
|
|
|
9
10
|
interface ReleasePR {
|
|
10
11
|
headRefName: string
|
|
@@ -12,11 +13,36 @@ interface ReleasePR {
|
|
|
12
13
|
state: string
|
|
13
14
|
title: string
|
|
14
15
|
baseRefName: string
|
|
16
|
+
createdAt: string
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface ReleasePRInfo {
|
|
18
20
|
branch: string
|
|
19
21
|
title: string
|
|
22
|
+
createdAt: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sort release head refs in the locked deterministic order (versions block
|
|
27
|
+
* first by semver ascending, then names by PR creation date). Head refs that
|
|
28
|
+
* are not valid release branches (parseBranchName → null) are filtered out
|
|
29
|
+
* rather than throwing or NaN-sorting, so a stray junk branch can never break
|
|
30
|
+
* discovery. Carries each PR's createdAt for name ordering.
|
|
31
|
+
*/
|
|
32
|
+
const sortReleasePRs = (prs: ReleasePR[]): ReleasePR[] => {
|
|
33
|
+
return prs
|
|
34
|
+
.map((pr) => {
|
|
35
|
+
return { pr, id: parseBranchName(pr.headRefName) }
|
|
36
|
+
})
|
|
37
|
+
.filter((entry): entry is { pr: ReleasePR; id: NonNullable<typeof entry.id> } => {
|
|
38
|
+
return entry.id !== null
|
|
39
|
+
})
|
|
40
|
+
.sort((a, b) => {
|
|
41
|
+
return compareReleaseIds(a.id, b.id, { a: a.pr.createdAt, b: b.pr.createdAt })
|
|
42
|
+
})
|
|
43
|
+
.map((entry) => {
|
|
44
|
+
return entry.pr
|
|
45
|
+
})
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
/**
|
|
@@ -26,10 +52,10 @@ export interface ReleasePRInfo {
|
|
|
26
52
|
*/
|
|
27
53
|
const fetchAllReleasePRs = async (): Promise<ReleasePR[]> => {
|
|
28
54
|
const releasePRs =
|
|
29
|
-
await $`gh pr list --search "Release in:title" --base dev --json number,title,headRefName,state,baseRefName`
|
|
55
|
+
await $`gh pr list --search "Release in:title" --base dev --json number,title,headRefName,state,baseRefName,createdAt`
|
|
30
56
|
|
|
31
57
|
const hotfixPRs =
|
|
32
|
-
await $`gh pr list --search "Hotfix in:title" --base main --json number,title,headRefName,state,baseRefName`
|
|
58
|
+
await $`gh pr list --search "Hotfix in:title" --base main --json number,title,headRefName,state,baseRefName,createdAt`
|
|
33
59
|
|
|
34
60
|
const all: ReleasePR[] = [...JSON.parse(releasePRs.stdout), ...JSON.parse(hotfixPRs.stdout)]
|
|
35
61
|
|
|
@@ -46,10 +72,12 @@ const fetchAllReleasePRs = async (): Promise<ReleasePR[]> => {
|
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
/**
|
|
49
|
-
* Fetch open release PRs from GitHub with 'Release' or 'Hotfix' in the title
|
|
50
|
-
* Returns an array of headRefName strings
|
|
75
|
+
* Fetch open release PRs from GitHub with 'Release' or 'Hotfix' in the title.
|
|
76
|
+
* Returns an array of headRefName strings in the locked deterministic order
|
|
77
|
+
* (version branches first by semver ascending, then named branches by PR
|
|
78
|
+
* creation date). Unparseable head refs are filtered out.
|
|
51
79
|
*
|
|
52
|
-
* @returns [release/v1.18.22, release/v1.18.23, release/
|
|
80
|
+
* @returns [release/v1.18.22, release/v1.18.23, release/n/checkout-redesign]
|
|
53
81
|
*/
|
|
54
82
|
export const getReleasePRs = async (): Promise<string[]> => {
|
|
55
83
|
try {
|
|
@@ -61,11 +89,9 @@ export const getReleasePRs = async (): Promise<string[]> => {
|
|
|
61
89
|
process.exit(1)
|
|
62
90
|
}
|
|
63
91
|
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}),
|
|
68
|
-
)
|
|
92
|
+
return sortReleasePRs(prs).map((pr) => {
|
|
93
|
+
return pr.headRefName
|
|
94
|
+
})
|
|
69
95
|
} catch (error) {
|
|
70
96
|
logger.error({ error }, '❌ Error fetching release PRs')
|
|
71
97
|
|
|
@@ -75,7 +101,9 @@ export const getReleasePRs = async (): Promise<string[]> => {
|
|
|
75
101
|
|
|
76
102
|
/**
|
|
77
103
|
* Fetch open release PRs with title info (for detecting release type).
|
|
78
|
-
* Returns ReleasePRInfo objects
|
|
104
|
+
* Returns ReleasePRInfo objects in the locked deterministic order (version
|
|
105
|
+
* branches first by semver ascending, then named branches by PR creation
|
|
106
|
+
* date). Unparseable head refs are filtered out.
|
|
79
107
|
*/
|
|
80
108
|
export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
|
|
81
109
|
try {
|
|
@@ -86,21 +114,11 @@ export const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {
|
|
|
86
114
|
process.exit(1)
|
|
87
115
|
}
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
prs.map((pr) => {
|
|
91
|
-
return pr.headRefName
|
|
92
|
-
}),
|
|
93
|
-
)
|
|
94
|
-
const prByBranch = new Map(
|
|
95
|
-
prs.map((pr) => {
|
|
96
|
-
return [pr.headRefName, pr]
|
|
97
|
-
}),
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
return sortedBranches.map((branch) => {
|
|
117
|
+
return sortReleasePRs(prs).map((pr) => {
|
|
101
118
|
return {
|
|
102
|
-
branch,
|
|
103
|
-
title:
|
|
119
|
+
branch: pr.headRefName,
|
|
120
|
+
title: pr.title,
|
|
121
|
+
createdAt: pr.createdAt,
|
|
104
122
|
}
|
|
105
123
|
})
|
|
106
124
|
} catch (error) {
|
|
@@ -131,7 +149,7 @@ export const updateReleasePRBody = async (args: UpdateReleasePRBodyArgs): Promis
|
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
interface CreateReleaseBranchArgs {
|
|
134
|
-
|
|
152
|
+
id: ReleaseId
|
|
135
153
|
jiraVersionUrl: string
|
|
136
154
|
type: ReleaseType
|
|
137
155
|
description?: string
|
|
@@ -141,11 +159,11 @@ interface CreateReleaseBranchArgs {
|
|
|
141
159
|
export const createReleaseBranch = async (
|
|
142
160
|
args: CreateReleaseBranchArgs,
|
|
143
161
|
): Promise<{ branchName: string; prUrl: string }> => {
|
|
144
|
-
const {
|
|
145
|
-
const
|
|
162
|
+
const { id, jiraVersionUrl, type, description } = args
|
|
163
|
+
const prTitle = formatPrTitle(id, type)
|
|
146
164
|
const baseBranch = getBaseBranch(type)
|
|
147
165
|
|
|
148
|
-
const branchName =
|
|
166
|
+
const branchName = formatBranchName(id)
|
|
149
167
|
|
|
150
168
|
const body = description && description.trim() !== '' ? `${jiraVersionUrl}\n\n${description}` : `${jiraVersionUrl} \n`
|
|
151
169
|
|
|
@@ -160,8 +178,7 @@ export const createReleaseBranch = async (
|
|
|
160
178
|
await $`git push origin ${branchName}`
|
|
161
179
|
|
|
162
180
|
// Create PR and capture URL
|
|
163
|
-
const prResult =
|
|
164
|
-
await $`gh pr create --title "${titlePrefix} v${version}" --body ${body} --base ${baseBranch} --head ${branchName}`
|
|
181
|
+
const prResult = await $`gh pr create --title "${prTitle}" --body ${body} --base ${baseBranch} --head ${branchName}`
|
|
165
182
|
|
|
166
183
|
const prLink = prResult.stdout.trim()
|
|
167
184
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"entries": {
|
|
4
|
+
"466399dafa2d20f60587180bad0a07358eb7f2bce724df0ff682f038ad33f5ad": {
|
|
5
|
+
"last_emitted_at_ms": 1780863529171,
|
|
6
|
+
"message": "Read multiple files in parallel when possible for faster analysis."
|
|
7
|
+
},
|
|
8
|
+
"79a93d4a2f8f50b95f852280616242fee1855dc99a3c75211917f55e72e95fae": {
|
|
9
|
+
"last_emitted_at_ms": 1780863517870,
|
|
10
|
+
"message": "Use parallel execution for independent tasks. Use run_in_background for long operations (npm install, builds, tests)."
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"updated_at": "2026-06-07T20:18:49.171Z"
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
atomicWriteFileSync,
|
|
3
|
+
ENV_CLEAR_FILE,
|
|
4
|
+
ENV_LOAD_FILE,
|
|
5
|
+
ENV_VAR_LINE_PATTERN,
|
|
6
|
+
getCacheRoot,
|
|
7
|
+
getSessionCacheDir,
|
|
8
|
+
INFRA_KIT_ENV_CONFIG_VAR,
|
|
9
|
+
INFRA_KIT_ENV_LOADED_AT_VAR,
|
|
10
|
+
INFRA_KIT_ENV_PROJECT_VAR,
|
|
11
|
+
INFRA_KIT_SESSION_VAR,
|
|
12
|
+
LOG_FILE_PATH,
|
|
13
|
+
parseVarNamesFromEnvFile,
|
|
14
|
+
WORKTREES_DIR_SUFFIX,
|
|
15
|
+
} from './constants'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { getCurrentWorktrees } from 'src/lib/git-utils'
|
|
4
|
+
|
|
5
|
+
const worktreeList = vi.hoisted(() => {
|
|
6
|
+
return { stdout: '' }
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
vi.mock('zx', () => {
|
|
10
|
+
return {
|
|
11
|
+
$: vi.fn(() => {
|
|
12
|
+
return Promise.resolve(worktreeList)
|
|
13
|
+
}),
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const asWorktreeLine = (branch: string): string => {
|
|
18
|
+
return `/repos/project-worktrees/${branch} abc1234 [${branch}]`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('getCurrentWorktrees', () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
worktreeList.stdout = [
|
|
24
|
+
asWorktreeLine('main'),
|
|
25
|
+
asWorktreeLine('release/v1.18.22'),
|
|
26
|
+
asWorktreeLine('release/n/checkout-redesign'),
|
|
27
|
+
asWorktreeLine('release/garbage'),
|
|
28
|
+
asWorktreeLine('feature/login-page'),
|
|
29
|
+
'/repos/project abc1234 (bare)',
|
|
30
|
+
'',
|
|
31
|
+
].join('\n')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns versioned AND named release worktrees for type release', async () => {
|
|
35
|
+
await expect(getCurrentWorktrees('release')).resolves.toEqual(['release/v1.18.22', 'release/n/checkout-redesign'])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('excludes junk release branches and non-release branches', async () => {
|
|
39
|
+
const branches = await getCurrentWorktrees('release')
|
|
40
|
+
|
|
41
|
+
expect(branches).not.toContain('release/garbage')
|
|
42
|
+
expect(branches).not.toContain('feature/login-page')
|
|
43
|
+
expect(branches).not.toContain('main')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns feature worktrees for type feature', async () => {
|
|
47
|
+
await expect(getCurrentWorktrees('feature')).resolves.toEqual(['feature/login-page'])
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { $ } from 'zx'
|
|
3
3
|
|
|
4
|
+
import { isReleaseBranch } from 'src/lib/release-id'
|
|
5
|
+
|
|
4
6
|
/**
|
|
5
7
|
* Get current git worktrees
|
|
6
8
|
*
|
|
@@ -60,7 +62,7 @@ const parseWorktreeBranch = (line: string): string | null => {
|
|
|
60
62
|
const releaseWorktreePredicate = (line: string): string | null => {
|
|
61
63
|
const branch = parseWorktreeBranch(line)
|
|
62
64
|
|
|
63
|
-
return branch
|
|
65
|
+
return isReleaseBranch(branch) ? branch : null
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
/**
|