@markjaquith/agency 2.54.4 → 2.56.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/README.md +68 -1
- package/cli-main.ts +20 -0
- package/package.json +1 -1
- package/src/cli-parser.test.ts +9 -0
- package/src/cli-parser.ts +10 -0
- package/src/cli.test.ts +1 -0
- package/src/commands/push.ts +25 -0
- package/src/protocol.ts +1 -0
- package/src/services/DoctorService.ts +12 -0
- package/src/services/IntegrationService.test.ts +2 -0
- package/src/services/PushService.test.ts +516 -0
- package/src/services/PushService.ts +576 -0
- package/src/services/WorkbaseService.test.ts +24 -0
- package/src/services/WorkbaseService.ts +18 -0
- package/src/services/WorktreeService.test.ts +383 -1
- package/src/services/WorktreeService.ts +232 -4
- package/src/test-utils.ts +2 -0
- package/src/workbase/AGENTS.md +5 -0
- package/src/workbase/checkout-command.test.ts +62 -0
- package/src/workbase/checkout-command.ts +66 -0
- package/src/workbase/schemas.test.ts +34 -0
- package/src/workbase/schemas.ts +1 -0
|
@@ -8,7 +8,12 @@ import {
|
|
|
8
8
|
expandWorktreeCreateCommand,
|
|
9
9
|
worktreeCommandEnvironment,
|
|
10
10
|
} from "../workbase/worktree-command"
|
|
11
|
-
import
|
|
11
|
+
import {
|
|
12
|
+
expandPostCheckoutCommand,
|
|
13
|
+
postCheckoutCommandEnvironment,
|
|
14
|
+
type CheckoutCommandVariables,
|
|
15
|
+
} from "../workbase/checkout-command"
|
|
16
|
+
import type { RepositoryReference, WorkbaseConfig } from "../workbase/schemas"
|
|
12
17
|
import type { BaseCommandOptions } from "../utils/command"
|
|
13
18
|
import { createLoggers } from "../utils/effect"
|
|
14
19
|
import { withWorktreeLocks } from "./WorktreeLock"
|
|
@@ -35,6 +40,7 @@ interface WorkspaceOperation {
|
|
|
35
40
|
| "create-branch"
|
|
36
41
|
| "create-worktree"
|
|
37
42
|
| "create-workspace"
|
|
43
|
+
| "post-checkout"
|
|
38
44
|
readonly repo: string
|
|
39
45
|
readonly command: readonly string[]
|
|
40
46
|
readonly status: "planned" | "completed"
|
|
@@ -176,6 +182,70 @@ const formatCommand = (args: readonly string[]) =>
|
|
|
176
182
|
)
|
|
177
183
|
.join(" ")
|
|
178
184
|
|
|
185
|
+
const runPostCheckoutHook = (options: {
|
|
186
|
+
readonly command: readonly string[] | undefined
|
|
187
|
+
readonly variables: CheckoutCommandVariables
|
|
188
|
+
readonly dryRun: boolean
|
|
189
|
+
readonly forwardOutput: boolean
|
|
190
|
+
readonly verboseLog: (...args: unknown[]) => void
|
|
191
|
+
readonly operations: WorkspaceOperation[]
|
|
192
|
+
}) =>
|
|
193
|
+
Effect.gen(function* () {
|
|
194
|
+
if (!options.command) return
|
|
195
|
+
let command: string[]
|
|
196
|
+
try {
|
|
197
|
+
command = expandPostCheckoutCommand(options.command, options.variables)
|
|
198
|
+
} catch (cause) {
|
|
199
|
+
return yield* new WorktreeError({
|
|
200
|
+
message:
|
|
201
|
+
cause instanceof Error
|
|
202
|
+
? cause.message
|
|
203
|
+
: "Invalid postCheckoutCommand",
|
|
204
|
+
cause,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
options.verboseLog(
|
|
208
|
+
`${options.dryRun ? "Planning" : "Running"} post-checkout command for '${options.variables.repoAlias}': ${formatCommand(command)}`,
|
|
209
|
+
)
|
|
210
|
+
if (options.dryRun) {
|
|
211
|
+
options.operations.push({
|
|
212
|
+
action: "post-checkout",
|
|
213
|
+
repo: options.variables.repoAlias,
|
|
214
|
+
command,
|
|
215
|
+
status: "planned",
|
|
216
|
+
})
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
const result = yield* FileSystemService.pipe(
|
|
220
|
+
Effect.flatMap((fs) =>
|
|
221
|
+
fs.runCommand(command, {
|
|
222
|
+
cwd: options.variables.checkoutPath,
|
|
223
|
+
captureOutput: true,
|
|
224
|
+
forwardOutput: options.forwardOutput,
|
|
225
|
+
env: postCheckoutCommandEnvironment(options.variables),
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
Effect.mapError(
|
|
229
|
+
(cause) =>
|
|
230
|
+
new WorktreeError({
|
|
231
|
+
message: `Failed to start post-checkout command for '${options.variables.repoAlias}': ${cause.message}`,
|
|
232
|
+
cause,
|
|
233
|
+
}),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
if (result.exitCode !== 0) {
|
|
237
|
+
return yield* new WorktreeError({
|
|
238
|
+
message: `Post-checkout command failed for '${options.variables.repoAlias}' with exit code ${result.exitCode}${result.stderr ? `: ${result.stderr}` : ""}`,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
options.operations.push({
|
|
242
|
+
action: "post-checkout",
|
|
243
|
+
repo: options.variables.repoAlias,
|
|
244
|
+
command,
|
|
245
|
+
status: "completed",
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
179
249
|
const isCommitId = (ref: string) => /^[0-9a-f]{40,64}$/i.test(ref)
|
|
180
250
|
|
|
181
251
|
const originRef = (ref: string) =>
|
|
@@ -812,6 +882,7 @@ const materializeJj = (options: {
|
|
|
812
882
|
| { readonly repo: string; readonly branch: string }
|
|
813
883
|
| RepositoryReference
|
|
814
884
|
)[]
|
|
885
|
+
readonly config: WorkbaseConfig
|
|
815
886
|
readonly commandOptions: MaterializeOptions
|
|
816
887
|
}) =>
|
|
817
888
|
Effect.gen(function* () {
|
|
@@ -826,6 +897,11 @@ const materializeJj = (options: {
|
|
|
826
897
|
workspaceName: string
|
|
827
898
|
}[] = []
|
|
828
899
|
const base = "base" in options.execution ? options.execution.base : null
|
|
900
|
+
const { verboseLog } = createLoggers(options.commandOptions)
|
|
901
|
+
const forwardCommandOutput =
|
|
902
|
+
options.commandOptions.verbose === true &&
|
|
903
|
+
!options.commandOptions.silent &&
|
|
904
|
+
!options.commandOptions.json
|
|
829
905
|
|
|
830
906
|
if (!options.commandOptions.dryRun)
|
|
831
907
|
yield* fs.createDirectory(options.codePath)
|
|
@@ -920,7 +996,37 @@ const materializeJj = (options: {
|
|
|
920
996
|
...("branch" in checkout ? { branch: checkout.branch } : {}),
|
|
921
997
|
})
|
|
922
998
|
created.push({ repositoryPath, workspacePath, workspaceName })
|
|
999
|
+
const canonicalWorkspacePath = yield* fs.realPath(workspacePath)
|
|
1000
|
+
const registeredAfterCreate = (yield* backend.listWorkspaces(
|
|
1001
|
+
repositoryPath,
|
|
1002
|
+
)).find((workspace) => workspace.path === canonicalWorkspacePath)
|
|
1003
|
+
const head = yield* backend.workspaceHead(workspacePath)
|
|
1004
|
+
if (!registeredAfterCreate || head !== revision) {
|
|
1005
|
+
return yield* new WorktreeError({
|
|
1006
|
+
message: `Created jj workspace for '${checkout.repo}' failed validation`,
|
|
1007
|
+
})
|
|
1008
|
+
}
|
|
923
1009
|
}
|
|
1010
|
+
yield* runPostCheckoutHook({
|
|
1011
|
+
command:
|
|
1012
|
+
options.config.repositories?.[checkout.repo]?.postCheckoutCommand,
|
|
1013
|
+
variables: {
|
|
1014
|
+
repoAlias: checkout.repo,
|
|
1015
|
+
repositoryPath,
|
|
1016
|
+
checkoutPath: workspacePath,
|
|
1017
|
+
checkoutKind: "branch" in checkout ? "writable" : "reference",
|
|
1018
|
+
requestedRef: requestedRevision,
|
|
1019
|
+
base: base ?? "",
|
|
1020
|
+
vcs: "jj",
|
|
1021
|
+
workbaseRoot: options.root,
|
|
1022
|
+
taskId: options.taskId,
|
|
1023
|
+
phaseId: options.phaseId ?? "",
|
|
1024
|
+
},
|
|
1025
|
+
dryRun: options.commandOptions.dryRun === true,
|
|
1026
|
+
forwardOutput: forwardCommandOutput,
|
|
1027
|
+
verboseLog,
|
|
1028
|
+
operations,
|
|
1029
|
+
})
|
|
924
1030
|
reports.push({
|
|
925
1031
|
repo: checkout.repo,
|
|
926
1032
|
kind: "branch" in checkout ? "writable" : "reference",
|
|
@@ -957,16 +1063,36 @@ const materializeJj = (options: {
|
|
|
957
1063
|
}).pipe(
|
|
958
1064
|
Effect.catchAll((cause) =>
|
|
959
1065
|
Effect.gen(function* () {
|
|
1066
|
+
const rolledBack: string[] = []
|
|
1067
|
+
const manualRecovery: string[] = []
|
|
960
1068
|
for (const workspace of [...created].reverse()) {
|
|
961
|
-
yield* backend
|
|
1069
|
+
const removed = yield* backend
|
|
962
1070
|
.removeWorkspace({
|
|
963
1071
|
repositoryPath: workspace.repositoryPath,
|
|
964
1072
|
workspacePath: workspace.workspacePath,
|
|
965
1073
|
workspaceName: workspace.workspaceName,
|
|
966
1074
|
})
|
|
967
|
-
.pipe(
|
|
1075
|
+
.pipe(
|
|
1076
|
+
Effect.as(true),
|
|
1077
|
+
Effect.catchAll(() => Effect.succeed(false)),
|
|
1078
|
+
)
|
|
1079
|
+
if (removed)
|
|
1080
|
+
rolledBack.push(`create-workspace ${workspace.workspaceName}`)
|
|
1081
|
+
else
|
|
1082
|
+
manualRecovery.push(
|
|
1083
|
+
`Remove jj workspace ${workspace.workspacePath}`,
|
|
1084
|
+
)
|
|
968
1085
|
}
|
|
969
|
-
return yield*
|
|
1086
|
+
return yield* new WorktreeError({
|
|
1087
|
+
message: `${cause instanceof Error ? cause.message : "Workspace creation failed"}. ${
|
|
1088
|
+
manualRecovery.length
|
|
1089
|
+
? "Some effects require manual recovery"
|
|
1090
|
+
: "Created jj workspaces were rolled back"
|
|
1091
|
+
}`,
|
|
1092
|
+
rolledBack,
|
|
1093
|
+
manualRecovery,
|
|
1094
|
+
cause,
|
|
1095
|
+
})
|
|
970
1096
|
}),
|
|
971
1097
|
),
|
|
972
1098
|
)
|
|
@@ -1245,6 +1371,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1245
1371
|
codePath,
|
|
1246
1372
|
execution,
|
|
1247
1373
|
requestedCheckouts,
|
|
1374
|
+
config,
|
|
1248
1375
|
commandOptions: options,
|
|
1249
1376
|
})
|
|
1250
1377
|
}
|
|
@@ -1695,6 +1822,26 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1695
1822
|
{ captureOutput: true },
|
|
1696
1823
|
)
|
|
1697
1824
|
}
|
|
1825
|
+
yield* runPostCheckoutHook({
|
|
1826
|
+
command:
|
|
1827
|
+
config.repositories?.[alias]?.postCheckoutCommand,
|
|
1828
|
+
variables: {
|
|
1829
|
+
repoAlias: alias,
|
|
1830
|
+
repositoryPath,
|
|
1831
|
+
checkoutPath,
|
|
1832
|
+
checkoutKind: "writable",
|
|
1833
|
+
requestedRef: checkout.branch,
|
|
1834
|
+
base: executionBase,
|
|
1835
|
+
vcs: "git",
|
|
1836
|
+
workbaseRoot: root,
|
|
1837
|
+
taskId,
|
|
1838
|
+
phaseId: phaseId ?? "",
|
|
1839
|
+
},
|
|
1840
|
+
dryRun: true,
|
|
1841
|
+
forwardOutput: false,
|
|
1842
|
+
verboseLog,
|
|
1843
|
+
operations,
|
|
1844
|
+
})
|
|
1698
1845
|
checkoutReports.push({
|
|
1699
1846
|
repo: alias,
|
|
1700
1847
|
kind: "writable",
|
|
@@ -1739,6 +1886,38 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1739
1886
|
["git", "-C", checkoutPath, "rev-parse", "HEAD"],
|
|
1740
1887
|
{ captureOutput: true },
|
|
1741
1888
|
)
|
|
1889
|
+
const currentBranch = yield* fs.runCommand(
|
|
1890
|
+
["git", "-C", checkoutPath, "branch", "--show-current"],
|
|
1891
|
+
{ captureOutput: true },
|
|
1892
|
+
)
|
|
1893
|
+
if (
|
|
1894
|
+
head.exitCode !== 0 ||
|
|
1895
|
+
currentBranch.exitCode !== 0 ||
|
|
1896
|
+
currentBranch.stdout.trim() !== checkout.branch
|
|
1897
|
+
) {
|
|
1898
|
+
return yield* new WorktreeError({
|
|
1899
|
+
message: `Created worktree for '${alias}' failed validation for branch '${checkout.branch}'`,
|
|
1900
|
+
})
|
|
1901
|
+
}
|
|
1902
|
+
yield* runPostCheckoutHook({
|
|
1903
|
+
command: config.repositories?.[alias]?.postCheckoutCommand,
|
|
1904
|
+
variables: {
|
|
1905
|
+
repoAlias: alias,
|
|
1906
|
+
repositoryPath,
|
|
1907
|
+
checkoutPath,
|
|
1908
|
+
checkoutKind: "writable",
|
|
1909
|
+
requestedRef: checkout.branch,
|
|
1910
|
+
base: executionBase,
|
|
1911
|
+
vcs: "git",
|
|
1912
|
+
workbaseRoot: root,
|
|
1913
|
+
taskId,
|
|
1914
|
+
phaseId: phaseId ?? "",
|
|
1915
|
+
},
|
|
1916
|
+
dryRun: false,
|
|
1917
|
+
forwardOutput: forwardCommandOutput,
|
|
1918
|
+
verboseLog,
|
|
1919
|
+
operations,
|
|
1920
|
+
})
|
|
1742
1921
|
checkoutReports.push({
|
|
1743
1922
|
repo: alias,
|
|
1744
1923
|
kind: "writable",
|
|
@@ -1831,6 +2010,26 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1831
2010
|
command,
|
|
1832
2011
|
status: "planned",
|
|
1833
2012
|
})
|
|
2013
|
+
yield* runPostCheckoutHook({
|
|
2014
|
+
command:
|
|
2015
|
+
config.repositories?.[alias]?.postCheckoutCommand,
|
|
2016
|
+
variables: {
|
|
2017
|
+
repoAlias: alias,
|
|
2018
|
+
repositoryPath,
|
|
2019
|
+
checkoutPath,
|
|
2020
|
+
checkoutKind: "reference",
|
|
2021
|
+
requestedRef: checkout.ref,
|
|
2022
|
+
base: executionBase,
|
|
2023
|
+
vcs: "git",
|
|
2024
|
+
workbaseRoot: root,
|
|
2025
|
+
taskId,
|
|
2026
|
+
phaseId: phaseId ?? "",
|
|
2027
|
+
},
|
|
2028
|
+
dryRun: true,
|
|
2029
|
+
forwardOutput: false,
|
|
2030
|
+
verboseLog,
|
|
2031
|
+
operations,
|
|
2032
|
+
})
|
|
1834
2033
|
checkoutReports.push({
|
|
1835
2034
|
repo: alias,
|
|
1836
2035
|
kind: "reference",
|
|
@@ -1855,6 +2054,34 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1855
2054
|
command,
|
|
1856
2055
|
status: "completed",
|
|
1857
2056
|
})
|
|
2057
|
+
const head = yield* fs.runCommand(
|
|
2058
|
+
["git", "-C", checkoutPath, "rev-parse", "HEAD"],
|
|
2059
|
+
{ captureOutput: true },
|
|
2060
|
+
)
|
|
2061
|
+
if (head.exitCode !== 0 || head.stdout.trim() !== commit) {
|
|
2062
|
+
return yield* new WorktreeError({
|
|
2063
|
+
message: `Created reference checkout for '${alias}' failed validation for '${checkout.ref}'`,
|
|
2064
|
+
})
|
|
2065
|
+
}
|
|
2066
|
+
yield* runPostCheckoutHook({
|
|
2067
|
+
command: config.repositories?.[alias]?.postCheckoutCommand,
|
|
2068
|
+
variables: {
|
|
2069
|
+
repoAlias: alias,
|
|
2070
|
+
repositoryPath,
|
|
2071
|
+
checkoutPath,
|
|
2072
|
+
checkoutKind: "reference",
|
|
2073
|
+
requestedRef: checkout.ref,
|
|
2074
|
+
base: executionBase,
|
|
2075
|
+
vcs: "git",
|
|
2076
|
+
workbaseRoot: root,
|
|
2077
|
+
taskId,
|
|
2078
|
+
phaseId: phaseId ?? "",
|
|
2079
|
+
},
|
|
2080
|
+
dryRun: false,
|
|
2081
|
+
forwardOutput: forwardCommandOutput,
|
|
2082
|
+
verboseLog,
|
|
2083
|
+
operations,
|
|
2084
|
+
})
|
|
1858
2085
|
checkoutReports.push({
|
|
1859
2086
|
repo: alias,
|
|
1860
2087
|
kind: "reference",
|
|
@@ -1918,6 +2145,7 @@ export class WorktreeService extends Effect.Service<WorktreeService>()(
|
|
|
1918
2145
|
join(root, "repos", checkout.repo),
|
|
1919
2146
|
"worktree",
|
|
1920
2147
|
"remove",
|
|
2148
|
+
"--force",
|
|
1921
2149
|
checkoutPath,
|
|
1922
2150
|
],
|
|
1923
2151
|
{ captureOutput: true },
|
package/src/test-utils.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { TaskService } from "./services/TaskService"
|
|
|
11
11
|
import { PhaseService } from "./services/PhaseService"
|
|
12
12
|
import { WorktreeService } from "./services/WorktreeService"
|
|
13
13
|
import { PullRequestService } from "./services/PullRequestService"
|
|
14
|
+
import { PushService } from "./services/PushService"
|
|
14
15
|
import { ArchiveService } from "./services/ArchiveService"
|
|
15
16
|
import { IntegrationService } from "./services/IntegrationService"
|
|
16
17
|
import { ContextService } from "./services/ContextService"
|
|
@@ -46,6 +47,7 @@ const TestLayer = Layer.mergeAll(
|
|
|
46
47
|
PhaseService.Default,
|
|
47
48
|
WorktreeService.Default,
|
|
48
49
|
PullRequestService.Default,
|
|
50
|
+
PushService.Default,
|
|
49
51
|
ArchiveService.Default,
|
|
50
52
|
IntegrationService.Default,
|
|
51
53
|
ContextService.Default,
|
package/src/workbase/AGENTS.md
CHANGED
|
@@ -69,6 +69,11 @@ change only the writable checkout, keep durable decisions current, and run the
|
|
|
69
69
|
repository's formatting, type checks, build, dead-code checks, and focused tests.
|
|
70
70
|
Review and commit the diff according to the repository's instructions.
|
|
71
71
|
|
|
72
|
+
Use `agency push` from an execution task or phase to validate and publish its
|
|
73
|
+
declared delivery branch or bookmark without creating a pull request. The
|
|
74
|
+
command never authors semantic commit descriptions; resolve its reported change
|
|
75
|
+
IDs and remediation commands before retrying.
|
|
76
|
+
|
|
72
77
|
`agency work` is the human launch flow: it reconciles managed integration,
|
|
73
78
|
selects work, checks readiness, prepares checkouts, marks execution work
|
|
74
79
|
`working` without creating a claim, and starts the runner. Epic and multi-phase
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
expandPostCheckoutCommand,
|
|
4
|
+
postCheckoutCommandEnvironment,
|
|
5
|
+
} from "./checkout-command"
|
|
6
|
+
|
|
7
|
+
const variables = {
|
|
8
|
+
repoAlias: "app",
|
|
9
|
+
repositoryPath: "/work/repos/app",
|
|
10
|
+
checkoutPath: "/work/tasks/example/code/app",
|
|
11
|
+
checkoutKind: "reference" as const,
|
|
12
|
+
requestedRef: "main",
|
|
13
|
+
base: "",
|
|
14
|
+
vcs: "jj" as const,
|
|
15
|
+
workbaseRoot: "/work",
|
|
16
|
+
taskId: "example",
|
|
17
|
+
phaseId: "",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("post-checkout command templates", () => {
|
|
21
|
+
test("expands all context placeholders without shell interpolation", () => {
|
|
22
|
+
expect(
|
|
23
|
+
expandPostCheckoutCommand(
|
|
24
|
+
[
|
|
25
|
+
"tool",
|
|
26
|
+
"{repoAlias}",
|
|
27
|
+
"{repositoryPath}",
|
|
28
|
+
"{checkoutPath}",
|
|
29
|
+
"{checkoutKind}",
|
|
30
|
+
"{requestedRef}",
|
|
31
|
+
"{base}",
|
|
32
|
+
"{vcs}",
|
|
33
|
+
"{workbaseRoot}",
|
|
34
|
+
"{taskId}",
|
|
35
|
+
"{phaseId}",
|
|
36
|
+
],
|
|
37
|
+
variables,
|
|
38
|
+
),
|
|
39
|
+
).toEqual(["tool", ...Object.values(variables)])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("rejects unknown placeholders", () => {
|
|
43
|
+
expect(() =>
|
|
44
|
+
expandPostCheckoutCommand(["tool", "{unknown}"], variables),
|
|
45
|
+
).toThrow("{unknown}")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("provides matching environment variables with empty optional values", () => {
|
|
49
|
+
expect(postCheckoutCommandEnvironment(variables)).toEqual({
|
|
50
|
+
AGENCY_REPO_ALIAS: variables.repoAlias,
|
|
51
|
+
AGENCY_REPOSITORY_PATH: variables.repositoryPath,
|
|
52
|
+
AGENCY_CHECKOUT_PATH: variables.checkoutPath,
|
|
53
|
+
AGENCY_CHECKOUT_KIND: variables.checkoutKind,
|
|
54
|
+
AGENCY_REQUESTED_REF: variables.requestedRef,
|
|
55
|
+
AGENCY_BASE: "",
|
|
56
|
+
AGENCY_VCS: variables.vcs,
|
|
57
|
+
AGENCY_WORKBASE_ROOT: variables.workbaseRoot,
|
|
58
|
+
AGENCY_TASK_ID: variables.taskId,
|
|
59
|
+
AGENCY_PHASE_ID: "",
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface CheckoutCommandVariables {
|
|
2
|
+
readonly repoAlias: string
|
|
3
|
+
readonly repositoryPath: string
|
|
4
|
+
readonly checkoutPath: string
|
|
5
|
+
readonly checkoutKind: "writable" | "reference"
|
|
6
|
+
readonly requestedRef: string
|
|
7
|
+
readonly base: string
|
|
8
|
+
readonly vcs: "git" | "jj"
|
|
9
|
+
readonly workbaseRoot: string
|
|
10
|
+
readonly taskId: string
|
|
11
|
+
readonly phaseId: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PLACEHOLDERS = new Set<keyof CheckoutCommandVariables>([
|
|
15
|
+
"repoAlias",
|
|
16
|
+
"repositoryPath",
|
|
17
|
+
"checkoutPath",
|
|
18
|
+
"checkoutKind",
|
|
19
|
+
"requestedRef",
|
|
20
|
+
"base",
|
|
21
|
+
"vcs",
|
|
22
|
+
"workbaseRoot",
|
|
23
|
+
"taskId",
|
|
24
|
+
"phaseId",
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
export const validatePostCheckoutCommand = (command: readonly string[]) => {
|
|
28
|
+
for (const argument of command) {
|
|
29
|
+
for (const match of argument.matchAll(/\{([^{}]+)\}/g)) {
|
|
30
|
+
const placeholder = match[1]!
|
|
31
|
+
if (!PLACEHOLDERS.has(placeholder as keyof CheckoutCommandVariables)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Unknown postCheckoutCommand placeholder: {${placeholder}}`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const expandPostCheckoutCommand = (
|
|
41
|
+
command: readonly string[],
|
|
42
|
+
variables: CheckoutCommandVariables,
|
|
43
|
+
): string[] => {
|
|
44
|
+
validatePostCheckoutCommand(command)
|
|
45
|
+
|
|
46
|
+
return command.map((argument) =>
|
|
47
|
+
argument.replaceAll(/\{([^{}]+)\}/g, (match, placeholder: string) => {
|
|
48
|
+
return variables[placeholder as keyof CheckoutCommandVariables] ?? match
|
|
49
|
+
}),
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const postCheckoutCommandEnvironment = (
|
|
54
|
+
variables: CheckoutCommandVariables,
|
|
55
|
+
): Record<string, string> => ({
|
|
56
|
+
AGENCY_REPO_ALIAS: variables.repoAlias,
|
|
57
|
+
AGENCY_REPOSITORY_PATH: variables.repositoryPath,
|
|
58
|
+
AGENCY_CHECKOUT_PATH: variables.checkoutPath,
|
|
59
|
+
AGENCY_CHECKOUT_KIND: variables.checkoutKind,
|
|
60
|
+
AGENCY_REQUESTED_REF: variables.requestedRef,
|
|
61
|
+
AGENCY_BASE: variables.base,
|
|
62
|
+
AGENCY_VCS: variables.vcs,
|
|
63
|
+
AGENCY_WORKBASE_ROOT: variables.workbaseRoot,
|
|
64
|
+
AGENCY_TASK_ID: variables.taskId,
|
|
65
|
+
AGENCY_PHASE_ID: variables.phaseId,
|
|
66
|
+
})
|
|
@@ -187,6 +187,40 @@ describe("body-of-work descriptions", () => {
|
|
|
187
187
|
})
|
|
188
188
|
})
|
|
189
189
|
|
|
190
|
+
describe("repository post-checkout configuration", () => {
|
|
191
|
+
test("accepts a per-repository argv command", () => {
|
|
192
|
+
const config = Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
193
|
+
version: 2,
|
|
194
|
+
repositories: {
|
|
195
|
+
agency: {
|
|
196
|
+
remote: "https://example.com/agency.git",
|
|
197
|
+
postCheckoutCommand: ["bun", "install", "--frozen-lockfile"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
expect(config.repositories?.agency?.postCheckoutCommand).toEqual([
|
|
203
|
+
"bun",
|
|
204
|
+
"install",
|
|
205
|
+
"--frozen-lockfile",
|
|
206
|
+
])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test("rejects shell strings in place of argv arrays", () => {
|
|
210
|
+
expect(() =>
|
|
211
|
+
Schema.decodeUnknownSync(WorkbaseConfig)({
|
|
212
|
+
version: 2,
|
|
213
|
+
repositories: {
|
|
214
|
+
agency: {
|
|
215
|
+
remote: "https://example.com/agency.git",
|
|
216
|
+
postCheckoutCommand: "bun install",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
).toThrow()
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
|
|
190
224
|
describe("runner configuration", () => {
|
|
191
225
|
test("accepts named argv commands with resume commands and environment", () => {
|
|
192
226
|
const config = Schema.decodeUnknownSync(WorkbaseConfig)({
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -24,6 +24,7 @@ export const RepositoryRemote = NonEmptyString.pipe(
|
|
|
24
24
|
|
|
25
25
|
export const RepositoryDeclaration = Schema.Struct({
|
|
26
26
|
remote: RepositoryRemote,
|
|
27
|
+
postCheckoutCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
27
28
|
})
|
|
28
29
|
|
|
29
30
|
export const RepositoryReference = Schema.Struct({
|