@pmelab/gtd 27.8.0 → 28.0.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/.claude-plugin/plugin.json +1 -1
- package/README.md +59 -30
- package/claude/hooks/doors.ts +12 -0
- package/claude/hooks/register.tsx +2 -4
- package/claude/hooks/ship.ts +115 -20
- package/dist/chunks/{reserved-UdSuHDUJ.mjs → reserved-BIsngMGL.mjs} +168 -27
- package/dist/chunks/{tail-fj60rQ3y.mjs → tail-DHEaOHyp.mjs} +1 -1
- package/dist/chunks/{wire-C12dahrH.mjs → wire-D3Rc5r22.mjs} +14 -1
- package/dist/gtd.bundle.mjs +174 -48
- package/dist/testkit.mjs +158 -49
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +3 -4
- package/dist/workflows/feature.mjs +2 -2
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +19 -4
- package/dist/workflows.mjs +186 -127
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +4 -3
- package/src/cli/Cli.test.ts +39 -3
- package/src/cli/Cli.ts +27 -6
- package/src/cli/Program.test.ts +3 -1
- package/src/cli/Program.ts +22 -0
- package/src/edge/Edge.test.ts +209 -54
- package/src/edge/Edge.ts +121 -42
- package/src/edge/index.ts +1 -0
- package/src/install/ConfigScaffold.ts +4 -1
- package/src/install/Install.test.ts +10 -0
- package/src/install/Install.ts +5 -2
- package/src/replay/Replay.test.ts +32 -0
- package/src/replay/Replay.ts +3 -1
- package/src/replay/Trailers.test.ts +11 -0
- package/src/replay/Trailers.ts +3 -1
- package/src/replay/index.ts +1 -1
- package/src/wire/Slices.ts +25 -0
- package/src/wire/WireSchema.test.ts +18 -0
- package/src/wire/golden/1/slices.json +11 -0
- package/src/wire/index.ts +1 -0
- package/src/workflow/load.test.ts +82 -5
- package/src/workflow/load.ts +40 -16
- package/src/workflows/cleanup/cleanup.feature +21 -0
- package/src/workflows/cleanup/cleanup.ts +1 -1
- package/src/workflows/index.ts +2 -1
- package/src/workflows/rollout/rollout.feature +282 -2
- package/src/workflows/rollout/rollout.test.ts +96 -2
- package/src/workflows/rollout/rollout.ts +41 -3
- package/src/workflows/shared/options.test.ts +25 -2
- package/src/workflows/shared/options.ts +7 -4
- package/src/workflows/shared/registry.test.ts +41 -1
- package/src/workflows/shared/registry.ts +26 -1
- package/src/workflows/shared/text.test.ts +252 -2
- package/src/workflows/shared/text.ts +44 -1
- package/src/workflows/shared/vars.ts +2 -0
- package/dist/chunks/registry-CP3YuV9j.mjs +0 -101
|
@@ -115,6 +115,17 @@ describe("the commit-message codec", () => {
|
|
|
115
115
|
expect(parsed.version).toBe("1.2.3")
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
+
it("reads a Gtd-Version carrying control bytes as unknown, and keeps spaces and parens", () => {
|
|
119
|
+
const withVersion = (value: string) =>
|
|
120
|
+
formatCommitMessage({ actor: "a", to: "b" }).replace(
|
|
121
|
+
"Gtd-Format: 1",
|
|
122
|
+
`Gtd-Version: ${value}\nGtd-Format: 1`,
|
|
123
|
+
)
|
|
124
|
+
expect(parseCommitMessage(withVersion("1.0\u001b]0;x\u0007")).version).toBeUndefined()
|
|
125
|
+
expect(parseCommitMessage(withVersion("1.0\u009b")).version).toBeUndefined()
|
|
126
|
+
expect(parseCommitMessage(withVersion("0.0.1 (dev)")).version).toBe("0.0.1 (dev)")
|
|
127
|
+
})
|
|
128
|
+
|
|
118
129
|
it("omits Gtd-Branch and Gtd-Version when unset, and reads them as undefined", () => {
|
|
119
130
|
const message = formatCommitMessage({ actor: "agent", to: "idle" })
|
|
120
131
|
expect(message).not.toContain("Gtd-Branch")
|
package/src/replay/Trailers.ts
CHANGED
|
@@ -196,7 +196,9 @@ const TRAILER_READERS: Readonly<Record<string, (value: string, into: Trailers) =
|
|
|
196
196
|
if (value !== "") into.branch ??= value
|
|
197
197
|
},
|
|
198
198
|
"Gtd-Version": (value, into) => {
|
|
199
|
-
|
|
199
|
+
// Untrusted (fetched history): control bytes would reach the terminal; a rejected value reads as unknown.
|
|
200
|
+
// eslint-disable-next-line no-control-regex
|
|
201
|
+
if (value !== "" && !/[\u0000-\u001f\u007f-\u009f]/.test(value)) into.version ??= value
|
|
200
202
|
},
|
|
201
203
|
"Gtd-Kept": (value, into) => {
|
|
202
204
|
if (HASH_RE.test(value)) into.kept ??= value
|
package/src/replay/index.ts
CHANGED
|
@@ -12,4 +12,4 @@ export {
|
|
|
12
12
|
export { accessShapeFault, foldAccess } from "./Access.js"
|
|
13
13
|
export { formatFault } from "./Format.js"
|
|
14
14
|
export { refNameFault } from "./RefName.js"
|
|
15
|
-
export { sliceRanges
|
|
15
|
+
export { sliceRanges } from "./Slices.js"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { WIRE_SCHEMA } from "./constants.js"
|
|
2
|
+
|
|
3
|
+
export interface SliceEntry {
|
|
4
|
+
readonly name: string
|
|
5
|
+
/** Full hash, exclusive: git's base..tip. */
|
|
6
|
+
readonly base: string
|
|
7
|
+
/** Full hash, inclusive; HEAD's hash for the open slice. */
|
|
8
|
+
readonly tip: string
|
|
9
|
+
/** false only for the last slice of an underway process. */
|
|
10
|
+
readonly closed: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SlicesDocument {
|
|
14
|
+
readonly schema: number
|
|
15
|
+
readonly slices: readonly SliceEntry[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The ONLY place `gtd slices --json`'s shape is built; copies exactly the four fields, in order. */
|
|
19
|
+
export const slicesDocument = (slices: readonly SliceEntry[]): SlicesDocument => ({
|
|
20
|
+
schema: WIRE_SCHEMA,
|
|
21
|
+
slices: slices.map(({ name, base, tip, closed }) => ({ name, base, tip, closed })),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const renderSlicesJson = (document: SlicesDocument): string =>
|
|
25
|
+
JSON.stringify(document) + "\n"
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
landFields,
|
|
10
10
|
renderBeatJson,
|
|
11
11
|
renderLandJson,
|
|
12
|
+
renderSlicesJson,
|
|
13
|
+
slicesDocument,
|
|
12
14
|
statusOf,
|
|
13
15
|
WIRE_SCHEMA,
|
|
14
16
|
type BeatKind,
|
|
@@ -116,6 +118,9 @@ const documents: Readonly<Record<string, string>> = {
|
|
|
116
118
|
}),
|
|
117
119
|
),
|
|
118
120
|
"judge.json": JUDGE,
|
|
121
|
+
"slices.json": renderSlicesJson(
|
|
122
|
+
slicesDocument([{ name: "build", base: "a".repeat(40), tip: "b".repeat(40), closed: true }]),
|
|
123
|
+
),
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
describe(`wire schema ${WIRE_SCHEMA}`, () => {
|
|
@@ -139,6 +144,19 @@ describe(`wire schema ${WIRE_SCHEMA}`, () => {
|
|
|
139
144
|
}
|
|
140
145
|
})
|
|
141
146
|
|
|
147
|
+
describe("slicesDocument", () => {
|
|
148
|
+
it("stamps WIRE_SCHEMA and drops any field beyond the four", () => {
|
|
149
|
+
const doc = slicesDocument([
|
|
150
|
+
{ name: "n", base: "b", tip: "t", closed: false, extra: 1 } as never,
|
|
151
|
+
])
|
|
152
|
+
expect(doc).toEqual({
|
|
153
|
+
schema: WIRE_SCHEMA,
|
|
154
|
+
slices: [{ name: "n", base: "b", tip: "t", closed: false }],
|
|
155
|
+
})
|
|
156
|
+
expect(Object.keys(doc.slices[0]!)).toEqual(["name", "base", "tip", "closed"])
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
142
160
|
describe("the shape check", () => {
|
|
143
161
|
const land = JSON.parse(documents["land.json"]!) as Record<string, unknown>
|
|
144
162
|
const { subject, ...rest } = land
|
package/src/wire/index.ts
CHANGED
|
@@ -651,15 +651,92 @@ describe("ConfigService", () => {
|
|
|
651
651
|
expect((await getConfig()).workflowNamed("review")?.def.base?.("review", {})).toBe("develop")
|
|
652
652
|
})
|
|
653
653
|
|
|
654
|
-
it("a door
|
|
655
|
-
const both = `export { feature as default, fix, fix as
|
|
654
|
+
it("a door several workflows build gets no door when none is exported under its name, until the doors export declares it", async () => {
|
|
655
|
+
const both = `export { feature as default, fix as fix2, fix as fix3 } from "@pmelab/gtd/workflows"`
|
|
656
656
|
writeFileSync(join(projectDir, "gtd.config.ts"), `${both}\n`)
|
|
657
|
-
await
|
|
657
|
+
expect((await getConfig()).doors.get("fix")).toBeUndefined()
|
|
658
658
|
writeFileSync(
|
|
659
659
|
join(projectDir, "gtd.config.ts"),
|
|
660
|
-
`${both}\nexport const doors = { fix: { workflow: "
|
|
660
|
+
`${both}\nexport const doors = { fix: { workflow: "fix3" } }\n`,
|
|
661
661
|
)
|
|
662
|
-
expect((await getConfig()).doors.get("fix")?.workflow).toBe("
|
|
662
|
+
expect((await getConfig()).doors.get("fix")?.workflow).toBe("fix3")
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
it("a door clash warns once at doors.<name>, builders sorted", async () => {
|
|
666
|
+
writeFileSync(
|
|
667
|
+
join(projectDir, "gtd.config.ts"),
|
|
668
|
+
`export { feature as default, fix as fix3, fix as fix4, fix as fix2 } from "@pmelab/gtd/workflows"\n`,
|
|
669
|
+
)
|
|
670
|
+
const cfg = await getConfig()
|
|
671
|
+
const clash = cfg.warnings.filter((w) => w.path[0] === "doors")
|
|
672
|
+
expect(clash).toEqual([
|
|
673
|
+
{
|
|
674
|
+
severity: "warning",
|
|
675
|
+
path: ["doors", "fix"],
|
|
676
|
+
origin: join(projectDir, "gtd.config.ts"),
|
|
677
|
+
message: `door "fix" is built by workflows "fix2", "fix3" and "fix4" and none is exported under that name — declare it in the "doors" export`,
|
|
678
|
+
},
|
|
679
|
+
])
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
it("no clash warning when an export is named after the door, one workflow builds it, or doors declares it", async () => {
|
|
683
|
+
const both = `export { feature as default, fix as fix2, fix as fix3 } from "@pmelab/gtd/workflows"`
|
|
684
|
+
for (const body of [
|
|
685
|
+
`export { feature as default, fix as fix2, fix } from "@pmelab/gtd/workflows"\n`,
|
|
686
|
+
`export { feature as default, fix } from "@pmelab/gtd/workflows"\n`,
|
|
687
|
+
`${both}\nexport const doors = { fix: { workflow: "fix3" } }\n`,
|
|
688
|
+
]) {
|
|
689
|
+
writeFileSync(join(projectDir, "gtd.config.ts"), body)
|
|
690
|
+
expect((await getConfig()).warnings.filter((w) => w.path[0] === "doors")).toEqual([])
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
it("door ownership does not depend on export order", async () => {
|
|
695
|
+
writeFileSync(
|
|
696
|
+
join(projectDir, "gtd.config.ts"),
|
|
697
|
+
`export { feature as default, fix as fix2, fix } from "@pmelab/gtd/workflows"\n`,
|
|
698
|
+
)
|
|
699
|
+
expect((await getConfig()).doors.get("fix")?.workflow).toBe("fix")
|
|
700
|
+
})
|
|
701
|
+
|
|
702
|
+
it("rollout and a second instance of it load, door rollout running workflow rollout whichever export comes first", async () => {
|
|
703
|
+
for (const body of [
|
|
704
|
+
`export { rollout } from "@pmelab/gtd/workflows"\nimport { rollout } from "@pmelab/gtd/workflows"\nexport const elsewhere = rollout({ flagRegistry: "x.ts" })`,
|
|
705
|
+
`import { rollout } from "@pmelab/gtd/workflows"\nexport const elsewhere = rollout({ flagRegistry: "x.ts" })\nexport { rollout }`,
|
|
706
|
+
]) {
|
|
707
|
+
writeFileSync(
|
|
708
|
+
join(projectDir, "gtd.config.ts"),
|
|
709
|
+
`export { feature as default } from "@pmelab/gtd/workflows"\n${body}\n`,
|
|
710
|
+
)
|
|
711
|
+
expect((await getConfig()).doors.get("rollout")?.workflow).toBe("rollout")
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
|
|
715
|
+
it("shipped rollout and cleanup list their doors beside fix and review", async () => {
|
|
716
|
+
writeFileSync(
|
|
717
|
+
join(projectDir, "gtd.config.ts"),
|
|
718
|
+
`export { feature as default, fix, review, rollout, cleanup } from "@pmelab/gtd/workflows"\n`,
|
|
719
|
+
)
|
|
720
|
+
const { doors } = await getConfig()
|
|
721
|
+
expect([...doors.keys()]).toEqual(["cleanup", "fix", "review", "rollout"])
|
|
722
|
+
expect(doors.get("rollout")?.args).toEqual([{ name: "flag" }])
|
|
723
|
+
expect(doors.get("cleanup")?.args).toEqual([{ name: "flag" }])
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
it("rollout and cleanup load from their own subpaths", async () => {
|
|
727
|
+
writeFileSync(
|
|
728
|
+
join(projectDir, "gtd.config.ts"),
|
|
729
|
+
[
|
|
730
|
+
`export { feature as default, fix, review } from "@pmelab/gtd/workflows"`,
|
|
731
|
+
`export { rollout } from "@pmelab/gtd/workflows/rollout"`,
|
|
732
|
+
`export { cleanup } from "@pmelab/gtd/workflows/cleanup"`,
|
|
733
|
+
``,
|
|
734
|
+
].join("\n"),
|
|
735
|
+
)
|
|
736
|
+
const { doors } = await getConfig()
|
|
737
|
+
expect([...doors.keys()]).toEqual(["cleanup", "fix", "review", "rollout"])
|
|
738
|
+
expect(doors.get("rollout")?.args).toEqual([{ name: "flag" }])
|
|
739
|
+
expect(doors.get("cleanup")?.args).toEqual([{ name: "flag" }])
|
|
663
740
|
})
|
|
664
741
|
|
|
665
742
|
it("a config's doors merge derived doors (exported workflows) with declared doors (doors export)", async () => {
|
package/src/workflow/load.ts
CHANGED
|
@@ -548,31 +548,43 @@ const doorsFrom = (exports: Record<string, unknown>): ReadonlyMap<string, Door>
|
|
|
548
548
|
|
|
549
549
|
/**
|
|
550
550
|
* Each exported workflow's own doors, onto the name it is exported under. The
|
|
551
|
-
* `doors` export adds to them and
|
|
551
|
+
* `doors` export adds to them and declares a name several workflows clash on.
|
|
552
552
|
*/
|
|
553
553
|
const derivedDoors = (
|
|
554
554
|
catalogue: ReadonlyMap<string, LoadedWorkflow>,
|
|
555
555
|
declared: ReadonlyMap<string, Door>,
|
|
556
|
-
):
|
|
557
|
-
|
|
556
|
+
): {
|
|
557
|
+
readonly doors: ReadonlyMap<string, Door>
|
|
558
|
+
readonly clashes: ReadonlyMap<string, readonly string[]>
|
|
559
|
+
} => {
|
|
560
|
+
const builders = new Map<string, Door[]>()
|
|
558
561
|
for (const [workflow, loaded] of catalogue) {
|
|
559
562
|
for (const [name, spec] of Object.entries(loaded.doors ?? {})) {
|
|
560
563
|
const door: Door = { ...spec, workflow }
|
|
561
564
|
const problem = doorProblem(name, door)
|
|
562
565
|
if (problem !== undefined)
|
|
563
566
|
throw new Error(`workflow "${workflow}" builds door "${name}", which ${problem}`)
|
|
564
|
-
|
|
565
|
-
if (other !== undefined && !declared.has(name)) {
|
|
566
|
-
throw new Error(
|
|
567
|
-
`door "${name}" is built by workflows "${other.workflow}" and "${workflow}" — declare it in the "doors" export to choose one (a declared door replaces the derived one, so restate its args and vars)`,
|
|
568
|
-
)
|
|
569
|
-
}
|
|
570
|
-
doors.set(name, door)
|
|
567
|
+
builders.set(name, [...(builders.get(name) ?? []), door])
|
|
571
568
|
}
|
|
572
569
|
}
|
|
573
|
-
|
|
570
|
+
const doors = new Map<string, Door>()
|
|
571
|
+
const clashes = new Map<string, readonly string[]>()
|
|
572
|
+
for (const [name, built] of builders) {
|
|
573
|
+
const owner = ownerOf(name, built)
|
|
574
|
+
if (owner !== undefined) doors.set(name, owner)
|
|
575
|
+
else if (!declared.has(name))
|
|
576
|
+
clashes.set(
|
|
577
|
+
name,
|
|
578
|
+
built.map((door) => door.workflow),
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
return { doors: new Map([...doors, ...declared]), clashes }
|
|
574
582
|
}
|
|
575
583
|
|
|
584
|
+
/** The export named after the door owns it, else the sole builder; a clash of several builds no door. */
|
|
585
|
+
const ownerOf = (name: string, built: readonly Door[]): Door | undefined =>
|
|
586
|
+
built.find((door) => door.workflow === name) ?? (built.length === 1 ? built[0] : undefined)
|
|
587
|
+
|
|
576
588
|
/** Sort the doors and check each names a startable workflow. */
|
|
577
589
|
const checkDoors = (
|
|
578
590
|
declared: ReadonlyMap<string, Door>,
|
|
@@ -628,16 +640,28 @@ const loadWorkflows = (module: WorkflowModule, shipped: ShippedModules): Loaded
|
|
|
628
640
|
}),
|
|
629
641
|
)
|
|
630
642
|
const defaultWorkflow = built.get(value)
|
|
643
|
+
const derived =
|
|
644
|
+
rejections.length > 0
|
|
645
|
+
? { doors: new Map<string, Door>(), clashes: new Map<string, readonly string[]>() }
|
|
646
|
+
: derivedDoors(catalogue, doorsFrom(exports))
|
|
631
647
|
return {
|
|
632
648
|
file,
|
|
633
649
|
catalogue,
|
|
634
|
-
doors:
|
|
635
|
-
rejections.length > 0
|
|
636
|
-
? new Map()
|
|
637
|
-
: checkDoors(derivedDoors(catalogue, doorsFrom(exports)), catalogue),
|
|
650
|
+
doors: checkDoors(derived.doors, catalogue),
|
|
638
651
|
default: defaultWorkflow && { ...defaultWorkflow, name: defaultName },
|
|
639
652
|
config: compileConfig(exports["config"], module.filepath),
|
|
640
|
-
diagnostics:
|
|
653
|
+
diagnostics: [
|
|
654
|
+
...nameRejections(rejections),
|
|
655
|
+
...[...derived.clashes].map(([name, builders]): Diagnostic => {
|
|
656
|
+
const names = [...builders].sort().map((b) => JSON.stringify(b))
|
|
657
|
+
return {
|
|
658
|
+
severity: "warning",
|
|
659
|
+
message: `door "${name}" is built by workflows ${names.slice(0, -1).join(", ")} and ${names.at(-1)} and none is exported under that name — declare it in the "doors" export`,
|
|
660
|
+
path: ["doors", name],
|
|
661
|
+
origin: file.origin,
|
|
662
|
+
}
|
|
663
|
+
}),
|
|
664
|
+
],
|
|
641
665
|
}
|
|
642
666
|
}
|
|
643
667
|
|
|
@@ -75,6 +75,27 @@ Feature: cleanup removes a default-on release flag and keeps its flag-on path
|
|
|
75
75
|
And the process is driven
|
|
76
76
|
Then the process rests at "build.review.reviewing"
|
|
77
77
|
|
|
78
|
+
@inmem
|
|
79
|
+
Scenario: gtd door cleanup starts the cleanup of its flag argument and reaches the review
|
|
80
|
+
Given step "cleanup.removing" writes "src/flags/index.ts" with:
|
|
81
|
+
"""
|
|
82
|
+
export const FLAGS = [
|
|
83
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release" },
|
|
84
|
+
] as const
|
|
85
|
+
"""
|
|
86
|
+
And step "cleanup.removing" writes "src/calc.ts" with:
|
|
87
|
+
"""
|
|
88
|
+
export const on = true
|
|
89
|
+
"""
|
|
90
|
+
And step "build.health.check" changes nothing
|
|
91
|
+
When gtd opens door "cleanup" with "live"
|
|
92
|
+
And the process is driven
|
|
93
|
+
Then the process rests at "build.review.reviewing"
|
|
94
|
+
|
|
95
|
+
@inmem
|
|
96
|
+
Scenario: gtd door cleanup with no argument is refused naming the flag
|
|
97
|
+
Then opening door "cleanup" is refused with "usage: gtd door cleanup <flag>"
|
|
98
|
+
|
|
78
99
|
@inmem
|
|
79
100
|
Scenario: a fix after a red suite that restores the registry entry is refused
|
|
80
101
|
Given step "cleanup.removing" writes "src/flags/index.ts" with:
|
package/src/workflows/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export default feature
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* @deprecated Empty: each shipped workflow builds its own doors, so a config
|
|
38
|
-
* gets `fix` and `review [base]` by exporting `fix` and `review
|
|
38
|
+
* gets `fix` and `review [base]` by exporting `fix` and `review`, and
|
|
39
|
+
* `rollout <flag>` and `cleanup <flag>` by exporting `rollout` and `cleanup`.
|
|
39
40
|
*/
|
|
40
41
|
export const doors: Doors = {}
|
|
@@ -47,11 +47,16 @@ Feature: rollout turns one release flag default-on and touches nothing else
|
|
|
47
47
|
] as const
|
|
48
48
|
"""
|
|
49
49
|
And step "rollout.suite.check" changes nothing
|
|
50
|
+
And step "rollout.mentions" runs its script
|
|
50
51
|
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
51
52
|
And the process is driven
|
|
52
53
|
Then the process rests at "idle"
|
|
53
54
|
And "src/flags/index.ts" contains "kind: \"release\", defaultOn: true },"
|
|
54
|
-
And the
|
|
55
|
+
And the commit subjects end with:
|
|
56
|
+
"""
|
|
57
|
+
gtd(check): rollout.suite.check → rollout.mentions
|
|
58
|
+
gtd(check): rollout.mentions → idle
|
|
59
|
+
"""
|
|
55
60
|
|
|
56
61
|
Scenario: a turn also changing a source file is refused naming that path
|
|
57
62
|
Given step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
@@ -98,11 +103,34 @@ Feature: rollout turns one release flag default-on and touches nothing else
|
|
|
98
103
|
] as const
|
|
99
104
|
"""
|
|
100
105
|
And step "rollout.suite.check" changes nothing
|
|
106
|
+
And step "rollout.mentions" runs its script
|
|
101
107
|
When gtd starts workflow "elsewhere" with "--var flag=gamma"
|
|
102
108
|
And the process is driven
|
|
103
109
|
Then the process rests at "idle"
|
|
104
110
|
And "config/flags.ts" contains "defaultOn: true"
|
|
105
111
|
|
|
112
|
+
@inmem
|
|
113
|
+
Scenario: gtd door rollout starts the rollout of its flag argument
|
|
114
|
+
Given step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
115
|
+
"""
|
|
116
|
+
export const FLAGS = [
|
|
117
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
118
|
+
{ name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
|
|
119
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
120
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
121
|
+
] as const
|
|
122
|
+
"""
|
|
123
|
+
And step "rollout.suite.check" changes nothing
|
|
124
|
+
And step "rollout.mentions" runs its script
|
|
125
|
+
When gtd opens door "rollout" with "alpha"
|
|
126
|
+
And the process is driven
|
|
127
|
+
Then the process rests at "idle"
|
|
128
|
+
And "src/flags/index.ts" contains "kind: \"release\", defaultOn: true },"
|
|
129
|
+
|
|
130
|
+
@inmem
|
|
131
|
+
Scenario: gtd door rollout with no argument is refused naming the flag
|
|
132
|
+
Then opening door "rollout" is refused with "usage: gtd door rollout <flag>"
|
|
133
|
+
|
|
106
134
|
Scenario: a missing flag var is refused before any turn
|
|
107
135
|
Then starting workflow "rollout" with "" is refused with "gtd: rollout needs --var flag=<name>"
|
|
108
136
|
|
|
@@ -209,6 +237,258 @@ Feature: rollout turns one release flag default-on and touches nothing else
|
|
|
209
237
|
fixed by hand
|
|
210
238
|
"""
|
|
211
239
|
And step "rollout.suite.check" from now on changes nothing
|
|
240
|
+
And step "rollout.mentions" runs its script
|
|
212
241
|
When the process is driven
|
|
213
242
|
Then the process rests at "idle"
|
|
214
|
-
And the
|
|
243
|
+
And the commit subjects end with:
|
|
244
|
+
"""
|
|
245
|
+
gtd(check): rollout.suite.check → rollout.mentions
|
|
246
|
+
gtd(check): rollout.mentions → idle
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
@inmem
|
|
250
|
+
Scenario: a stray file written at the mentions step is refused naming that path
|
|
251
|
+
Given step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
252
|
+
"""
|
|
253
|
+
export const FLAGS = [
|
|
254
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
255
|
+
{ name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
|
|
256
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
257
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
258
|
+
] as const
|
|
259
|
+
"""
|
|
260
|
+
And step "rollout.suite.check" changes nothing
|
|
261
|
+
And step "rollout.mentions" runs its script
|
|
262
|
+
And step "rollout.mentions" writes "src/stray.ts" with:
|
|
263
|
+
"""
|
|
264
|
+
export const x = 1
|
|
265
|
+
"""
|
|
266
|
+
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
267
|
+
Then driving the process is refused with "rollout-only: src/stray.ts changed"
|
|
268
|
+
|
|
269
|
+
@inmem
|
|
270
|
+
Scenario: a second registry entry edited at the mentions step is refused
|
|
271
|
+
Given step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
272
|
+
"""
|
|
273
|
+
export const FLAGS = [
|
|
274
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
275
|
+
{ name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
|
|
276
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
277
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
278
|
+
] as const
|
|
279
|
+
"""
|
|
280
|
+
And step "rollout.suite.check" changes nothing
|
|
281
|
+
And step "rollout.mentions" runs its script
|
|
282
|
+
And step "rollout.mentions" writes "src/flags/index.ts" with:
|
|
283
|
+
"""
|
|
284
|
+
export const FLAGS = [
|
|
285
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
286
|
+
{ name: "beta", owner: "z", expiry: "2027-02-01", kind: "release" },
|
|
287
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
288
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
289
|
+
] as const
|
|
290
|
+
"""
|
|
291
|
+
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
292
|
+
Then driving the process is refused with "rollout-only: other edits in src/flags/index.ts"
|
|
293
|
+
|
|
294
|
+
@inmem
|
|
295
|
+
Scenario: every line naming the flag is listed, no file is edited, and gtd summary repeats the list
|
|
296
|
+
Given a file "README.md" with:
|
|
297
|
+
"""
|
|
298
|
+
# calc
|
|
299
|
+
|
|
300
|
+
The alpha formatter is on under GTD_FLAG_ALPHA=1.
|
|
301
|
+
"""
|
|
302
|
+
And a file "src/calc.ts" with:
|
|
303
|
+
"""
|
|
304
|
+
export const help = "pass --flag_alpha to enable the alpha formatter"
|
|
305
|
+
"""
|
|
306
|
+
And the working tree is committed as "docs: mention the flag"
|
|
307
|
+
And step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
308
|
+
"""
|
|
309
|
+
export const FLAGS = [
|
|
310
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
311
|
+
{ name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
|
|
312
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
313
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
314
|
+
] as const
|
|
315
|
+
"""
|
|
316
|
+
And step "rollout.suite.check" changes nothing
|
|
317
|
+
And step "rollout.mentions" runs its script
|
|
318
|
+
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
319
|
+
And the process is driven
|
|
320
|
+
Then the process rests at "idle"
|
|
321
|
+
And ".gtd/FEEDBACK.md" does not exist
|
|
322
|
+
And "README.md" contains "The alpha formatter is on under GTD_FLAG_ALPHA=1."
|
|
323
|
+
And "src/calc.ts" contains "pass --flag_alpha to enable the alpha formatter"
|
|
324
|
+
And the output of gtd "summary" contains "`README.md:3`"
|
|
325
|
+
And the output of gtd "summary" contains "src/calc.ts:1"
|
|
326
|
+
|
|
327
|
+
@inmem
|
|
328
|
+
Scenario: gtd summary's grep hint excludes a factory flagRegistry
|
|
329
|
+
Given a file "config/flags.ts" with:
|
|
330
|
+
"""
|
|
331
|
+
export const FLAGS = [
|
|
332
|
+
{ name: "alpha", owner: "g", expiry: "2027-05-01", kind: "release" },
|
|
333
|
+
] as const
|
|
334
|
+
"""
|
|
335
|
+
And a file "NOTES.md" with:
|
|
336
|
+
"""
|
|
337
|
+
alpha 1
|
|
338
|
+
alpha 2
|
|
339
|
+
alpha 3
|
|
340
|
+
alpha 4
|
|
341
|
+
alpha 5
|
|
342
|
+
alpha 6
|
|
343
|
+
alpha 7
|
|
344
|
+
alpha 8
|
|
345
|
+
alpha 9
|
|
346
|
+
alpha 10
|
|
347
|
+
alpha 11
|
|
348
|
+
alpha 12
|
|
349
|
+
alpha 13
|
|
350
|
+
alpha 14
|
|
351
|
+
alpha 15
|
|
352
|
+
alpha 16
|
|
353
|
+
alpha 17
|
|
354
|
+
alpha 18
|
|
355
|
+
alpha 19
|
|
356
|
+
alpha 20
|
|
357
|
+
alpha 21
|
|
358
|
+
alpha 22
|
|
359
|
+
alpha 23
|
|
360
|
+
alpha 24
|
|
361
|
+
alpha 25
|
|
362
|
+
alpha 26
|
|
363
|
+
alpha 27
|
|
364
|
+
alpha 28
|
|
365
|
+
alpha 29
|
|
366
|
+
alpha 30
|
|
367
|
+
alpha 31
|
|
368
|
+
alpha 32
|
|
369
|
+
alpha 33
|
|
370
|
+
alpha 34
|
|
371
|
+
alpha 35
|
|
372
|
+
alpha 36
|
|
373
|
+
alpha 37
|
|
374
|
+
alpha 38
|
|
375
|
+
alpha 39
|
|
376
|
+
alpha 40
|
|
377
|
+
alpha 41
|
|
378
|
+
alpha 42
|
|
379
|
+
alpha 43
|
|
380
|
+
alpha 44
|
|
381
|
+
alpha 45
|
|
382
|
+
alpha 46
|
|
383
|
+
alpha 47
|
|
384
|
+
alpha 48
|
|
385
|
+
alpha 49
|
|
386
|
+
alpha 50
|
|
387
|
+
alpha 51
|
|
388
|
+
alpha 52
|
|
389
|
+
alpha 53
|
|
390
|
+
alpha 54
|
|
391
|
+
alpha 55
|
|
392
|
+
"""
|
|
393
|
+
And the working tree is committed as "docs: mention the flag"
|
|
394
|
+
And step "rollout.flipping" writes "config/flags.ts" with:
|
|
395
|
+
"""
|
|
396
|
+
export const FLAGS = [
|
|
397
|
+
{ name: "alpha", owner: "g", expiry: "2027-05-01", kind: "release", defaultOn: true },
|
|
398
|
+
] as const
|
|
399
|
+
"""
|
|
400
|
+
And step "rollout.suite.check" changes nothing
|
|
401
|
+
And step "rollout.mentions" runs its script
|
|
402
|
+
When gtd starts workflow "elsewhere" with "--var flag=alpha"
|
|
403
|
+
And the process is driven
|
|
404
|
+
Then the process rests at "idle"
|
|
405
|
+
And the output of gtd "summary" contains "':(top,exclude,literal)config/flags.ts'"
|
|
406
|
+
|
|
407
|
+
@inmem
|
|
408
|
+
Scenario: a flag named nowhere outside the registry says so in gtd summary
|
|
409
|
+
Given step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
410
|
+
"""
|
|
411
|
+
export const FLAGS = [
|
|
412
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
413
|
+
{ name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
|
|
414
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
415
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
416
|
+
] as const
|
|
417
|
+
"""
|
|
418
|
+
And step "rollout.suite.check" changes nothing
|
|
419
|
+
And step "rollout.mentions" runs its script
|
|
420
|
+
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
421
|
+
And the process is driven
|
|
422
|
+
Then the process rests at "idle"
|
|
423
|
+
And ".gtd/FEEDBACK.md" does not exist
|
|
424
|
+
And the output of gtd "summary" contains "no line outside the registry names it"
|
|
425
|
+
|
|
426
|
+
@inmem
|
|
427
|
+
Scenario: the closing commit marks the total and each listed line
|
|
428
|
+
Given a file "README.md" with:
|
|
429
|
+
"""
|
|
430
|
+
# calc
|
|
431
|
+
|
|
432
|
+
The alpha formatter is on under GTD_FLAG_ALPHA=1.
|
|
433
|
+
"""
|
|
434
|
+
And a file "src/calc.ts" with:
|
|
435
|
+
"""
|
|
436
|
+
export const help = "pass --flag_alpha to enable the alpha formatter"
|
|
437
|
+
"""
|
|
438
|
+
And the working tree is committed as "docs: mention the flag"
|
|
439
|
+
And step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
440
|
+
"""
|
|
441
|
+
export const FLAGS = [
|
|
442
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
443
|
+
{ name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
|
|
444
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
445
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
446
|
+
] as const
|
|
447
|
+
"""
|
|
448
|
+
And step "rollout.suite.check" changes nothing
|
|
449
|
+
And step "rollout.mentions" runs its script
|
|
450
|
+
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
451
|
+
And the process is driven
|
|
452
|
+
Then the last commit subject is "gtd(check): rollout.mentions → idle"
|
|
453
|
+
And the last commit body contains "Gtd-Mark: flag-mentions=2"
|
|
454
|
+
And the last commit body contains "Gtd-Mark: flag-mention=README.md:3"
|
|
455
|
+
And the last commit body contains "Gtd-Mark: flag-mention=src/calc.ts:1"
|
|
456
|
+
|
|
457
|
+
@inmem
|
|
458
|
+
Scenario: a finished rollout leaves no state file for the next feature start
|
|
459
|
+
Given a file "README.md" with:
|
|
460
|
+
"""
|
|
461
|
+
# calc
|
|
462
|
+
|
|
463
|
+
The alpha formatter is on under GTD_FLAG_ALPHA=1.
|
|
464
|
+
"""
|
|
465
|
+
And a file "src/calc.ts" with:
|
|
466
|
+
"""
|
|
467
|
+
export const help = "pass --flag_alpha to enable the alpha formatter"
|
|
468
|
+
"""
|
|
469
|
+
And the working tree is committed as "docs: mention the flag"
|
|
470
|
+
And step "rollout.flipping" writes "src/flags/index.ts" with:
|
|
471
|
+
"""
|
|
472
|
+
export const FLAGS = [
|
|
473
|
+
{ name: "alpha", owner: "a", expiry: "2027-01-01", kind: "release", defaultOn: true },
|
|
474
|
+
{ name: "beta", owner: "b", expiry: "2027-02-01", kind: "release" },
|
|
475
|
+
{ name: "ops1", owner: "c", expiry: "2027-03-01", kind: "ops" },
|
|
476
|
+
{ name: "live", owner: "d", expiry: "2027-04-01", kind: "release", defaultOn: true },
|
|
477
|
+
] as const
|
|
478
|
+
"""
|
|
479
|
+
And step "rollout.suite.check" changes nothing
|
|
480
|
+
And step "rollout.mentions" runs its script
|
|
481
|
+
And step "unwind" changes nothing
|
|
482
|
+
And step "start-gate.check" changes nothing
|
|
483
|
+
When gtd starts workflow "rollout" with "--var flag=alpha"
|
|
484
|
+
And the process is driven
|
|
485
|
+
Then the process rests at "idle"
|
|
486
|
+
Given a file "TODO.md" with:
|
|
487
|
+
"""
|
|
488
|
+
# Add subtract
|
|
489
|
+
|
|
490
|
+
subtract(a, b) returns the difference.
|
|
491
|
+
"""
|
|
492
|
+
When the process is driven
|
|
493
|
+
Then the process rests at "design.triage"
|
|
494
|
+
And ".gtd/FEEDBACK.md" does not exist
|