@defrex/autobuild 0.12.0 → 0.14.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 +1 -1
- package/SPEC.md +138 -16
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +155 -73
- package/docs/configuration.md +41 -9
- package/docs/hosted-service.md +31 -5
- package/docs/operator-api.md +8 -1
- package/docs/remote-store-protocol.md +94 -2
- package/docs/remote-ticket-protocol.md +151 -9
- package/docs/setup.md +33 -7
- package/docs/ticket-migration.md +64 -0
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +23 -16
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/connect.ts +141 -38
- package/packages/core/src/cli/context.ts +23 -79
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +304 -82
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +84 -42
- package/packages/core/src/cli/help.ts +14 -8
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +23 -5
- package/packages/core/src/cli/mcp.ts +2 -1
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/self-update.ts +5 -1
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +166 -36
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/config/schema.ts +35 -3
- package/packages/core/src/connection.ts +3 -3
- package/packages/core/src/events/catalog.ts +3 -2
- package/packages/core/src/events/payloads.ts +6 -0
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +38 -4
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/build-log.ts +1 -1
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +139 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/ticket-source-backend.ts +17 -21
- package/packages/core/src/operator/tickets.ts +132 -22
- package/packages/core/src/plugin-sdk/index.ts +19 -0
- package/packages/core/src/plugins/runtimes.ts +5 -1
- package/packages/core/src/ports/runner/session-env.ts +2 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +48 -12
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
- package/packages/core/src/ports/tickets/remote.ts +60 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +1 -0
- package/packages/core/src/ports/workspace/create.ts +5 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +223 -205
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +30 -4
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +58 -74
- package/packages/core/src/processes/harvest.ts +72 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1099
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +283 -1
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +180 -55
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +40 -3
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +97 -12
- package/skills/guide/references/operator-api.md +8 -1
- package/skills/guide/references/remote-store-protocol.md +94 -2
- package/skills/guide/references/remote-ticket-protocol.md +151 -9
- package/skills/guide/references/setup.md +33 -7
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { readBuildFacts } from '../store/reduced-reads'
|
|
2
|
+
import { exportTickets, importTickets } from '../ports/tickets/migration'
|
|
3
|
+
import { composeTicketAssets } from '../store/ticket-assets'
|
|
4
|
+
import { editTicket, type TicketEditorRunner } from './ticket-editor'
|
|
1
5
|
/**
|
|
2
6
|
* Source-agnostic pre-build ticket operations (SPEC §8.8). These commands run
|
|
3
7
|
* outside build sessions and resolve the repository's configured TicketSource,
|
|
@@ -19,8 +23,13 @@ import {
|
|
|
19
23
|
ticketListCriteria,
|
|
20
24
|
updateTicket,
|
|
21
25
|
} from '../ports/tickets/operations'
|
|
22
|
-
import type {
|
|
23
|
-
|
|
26
|
+
import type {
|
|
27
|
+
TicketAssetStore,
|
|
28
|
+
Ticket,
|
|
29
|
+
TicketSource,
|
|
30
|
+
TicketStateInfo,
|
|
31
|
+
TicketUpdate,
|
|
32
|
+
} from '../ports/types'
|
|
24
33
|
import type { BuildStore } from '../store/types'
|
|
25
34
|
import { spawnExec, type Exec } from '../ports/workspace/git-worktree'
|
|
26
35
|
import type { TicketAssetSummary } from '../store/ticket-assets'
|
|
@@ -45,6 +54,8 @@ export type TicketSourceFactory = (
|
|
|
45
54
|
|
|
46
55
|
export interface TicketCommandOpts {
|
|
47
56
|
targetRepo: string
|
|
57
|
+
expectedRevision?: string
|
|
58
|
+
editorRunner?: TicketEditorRunner
|
|
48
59
|
/** Process environment — store selection and adapter secrets. */
|
|
49
60
|
env: Record<string, string | undefined>
|
|
50
61
|
/** Git seam supplied by the CLI; omitted direct callers use targetRepo. */
|
|
@@ -152,10 +163,13 @@ export interface TicketStateCreateOpts extends TicketCommandOpts {
|
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
type TicketCommandName =
|
|
166
|
+
| 'export'
|
|
167
|
+
| 'import'
|
|
155
168
|
| 'states'
|
|
156
169
|
| 'state create'
|
|
157
170
|
| 'create'
|
|
158
171
|
| 'update'
|
|
172
|
+
| 'edit'
|
|
159
173
|
| 'block'
|
|
160
174
|
| 'unblock'
|
|
161
175
|
| 'list'
|
|
@@ -170,12 +184,11 @@ export interface ResolvedTicketCommand {
|
|
|
170
184
|
source: TicketSource
|
|
171
185
|
}
|
|
172
186
|
|
|
173
|
-
/** Resolve
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
opts: TicketCommandOpts & { storeRef?: string },
|
|
187
|
+
/** Resolve the main checkout and config without requiring provider credentials. */
|
|
188
|
+
async function resolveTicketConfig(
|
|
189
|
+
opts: TicketCommandOpts,
|
|
177
190
|
label: string,
|
|
178
|
-
): Promise<
|
|
191
|
+
): Promise<{ targetRepo: string; config: Config }> {
|
|
179
192
|
const targetRepo =
|
|
180
193
|
opts.exec === undefined
|
|
181
194
|
? resolve(opts.targetRepo)
|
|
@@ -194,6 +207,21 @@ export async function openTicketSource(
|
|
|
194
207
|
throw error
|
|
195
208
|
}
|
|
196
209
|
|
|
210
|
+
return { targetRepo, config }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Resolve configuration and construct the configured ticket adapter. */
|
|
214
|
+
export async function openTicketSource(
|
|
215
|
+
opts: TicketCommandOpts & { storeRef?: string },
|
|
216
|
+
label: string,
|
|
217
|
+
): Promise<ResolvedTicketCommand> {
|
|
218
|
+
return createTicketCommandSource(opts, await resolveTicketConfig(opts, label))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function createTicketCommandSource(
|
|
222
|
+
opts: TicketCommandOpts,
|
|
223
|
+
{ targetRepo, config }: { targetRepo: string; config: Config },
|
|
224
|
+
): Promise<ResolvedTicketCommand> {
|
|
197
225
|
// Ticket commands use the same trusted plugin catalog as dispatch. Loading
|
|
198
226
|
// and registration finish before any adapter is constructed or called.
|
|
199
227
|
const plugins = await loadPlugins(config.plugins, targetRepo)
|
|
@@ -313,18 +341,18 @@ export async function abTicketUpdate(opts: TicketUpdateOpts): Promise<void> {
|
|
|
313
341
|
...(opts.labels !== undefined ? { labels: [...opts.labels] } : {}),
|
|
314
342
|
}
|
|
315
343
|
if (opts.json === true) {
|
|
316
|
-
const updated = await updateTicket(source, opts.id, patch)
|
|
344
|
+
const updated = await updateTicket(source, opts.id, patch, true, opts.expectedRevision)
|
|
317
345
|
if (updated !== null) emitTicketJson(opts, updated)
|
|
318
346
|
return
|
|
319
347
|
}
|
|
320
|
-
await updateTicket(source, opts.id, patch, false)
|
|
348
|
+
await updateTicket(source, opts.id, patch, false, opts.expectedRevision)
|
|
321
349
|
opts.stdout(`ticket updated: ${source.name}:${opts.id}`)
|
|
322
350
|
}
|
|
323
351
|
|
|
324
352
|
export async function abTicketBlock(opts: TicketBlockerOpts): Promise<void> {
|
|
325
353
|
const { source } = await resolveTicketCommand(opts, 'block')
|
|
326
354
|
const blockerIds = [...new Set(opts.blockerIds)]
|
|
327
|
-
const updated = await changeBlockers(source, opts.id, blockerIds, 'block')
|
|
355
|
+
const updated = await changeBlockers(source, opts.id, blockerIds, 'block', opts.expectedRevision)
|
|
328
356
|
if (opts.json === true) {
|
|
329
357
|
emitTicketJson(opts, updated)
|
|
330
358
|
return
|
|
@@ -337,7 +365,13 @@ export async function abTicketBlock(opts: TicketBlockerOpts): Promise<void> {
|
|
|
337
365
|
export async function abTicketUnblock(opts: TicketBlockerOpts): Promise<void> {
|
|
338
366
|
const { source } = await resolveTicketCommand(opts, 'unblock')
|
|
339
367
|
const blockerIds = [...new Set(opts.blockerIds)]
|
|
340
|
-
const updated = await changeBlockers(
|
|
368
|
+
const updated = await changeBlockers(
|
|
369
|
+
source,
|
|
370
|
+
opts.id,
|
|
371
|
+
blockerIds,
|
|
372
|
+
'unblock',
|
|
373
|
+
opts.expectedRevision,
|
|
374
|
+
)
|
|
341
375
|
if (opts.json === true) {
|
|
342
376
|
emitTicketJson(opts, updated)
|
|
343
377
|
return
|
|
@@ -372,7 +406,7 @@ export async function abTicketShow(opts: TicketShowOpts): Promise<void> {
|
|
|
372
406
|
const { source } = await resolveTicketCommand(opts, 'show')
|
|
373
407
|
const ticket = await requireTicket(source, opts.id)
|
|
374
408
|
// An asset-listing failure fails the command rather than hiding the assets.
|
|
375
|
-
const assets = await
|
|
409
|
+
const assets = await withTicketAssets(opts, source, ({ store, repo }) =>
|
|
376
410
|
store.listTicketAssets(repo, opts.id),
|
|
377
411
|
)
|
|
378
412
|
if (opts.json === true) {
|
|
@@ -399,6 +433,12 @@ export async function abTicketShow(opts: TicketShowOpts): Promise<void> {
|
|
|
399
433
|
// Keep the adapter-provided body untouched. It may itself be multiline and
|
|
400
434
|
// may intentionally end (or not end) with a newline.
|
|
401
435
|
opts.stdout(ticket.body)
|
|
436
|
+
for (const comment of ticket.comments ?? []) {
|
|
437
|
+
opts.stdout(
|
|
438
|
+
`Comment ${comment.createdAt}${comment.author !== undefined ? ` — ${comment.author}` : ''}`,
|
|
439
|
+
)
|
|
440
|
+
opts.stdout(comment.body)
|
|
441
|
+
}
|
|
402
442
|
}
|
|
403
443
|
|
|
404
444
|
function withTicketStore<T>(
|
|
@@ -417,6 +457,19 @@ function withTicketStore<T>(
|
|
|
417
457
|
)
|
|
418
458
|
}
|
|
419
459
|
|
|
460
|
+
function withTicketAssets<T>(
|
|
461
|
+
opts: TicketCommandOpts,
|
|
462
|
+
source: TicketSource,
|
|
463
|
+
use: (context: { store: TicketAssetStore; buildStore: BuildStore; repo: string }) => Promise<T>,
|
|
464
|
+
): Promise<T> {
|
|
465
|
+
if (source.name === 'hosted' && !source.assets) {
|
|
466
|
+
throw new Error('hosted ticket source has no asset capability')
|
|
467
|
+
}
|
|
468
|
+
return withTicketStore(opts, ({ store, repo }) =>
|
|
469
|
+
use({ store: composeTicketAssets(source, store), buildStore: store, repo }),
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
|
|
420
473
|
/** Slugs of this repo's non-terminal builds for the ticket. A build freezes its
|
|
421
474
|
* ticket assets at the claim (SPEC §6.3), so a change to them does not reach
|
|
422
475
|
* it. The notice is advisory: a failed lookup yields none and never blocks. */
|
|
@@ -425,7 +478,7 @@ async function activeBuildsFor(store: BuildStore, repo: string, id: string): Pro
|
|
|
425
478
|
const active: string[] = []
|
|
426
479
|
for (const record of await store.listBuilds()) {
|
|
427
480
|
if (record.repo !== repo || record.ticket?.id !== id) continue
|
|
428
|
-
const { status } =
|
|
481
|
+
const { status } = (await readBuildFacts(store, record.slug)).value.state
|
|
429
482
|
if (status !== 'done' && status !== 'aborted') active.push(record.slug)
|
|
430
483
|
}
|
|
431
484
|
return active
|
|
@@ -455,18 +508,26 @@ export async function abTicketAttach(opts: TicketAttachOpts): Promise<void> {
|
|
|
455
508
|
await requireTicket(source, opts.id)
|
|
456
509
|
const walk = await walkAssetPath(opts.path)
|
|
457
510
|
const name = opts.name ?? walk.basename
|
|
458
|
-
const stored = await
|
|
511
|
+
const stored = await withTicketAssets(opts, source, async ({ store, buildStore, repo }) => {
|
|
459
512
|
const limits = await store.ticketAssetLimits(repo)
|
|
460
513
|
const input = await buildAssetInput(walk, { kind: opts.kind, name }, limits)
|
|
461
514
|
const live = await store.listTicketAssets(repo, opts.id)
|
|
462
515
|
const replacing = live.some((asset) => asset.kind === opts.kind && asset.name === name)
|
|
463
516
|
const meta = await store.putTicketAsset(repo, opts.id, input)
|
|
464
|
-
return { meta, replacing, active: await activeBuildsFor(
|
|
517
|
+
return { meta, replacing, active: await activeBuildsFor(buildStore, repo, opts.id) }
|
|
465
518
|
})
|
|
466
519
|
const { meta } = stored
|
|
467
520
|
warnActiveBuilds(opts, stored.active)
|
|
468
521
|
try {
|
|
469
|
-
await source.comment(
|
|
522
|
+
await source.comment(
|
|
523
|
+
opts.id,
|
|
524
|
+
ticketAssetNote(
|
|
525
|
+
stored.replacing ? 'replaced' : 'attached',
|
|
526
|
+
meta,
|
|
527
|
+
'ab',
|
|
528
|
+
source.name === 'hosted',
|
|
529
|
+
),
|
|
530
|
+
)
|
|
470
531
|
} catch (error) {
|
|
471
532
|
throw new Error(
|
|
472
533
|
`ticket asset ${meta.kind}/${meta.name} was stored as revision ${meta.revision}, but the note on ` +
|
|
@@ -487,9 +548,17 @@ export async function abTicketAttach(opts: TicketAttachOpts): Promise<void> {
|
|
|
487
548
|
/** `ab ticket asset get <id> <kind> <name> <dest>` — exact bytes at the
|
|
488
549
|
* latest or a named revision. */
|
|
489
550
|
export async function abTicketAssetGet(opts: TicketAssetGetOpts): Promise<void> {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
551
|
+
const resolved = await resolveTicketConfig(opts, 'ab ticket asset get')
|
|
552
|
+
const readAsset = ({ store, repo }: { store: TicketAssetStore; repo: string }) =>
|
|
553
|
+
store.getTicketAsset(repo, opts.id, opts.kind, opts.name, opts.rev)
|
|
554
|
+
const asset =
|
|
555
|
+
resolved.config.tickets.source === 'linear'
|
|
556
|
+
? await withTicketStore(opts, readAsset)
|
|
557
|
+
: await withTicketAssets(
|
|
558
|
+
opts,
|
|
559
|
+
(await createTicketCommandSource(opts, resolved)).source,
|
|
560
|
+
readAsset,
|
|
561
|
+
)
|
|
493
562
|
if (asset === null) {
|
|
494
563
|
const revision = opts.rev === undefined ? '' : ` revision ${opts.rev}`
|
|
495
564
|
throw new Error(
|
|
@@ -511,16 +580,23 @@ export async function abTicketAssetGet(opts: TicketAssetGetOpts): Promise<void>
|
|
|
511
580
|
* (earlier revisions stay retrievable) and note the removal on the ticket. */
|
|
512
581
|
export async function abTicketAssetRm(opts: TicketAssetRmOpts): Promise<void> {
|
|
513
582
|
const { source } = await resolveTicketCommand(opts, 'asset rm')
|
|
514
|
-
const { removed, active } = await
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
583
|
+
const { removed, active } = await withTicketAssets(
|
|
584
|
+
opts,
|
|
585
|
+
source,
|
|
586
|
+
async ({ store, buildStore, repo }) => ({
|
|
587
|
+
removed: await store.removeTicketAsset(repo, opts.id, opts.kind, opts.name),
|
|
588
|
+
active: await activeBuildsFor(buildStore, repo, opts.id),
|
|
589
|
+
}),
|
|
590
|
+
)
|
|
518
591
|
if (removed === null) {
|
|
519
592
|
throw new Error(`ticket ${opts.id} has no current asset ${opts.kind}/${opts.name} to remove`)
|
|
520
593
|
}
|
|
521
594
|
warnActiveBuilds(opts, active)
|
|
522
595
|
try {
|
|
523
|
-
await source.comment(
|
|
596
|
+
await source.comment(
|
|
597
|
+
opts.id,
|
|
598
|
+
ticketAssetNote('removed', removed, 'ab', source.name === 'hosted'),
|
|
599
|
+
)
|
|
524
600
|
} catch (error) {
|
|
525
601
|
throw new Error(
|
|
526
602
|
`ticket asset ${opts.kind}/${opts.name} was removed, but the note on ${source.name}:${opts.id} ` +
|
|
@@ -537,7 +613,7 @@ export async function abTicketAssetRm(opts: TicketAssetRmOpts): Promise<void> {
|
|
|
537
613
|
/** `ab ticket move <id> <state>` — adapter-owned state validation and move. */
|
|
538
614
|
export async function abTicketMove(opts: TicketMoveOpts): Promise<void> {
|
|
539
615
|
const { source } = await resolveTicketCommand(opts, 'move')
|
|
540
|
-
const moved = await moveTicket(source, opts.id, opts.state)
|
|
616
|
+
const moved = await moveTicket(source, opts.id, opts.state, opts.expectedRevision)
|
|
541
617
|
if (opts.json === true) {
|
|
542
618
|
opts.stdout(JSON.stringify(moved, null, 2))
|
|
543
619
|
return
|
|
@@ -566,10 +642,10 @@ export function stateRoles(
|
|
|
566
642
|
const named: Array<[TicketStateRole, string]> = [
|
|
567
643
|
['create', tickets.createState ?? 'triage'],
|
|
568
644
|
['ready', tickets.readyState],
|
|
569
|
-
['claimed', 'doing'],
|
|
645
|
+
['claimed', tickets.source === 'hosted' ? (tickets.claimedState ?? 'doing') : 'doing'],
|
|
570
646
|
['triage', triage],
|
|
571
647
|
['proposal', tickets.proposalState ?? triage],
|
|
572
|
-
['done', 'done'],
|
|
648
|
+
['done', tickets.source === 'hosted' ? (tickets.doneState ?? 'done') : 'done'],
|
|
573
649
|
]
|
|
574
650
|
const roles = new Map<string, TicketStateRole[]>(names.map((name) => [name, []]))
|
|
575
651
|
for (const [role, target] of named) {
|
|
@@ -620,7 +696,7 @@ export async function abTicketStates(opts: TicketStatesOpts): Promise<void> {
|
|
|
620
696
|
})
|
|
621
697
|
}
|
|
622
698
|
|
|
623
|
-
/** `ab ticket state create <name> [--about <text>]` —
|
|
699
|
+
/** `ab ticket state create <name> [--about <text>]` — duplicate policy belongs to the source. */
|
|
624
700
|
export async function abTicketStateCreate(opts: TicketStateCreateOpts): Promise<void> {
|
|
625
701
|
const { config, source } = await resolveTicketCommand(opts, 'state create')
|
|
626
702
|
if (typeof source.addState !== 'function') {
|
|
@@ -650,9 +726,11 @@ export async function abTicketStateCreate(opts: TicketStateCreateOpts): Promise<
|
|
|
650
726
|
const CREATE_USAGE =
|
|
651
727
|
'usage: ab ticket create <title> --body <file> [--state <state>] [--labels a,b] [--blocked-by id,id] [--json] (§8.8)'
|
|
652
728
|
const UPDATE_USAGE =
|
|
653
|
-
'usage: ab ticket update <id> [--title <title>] [--body <file>] [--labels a,b] [--json] (§8.8)'
|
|
654
|
-
const BLOCK_USAGE =
|
|
655
|
-
|
|
729
|
+
'usage: ab ticket update <id> [--title <title>] [--body <file>] [--labels a,b] [--expected-revision <opaque>] [--json] (§8.8)'
|
|
730
|
+
const BLOCK_USAGE =
|
|
731
|
+
'usage: ab ticket block <id> <blocker-id[,blocker-id...]> [--expected-revision <opaque>] [--json] (§8.8)'
|
|
732
|
+
const UNBLOCK_USAGE =
|
|
733
|
+
'usage: ab ticket unblock <id> <blocker-id[,blocker-id...]> [--expected-revision <opaque>] [--json] (§8.8)'
|
|
656
734
|
const LIST_USAGE = 'usage: ab ticket list [--state <state>] [--labels a,b] [--json] (§8.8)'
|
|
657
735
|
const SHOW_USAGE = 'usage: ab ticket show <id> [--store <ref>] [--json] (§8.8)'
|
|
658
736
|
const ATTACH_USAGE =
|
|
@@ -661,11 +739,15 @@ const ASSET_GET_USAGE =
|
|
|
661
739
|
'usage: ab ticket asset get <id> <kind> <name> <dest> [--rev <n>] [--store <ref>] [--json] (§8.8)'
|
|
662
740
|
const ASSET_RM_USAGE =
|
|
663
741
|
'usage: ab ticket asset rm <id> <kind> <name> [--store <ref>] [--json] (§8.8)'
|
|
664
|
-
const MOVE_USAGE =
|
|
742
|
+
const MOVE_USAGE =
|
|
743
|
+
'usage: ab ticket move <id> <state> [--expected-revision <opaque>] [--json] (§8.8)'
|
|
665
744
|
const STATES_USAGE = 'usage: ab ticket states [--json] (§8.8)'
|
|
666
745
|
const STATE_CREATE_USAGE = 'usage: ab ticket state create <name> [--about <text>] [--json] (§8.8)'
|
|
667
746
|
export const TICKET_USAGE = [
|
|
747
|
+
'usage: ab ticket export <dir> [--store <ref>]',
|
|
748
|
+
'usage: ab ticket import <dir> [--store <ref>]',
|
|
668
749
|
CREATE_USAGE,
|
|
750
|
+
'usage: ab ticket edit <id> (§8.8)',
|
|
669
751
|
UPDATE_USAGE,
|
|
670
752
|
BLOCK_USAGE,
|
|
671
753
|
UNBLOCK_USAGE,
|
|
@@ -696,6 +778,25 @@ interface TicketCliOpts extends TicketCommandOpts {
|
|
|
696
778
|
export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<void> {
|
|
697
779
|
const [command, ...args] = argv
|
|
698
780
|
switch (command) {
|
|
781
|
+
case 'export':
|
|
782
|
+
case 'import': {
|
|
783
|
+
const parsed = parseArgs(args, { store: 'value' }, TICKET_USAGE)
|
|
784
|
+
const [dir, ...extra] = parsed.positionals
|
|
785
|
+
if (!dir?.trim() || extra.length) throw new Error(TICKET_USAGE)
|
|
786
|
+
const migrationOpts = { ...opts, storeRef: stringFlag(parsed, 'store') ?? opts.storeRef }
|
|
787
|
+
const { source } = await resolveTicketCommand(migrationOpts, command)
|
|
788
|
+
await withTicketAssets(migrationOpts, source, ({ store, repo }) =>
|
|
789
|
+
(command === 'export' ? exportTickets : importTickets)({
|
|
790
|
+
source,
|
|
791
|
+
assets: store,
|
|
792
|
+
repo,
|
|
793
|
+
dir,
|
|
794
|
+
print: opts.stdout,
|
|
795
|
+
}),
|
|
796
|
+
)
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
|
|
699
800
|
case 'create': {
|
|
700
801
|
const parsed = parseArgs(
|
|
701
802
|
args,
|
|
@@ -723,10 +824,23 @@ export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<voi
|
|
|
723
824
|
return
|
|
724
825
|
}
|
|
725
826
|
|
|
827
|
+
case 'edit': {
|
|
828
|
+
if (args.length !== 1 || !args[0]?.trim()) throw new Error('usage: ab ticket edit <id>')
|
|
829
|
+
const { source } = await resolveTicketCommand(opts, 'edit')
|
|
830
|
+
await editTicket(source, args[0], opts.env, opts.stdout, opts.editorRunner)
|
|
831
|
+
return
|
|
832
|
+
}
|
|
833
|
+
|
|
726
834
|
case 'update': {
|
|
727
835
|
const parsed = parseArgs(
|
|
728
836
|
args,
|
|
729
|
-
{
|
|
837
|
+
{
|
|
838
|
+
title: 'value',
|
|
839
|
+
body: 'value',
|
|
840
|
+
labels: 'value',
|
|
841
|
+
'expected-revision': 'value',
|
|
842
|
+
json: 'boolean',
|
|
843
|
+
},
|
|
730
844
|
TICKET_USAGE,
|
|
731
845
|
)
|
|
732
846
|
const [id, ...extra] = parsed.positionals
|
|
@@ -743,6 +857,7 @@ export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<voi
|
|
|
743
857
|
await abTicketUpdate({
|
|
744
858
|
...opts,
|
|
745
859
|
id,
|
|
860
|
+
expectedRevision: stringFlag(parsed, 'expected-revision'),
|
|
746
861
|
...(title !== undefined ? { title } : {}),
|
|
747
862
|
...(bodyFile !== undefined ? { bodyFile } : {}),
|
|
748
863
|
...(labels !== undefined ? { labels: commaList(labels) } : {}),
|
|
@@ -753,7 +868,11 @@ export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<voi
|
|
|
753
868
|
|
|
754
869
|
case 'block':
|
|
755
870
|
case 'unblock': {
|
|
756
|
-
const parsed = parseArgs(
|
|
871
|
+
const parsed = parseArgs(
|
|
872
|
+
args,
|
|
873
|
+
{ json: 'boolean', 'expected-revision': 'value' },
|
|
874
|
+
TICKET_USAGE,
|
|
875
|
+
)
|
|
757
876
|
const [id, blockerList, ...extra] = parsed.positionals
|
|
758
877
|
if (
|
|
759
878
|
id === undefined ||
|
|
@@ -766,7 +885,13 @@ export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<voi
|
|
|
766
885
|
}
|
|
767
886
|
const blockerIds = commaList(blockerList)
|
|
768
887
|
if (blockerIds.length === 0) throw new Error(TICKET_USAGE)
|
|
769
|
-
const blockerOpts = {
|
|
888
|
+
const blockerOpts = {
|
|
889
|
+
...opts,
|
|
890
|
+
id,
|
|
891
|
+
blockerIds,
|
|
892
|
+
expectedRevision: stringFlag(parsed, 'expected-revision'),
|
|
893
|
+
json: parsed.flags.has('json'),
|
|
894
|
+
}
|
|
770
895
|
if (command === 'block') await abTicketBlock(blockerOpts)
|
|
771
896
|
else await abTicketUnblock(blockerOpts)
|
|
772
897
|
return
|
|
@@ -910,7 +1035,11 @@ export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<voi
|
|
|
910
1035
|
}
|
|
911
1036
|
|
|
912
1037
|
case 'move': {
|
|
913
|
-
const parsed = parseArgs(
|
|
1038
|
+
const parsed = parseArgs(
|
|
1039
|
+
args,
|
|
1040
|
+
{ json: 'boolean', 'expected-revision': 'value' },
|
|
1041
|
+
TICKET_USAGE,
|
|
1042
|
+
)
|
|
914
1043
|
const [id, state, ...extra] = parsed.positionals
|
|
915
1044
|
if (
|
|
916
1045
|
id === undefined ||
|
|
@@ -925,6 +1054,7 @@ export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<voi
|
|
|
925
1054
|
...opts,
|
|
926
1055
|
id,
|
|
927
1056
|
state,
|
|
1057
|
+
expectedRevision: stringFlag(parsed, 'expected-revision'),
|
|
928
1058
|
json: parsed.flags.has('json'),
|
|
929
1059
|
})
|
|
930
1060
|
return
|