@wopr-network/defcon 0.2.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 +274 -0
- package/dist/api/router.d.ts +24 -0
- package/dist/api/router.js +44 -0
- package/dist/api/server.d.ts +13 -0
- package/dist/api/server.js +280 -0
- package/dist/api/wire-types.d.ts +46 -0
- package/dist/api/wire-types.js +5 -0
- package/dist/config/db-path.d.ts +1 -0
- package/dist/config/db-path.js +1 -0
- package/dist/config/exporter.d.ts +3 -0
- package/dist/config/exporter.js +87 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +4 -0
- package/dist/config/seed-loader.d.ts +10 -0
- package/dist/config/seed-loader.js +108 -0
- package/dist/config/zod-schemas.d.ts +165 -0
- package/dist/config/zod-schemas.js +283 -0
- package/dist/cors.d.ts +8 -0
- package/dist/cors.js +21 -0
- package/dist/engine/constants.d.ts +1 -0
- package/dist/engine/constants.js +1 -0
- package/dist/engine/engine.d.ts +69 -0
- package/dist/engine/engine.js +485 -0
- package/dist/engine/event-emitter.d.ts +9 -0
- package/dist/engine/event-emitter.js +19 -0
- package/dist/engine/event-types.d.ts +105 -0
- package/dist/engine/event-types.js +1 -0
- package/dist/engine/flow-spawner.d.ts +8 -0
- package/dist/engine/flow-spawner.js +28 -0
- package/dist/engine/gate-command-validator.d.ts +6 -0
- package/dist/engine/gate-command-validator.js +46 -0
- package/dist/engine/gate-evaluator.d.ts +12 -0
- package/dist/engine/gate-evaluator.js +233 -0
- package/dist/engine/handlebars.d.ts +9 -0
- package/dist/engine/handlebars.js +51 -0
- package/dist/engine/index.d.ts +12 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/invocation-builder.d.ts +18 -0
- package/dist/engine/invocation-builder.js +58 -0
- package/dist/engine/on-enter.d.ts +8 -0
- package/dist/engine/on-enter.js +102 -0
- package/dist/engine/ssrf-guard.d.ts +22 -0
- package/dist/engine/ssrf-guard.js +159 -0
- package/dist/engine/state-machine.d.ts +12 -0
- package/dist/engine/state-machine.js +74 -0
- package/dist/execution/active-runner.d.ts +45 -0
- package/dist/execution/active-runner.js +165 -0
- package/dist/execution/admin-schemas.d.ts +116 -0
- package/dist/execution/admin-schemas.js +125 -0
- package/dist/execution/cli.d.ts +57 -0
- package/dist/execution/cli.js +498 -0
- package/dist/execution/handlers/admin.d.ts +67 -0
- package/dist/execution/handlers/admin.js +200 -0
- package/dist/execution/handlers/flow.d.ts +25 -0
- package/dist/execution/handlers/flow.js +289 -0
- package/dist/execution/handlers/query.d.ts +31 -0
- package/dist/execution/handlers/query.js +64 -0
- package/dist/execution/index.d.ts +4 -0
- package/dist/execution/index.js +3 -0
- package/dist/execution/mcp-helpers.d.ts +42 -0
- package/dist/execution/mcp-helpers.js +23 -0
- package/dist/execution/mcp-server.d.ts +33 -0
- package/dist/execution/mcp-server.js +1020 -0
- package/dist/execution/provision-worktree.d.ts +16 -0
- package/dist/execution/provision-worktree.js +123 -0
- package/dist/execution/tool-schemas.d.ts +40 -0
- package/dist/execution/tool-schemas.js +44 -0
- package/dist/gates/blocking-graph.d.ts +26 -0
- package/dist/gates/blocking-graph.js +102 -0
- package/dist/gates/test/bad-return-gate.d.ts +1 -0
- package/dist/gates/test/bad-return-gate.js +4 -0
- package/dist/gates/test/passing-gate.d.ts +2 -0
- package/dist/gates/test/passing-gate.js +3 -0
- package/dist/gates/test/slow-gate.d.ts +2 -0
- package/dist/gates/test/slow-gate.js +5 -0
- package/dist/gates/test/throwing-gate.d.ts +1 -0
- package/dist/gates/test/throwing-gate.js +3 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +12 -0
- package/dist/main.d.ts +14 -0
- package/dist/main.js +28 -0
- package/dist/repositories/drizzle/entity.repo.d.ts +27 -0
- package/dist/repositories/drizzle/entity.repo.js +190 -0
- package/dist/repositories/drizzle/event.repo.d.ts +12 -0
- package/dist/repositories/drizzle/event.repo.js +24 -0
- package/dist/repositories/drizzle/flow.repo.d.ts +22 -0
- package/dist/repositories/drizzle/flow.repo.js +364 -0
- package/dist/repositories/drizzle/gate.repo.d.ts +16 -0
- package/dist/repositories/drizzle/gate.repo.js +98 -0
- package/dist/repositories/drizzle/index.d.ts +6 -0
- package/dist/repositories/drizzle/index.js +7 -0
- package/dist/repositories/drizzle/invocation.repo.d.ts +23 -0
- package/dist/repositories/drizzle/invocation.repo.js +199 -0
- package/dist/repositories/drizzle/schema.d.ts +1932 -0
- package/dist/repositories/drizzle/schema.js +155 -0
- package/dist/repositories/drizzle/transition-log.repo.d.ts +11 -0
- package/dist/repositories/drizzle/transition-log.repo.js +42 -0
- package/dist/repositories/interfaces.d.ts +321 -0
- package/dist/repositories/interfaces.js +2 -0
- package/dist/src/api/router.d.ts +24 -0
- package/dist/src/api/router.js +44 -0
- package/dist/src/api/server.d.ts +13 -0
- package/dist/src/api/server.js +280 -0
- package/dist/src/api/wire-types.d.ts +46 -0
- package/dist/src/api/wire-types.js +5 -0
- package/dist/src/config/db-path.d.ts +1 -0
- package/dist/src/config/db-path.js +1 -0
- package/dist/src/config/exporter.d.ts +3 -0
- package/dist/src/config/exporter.js +87 -0
- package/dist/src/config/index.d.ts +4 -0
- package/dist/src/config/index.js +4 -0
- package/dist/src/config/seed-loader.d.ts +14 -0
- package/dist/src/config/seed-loader.js +131 -0
- package/dist/src/config/zod-schemas.d.ts +165 -0
- package/dist/src/config/zod-schemas.js +283 -0
- package/dist/src/cors.d.ts +8 -0
- package/dist/src/cors.js +21 -0
- package/dist/src/engine/constants.d.ts +1 -0
- package/dist/src/engine/constants.js +1 -0
- package/dist/src/engine/engine.d.ts +69 -0
- package/dist/src/engine/engine.js +485 -0
- package/dist/src/engine/event-emitter.d.ts +9 -0
- package/dist/src/engine/event-emitter.js +19 -0
- package/dist/src/engine/event-types.d.ts +105 -0
- package/dist/src/engine/event-types.js +1 -0
- package/dist/src/engine/flow-spawner.d.ts +8 -0
- package/dist/src/engine/flow-spawner.js +28 -0
- package/dist/src/engine/gate-command-validator.d.ts +6 -0
- package/dist/src/engine/gate-command-validator.js +46 -0
- package/dist/src/engine/gate-evaluator.d.ts +12 -0
- package/dist/src/engine/gate-evaluator.js +233 -0
- package/dist/src/engine/handlebars.d.ts +9 -0
- package/dist/src/engine/handlebars.js +51 -0
- package/dist/src/engine/index.d.ts +12 -0
- package/dist/src/engine/index.js +7 -0
- package/dist/src/engine/invocation-builder.d.ts +18 -0
- package/dist/src/engine/invocation-builder.js +58 -0
- package/dist/src/engine/on-enter.d.ts +8 -0
- package/dist/src/engine/on-enter.js +102 -0
- package/dist/src/engine/ssrf-guard.d.ts +22 -0
- package/dist/src/engine/ssrf-guard.js +159 -0
- package/dist/src/engine/state-machine.d.ts +12 -0
- package/dist/src/engine/state-machine.js +74 -0
- package/dist/src/execution/active-runner.d.ts +45 -0
- package/dist/src/execution/active-runner.js +165 -0
- package/dist/src/execution/admin-schemas.d.ts +116 -0
- package/dist/src/execution/admin-schemas.js +125 -0
- package/dist/src/execution/cli.d.ts +57 -0
- package/dist/src/execution/cli.js +501 -0
- package/dist/src/execution/handlers/admin.d.ts +67 -0
- package/dist/src/execution/handlers/admin.js +200 -0
- package/dist/src/execution/handlers/flow.d.ts +25 -0
- package/dist/src/execution/handlers/flow.js +289 -0
- package/dist/src/execution/handlers/query.d.ts +31 -0
- package/dist/src/execution/handlers/query.js +64 -0
- package/dist/src/execution/index.d.ts +4 -0
- package/dist/src/execution/index.js +3 -0
- package/dist/src/execution/mcp-helpers.d.ts +42 -0
- package/dist/src/execution/mcp-helpers.js +23 -0
- package/dist/src/execution/mcp-server.d.ts +33 -0
- package/dist/src/execution/mcp-server.js +1020 -0
- package/dist/src/execution/provision-worktree.d.ts +16 -0
- package/dist/src/execution/provision-worktree.js +123 -0
- package/dist/src/execution/tool-schemas.d.ts +40 -0
- package/dist/src/execution/tool-schemas.js +44 -0
- package/dist/src/logger.d.ts +8 -0
- package/dist/src/logger.js +12 -0
- package/dist/src/main.d.ts +14 -0
- package/dist/src/main.js +28 -0
- package/dist/src/repositories/drizzle/entity.repo.d.ts +27 -0
- package/dist/src/repositories/drizzle/entity.repo.js +190 -0
- package/dist/src/repositories/drizzle/event.repo.d.ts +12 -0
- package/dist/src/repositories/drizzle/event.repo.js +24 -0
- package/dist/src/repositories/drizzle/flow.repo.d.ts +22 -0
- package/dist/src/repositories/drizzle/flow.repo.js +364 -0
- package/dist/src/repositories/drizzle/gate.repo.d.ts +16 -0
- package/dist/src/repositories/drizzle/gate.repo.js +98 -0
- package/dist/src/repositories/drizzle/index.d.ts +6 -0
- package/dist/src/repositories/drizzle/index.js +7 -0
- package/dist/src/repositories/drizzle/invocation.repo.d.ts +23 -0
- package/dist/src/repositories/drizzle/invocation.repo.js +199 -0
- package/dist/src/repositories/drizzle/schema.d.ts +1932 -0
- package/dist/src/repositories/drizzle/schema.js +155 -0
- package/dist/src/repositories/drizzle/transition-log.repo.d.ts +11 -0
- package/dist/src/repositories/drizzle/transition-log.repo.js +42 -0
- package/dist/src/repositories/interfaces.d.ts +321 -0
- package/dist/src/repositories/interfaces.js +2 -0
- package/dist/src/utils/redact.d.ts +2 -0
- package/dist/src/utils/redact.js +62 -0
- package/dist/utils/redact.d.ts +2 -0
- package/dist/utils/redact.js +62 -0
- package/drizzle/.gitkeep +0 -0
- package/drizzle/0000_simple_surge.sql +144 -0
- package/drizzle/0001_peaceful_marvel_apes.sql +18 -0
- package/drizzle/0002_add_invocations_created_at.sql +1 -0
- package/drizzle/0003_drop_integration_config.sql +1 -0
- package/drizzle/0004_add_flow_discipline.sql +2 -0
- package/drizzle/0004_lucky_silverclaw.sql +5 -0
- package/drizzle/0005_old_blue_shield.sql +2 -0
- package/drizzle/0006_solid_magik.sql +2 -0
- package/drizzle/0007_fancy_luke_cage.sql +1 -0
- package/drizzle/0008_thick_dark_beast.sql +1 -0
- package/drizzle/0009_brief_midnight.sql +1 -0
- package/drizzle/0010_amusing_bastion.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +996 -0
- package/drizzle/meta/0004_snapshot.json +1008 -0
- package/drizzle/meta/0005_snapshot.json +1023 -0
- package/drizzle/meta/0006_snapshot.json +1037 -0
- package/drizzle/meta/0007_snapshot.json +1044 -0
- package/drizzle/meta/0008_snapshot.json +1051 -0
- package/drizzle/meta/0009_snapshot.json +1058 -0
- package/drizzle/meta/0010_snapshot.json +1065 -0
- package/drizzle/meta/_journal.json +83 -0
- package/gates/.gitkeep +0 -0
- package/gates/blocking-graph.d.ts +26 -0
- package/gates/blocking-graph.js +102 -0
- package/gates/blocking-graph.ts +121 -0
- package/gates/check-design-posted.sh +39 -0
- package/gates/check-merge.sh +51 -0
- package/gates/check-pr-capacity.sh +17 -0
- package/gates/check-review-ready.sh +47 -0
- package/gates/check-spec-posted.sh +34 -0
- package/gates/check-unblocked.sh +56 -0
- package/gates/ci-green.sh +9 -0
- package/gates/merge-queue.sh +14 -0
- package/gates/review-bots-ready.sh +9 -0
- package/gates/spec-posted.sh +31 -0
- package/gates/test/bad-return-gate.d.ts +1 -0
- package/gates/test/bad-return-gate.js +4 -0
- package/gates/test/bad-return-gate.ts +4 -0
- package/gates/test/passing-gate.d.ts +2 -0
- package/gates/test/passing-gate.js +3 -0
- package/gates/test/passing-gate.ts +5 -0
- package/gates/test/slow-gate.d.ts +2 -0
- package/gates/test/slow-gate.js +5 -0
- package/gates/test/slow-gate.ts +7 -0
- package/gates/test/throwing-gate.d.ts +1 -0
- package/gates/test/throwing-gate.js +3 -0
- package/gates/test/throwing-gate.ts +3 -0
- package/gates/test-fail.sh +2 -0
- package/gates/test-pass.sh +2 -0
- package/gates/timeout-gate-script.sh +3 -0
- package/package.json +64 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "7",
|
|
3
|
+
"dialect": "sqlite",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"idx": 0,
|
|
7
|
+
"version": "6",
|
|
8
|
+
"when": 1772820079904,
|
|
9
|
+
"tag": "0000_simple_surge",
|
|
10
|
+
"breakpoints": true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"idx": 1,
|
|
14
|
+
"version": "6",
|
|
15
|
+
"when": 1772820079905,
|
|
16
|
+
"tag": "0001_peaceful_marvel_apes",
|
|
17
|
+
"breakpoints": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"idx": 2,
|
|
21
|
+
"version": "6",
|
|
22
|
+
"when": 1772820079906,
|
|
23
|
+
"tag": "0002_add_invocations_created_at",
|
|
24
|
+
"breakpoints": true
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"idx": 3,
|
|
28
|
+
"version": "6",
|
|
29
|
+
"when": 1772820079907,
|
|
30
|
+
"tag": "0003_drop_integration_config",
|
|
31
|
+
"breakpoints": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"idx": 4,
|
|
35
|
+
"version": "6",
|
|
36
|
+
"when": 1772868838854,
|
|
37
|
+
"tag": "0004_lucky_silverclaw",
|
|
38
|
+
"breakpoints": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"idx": 5,
|
|
42
|
+
"version": "6",
|
|
43
|
+
"when": 1772894164983,
|
|
44
|
+
"tag": "0005_old_blue_shield",
|
|
45
|
+
"breakpoints": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"idx": 6,
|
|
49
|
+
"version": "6",
|
|
50
|
+
"when": 1772893521314,
|
|
51
|
+
"tag": "0006_solid_magik",
|
|
52
|
+
"breakpoints": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"idx": 7,
|
|
56
|
+
"version": "6",
|
|
57
|
+
"when": 1741305600000,
|
|
58
|
+
"tag": "0007_fancy_luke_cage",
|
|
59
|
+
"breakpoints": true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"idx": 8,
|
|
63
|
+
"version": "6",
|
|
64
|
+
"when": 1772909877406,
|
|
65
|
+
"tag": "0008_thick_dark_beast",
|
|
66
|
+
"breakpoints": true
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"idx": 9,
|
|
70
|
+
"version": "6",
|
|
71
|
+
"when": 1772906724993,
|
|
72
|
+
"tag": "0009_brief_midnight",
|
|
73
|
+
"breakpoints": true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"idx": 10,
|
|
77
|
+
"version": "6",
|
|
78
|
+
"when": 1772906724994,
|
|
79
|
+
"tag": "0010_amusing_bastion",
|
|
80
|
+
"breakpoints": true
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
}
|
package/gates/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom gate: checks whether all Linear blockers for an entity's issue
|
|
3
|
+
* have corresponding merged PRs on GitHub.
|
|
4
|
+
*
|
|
5
|
+
* Referenced by seed as: "gates/blocking-graph.ts:isUnblocked"
|
|
6
|
+
*
|
|
7
|
+
* NOTE: Function gates are not yet evaluated by the engine (gate-evaluator.ts
|
|
8
|
+
* throws for type "function"). This file exists as the implementation target
|
|
9
|
+
* for when function gate support lands.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This file is intentionally outside src/ — it is loaded dynamically at
|
|
12
|
+
* runtime, not compiled by the main build.
|
|
13
|
+
*/
|
|
14
|
+
import type { Entity } from "../src/repositories/interfaces.js";
|
|
15
|
+
export interface BlockingGraphResult {
|
|
16
|
+
passed: boolean;
|
|
17
|
+
output: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if all blocking issues for the given entity have merged PRs.
|
|
21
|
+
*
|
|
22
|
+
* Expects entity.refs.linear.id to be the Linear issue ID.
|
|
23
|
+
* Uses LINEAR_API_KEY env var for authentication.
|
|
24
|
+
* Uses `gh` CLI to check PR merge status on GitHub.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isUnblocked(entity: Entity): Promise<BlockingGraphResult>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom gate: checks whether all Linear blockers for an entity's issue
|
|
3
|
+
* have corresponding merged PRs on GitHub.
|
|
4
|
+
*
|
|
5
|
+
* Referenced by seed as: "gates/blocking-graph.ts:isUnblocked"
|
|
6
|
+
*
|
|
7
|
+
* NOTE: Function gates are not yet evaluated by the engine (gate-evaluator.ts
|
|
8
|
+
* throws for type "function"). This file exists as the implementation target
|
|
9
|
+
* for when function gate support lands.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This file is intentionally outside src/ — it is loaded dynamically at
|
|
12
|
+
* runtime, not compiled by the main build.
|
|
13
|
+
*/
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
import { promisify } from "node:util";
|
|
16
|
+
import { LinearClient } from "@linear/sdk";
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
/**
|
|
19
|
+
* Check if all blocking issues for the given entity have merged PRs.
|
|
20
|
+
*
|
|
21
|
+
* Expects entity.refs.linear.id to be the Linear issue ID.
|
|
22
|
+
* Uses LINEAR_API_KEY env var for authentication.
|
|
23
|
+
* Uses `gh` CLI to check PR merge status on GitHub.
|
|
24
|
+
*/
|
|
25
|
+
export async function isUnblocked(entity) {
|
|
26
|
+
const linearApiKey = process.env.LINEAR_API_KEY;
|
|
27
|
+
if (!linearApiKey) {
|
|
28
|
+
return { passed: false, output: "LINEAR_API_KEY not set" };
|
|
29
|
+
}
|
|
30
|
+
const issueId = entity.refs?.linear?.id;
|
|
31
|
+
if (!issueId) {
|
|
32
|
+
return { passed: false, output: "Entity has no linear ref" };
|
|
33
|
+
}
|
|
34
|
+
const client = new LinearClient({ apiKey: linearApiKey });
|
|
35
|
+
let issue;
|
|
36
|
+
let inverseRelations;
|
|
37
|
+
try {
|
|
38
|
+
issue = await client.issue(issueId);
|
|
39
|
+
inverseRelations = await issue.inverseRelations();
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
return { passed: false, output: `Linear API error: ${message}` };
|
|
44
|
+
}
|
|
45
|
+
const blockers = inverseRelations.nodes.filter((r) => r.type === "blocks");
|
|
46
|
+
if (blockers.length === 0) {
|
|
47
|
+
return { passed: true, output: "No blockers" };
|
|
48
|
+
}
|
|
49
|
+
const unmerged = [];
|
|
50
|
+
for (const relation of blockers) {
|
|
51
|
+
try {
|
|
52
|
+
const blockerIssue = await relation.issue;
|
|
53
|
+
if (!blockerIssue)
|
|
54
|
+
continue;
|
|
55
|
+
const identifier = blockerIssue.identifier;
|
|
56
|
+
// Resolve the PR via Linear attachment — the attachment URL tells us which repo
|
|
57
|
+
const attachments = await blockerIssue.attachments();
|
|
58
|
+
const prAttachment = attachments.nodes.find((a) => {
|
|
59
|
+
if (!a.url)
|
|
60
|
+
return false;
|
|
61
|
+
let hostname;
|
|
62
|
+
try {
|
|
63
|
+
hostname = new URL(a.url).hostname;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return hostname === "github.com" && a.url.includes("/pull/");
|
|
69
|
+
});
|
|
70
|
+
if (!prAttachment?.url) {
|
|
71
|
+
unmerged.push(`${identifier} (no PR found)`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const match = prAttachment.url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
|
|
75
|
+
if (!match) {
|
|
76
|
+
unmerged.push(`${identifier} (unrecognized PR URL)`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const [, repo, prNum] = match;
|
|
80
|
+
try {
|
|
81
|
+
const { stdout } = await execFileAsync("gh", ["pr", "view", prNum, "--repo", repo, "--json", "state", "--jq", ".state"], { timeout: 10000 });
|
|
82
|
+
if (stdout.trim() !== "MERGED") {
|
|
83
|
+
unmerged.push(identifier);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
unmerged.push(`${identifier} (gh check failed)`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
92
|
+
unmerged.push(`(blocker check failed: ${errMsg})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (unmerged.length > 0) {
|
|
96
|
+
return {
|
|
97
|
+
passed: false,
|
|
98
|
+
output: `Blocked by unmerged: ${unmerged.join(", ")}`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { passed: true, output: `All ${blockers.length} blockers merged` };
|
|
102
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom gate: checks whether all Linear blockers for an entity's issue
|
|
3
|
+
* have corresponding merged PRs on GitHub.
|
|
4
|
+
*
|
|
5
|
+
* Referenced by seed as: "gates/blocking-graph.ts:isUnblocked"
|
|
6
|
+
*
|
|
7
|
+
* NOTE: Function gates are not yet evaluated by the engine (gate-evaluator.ts
|
|
8
|
+
* throws for type "function"). This file exists as the implementation target
|
|
9
|
+
* for when function gate support lands.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: This file is intentionally outside src/ — it is loaded dynamically at
|
|
12
|
+
* runtime, not compiled by the main build.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import { LinearClient } from "@linear/sdk";
|
|
18
|
+
import type { Entity } from "../src/repositories/interfaces.js";
|
|
19
|
+
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
|
|
22
|
+
export interface BlockingGraphResult {
|
|
23
|
+
passed: boolean;
|
|
24
|
+
output: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if all blocking issues for the given entity have merged PRs.
|
|
29
|
+
*
|
|
30
|
+
* Expects entity.refs.linear.id to be the Linear issue ID.
|
|
31
|
+
* Uses LINEAR_API_KEY env var for authentication.
|
|
32
|
+
* Uses `gh` CLI to check PR merge status on GitHub.
|
|
33
|
+
*/
|
|
34
|
+
export async function isUnblocked(entity: Entity): Promise<BlockingGraphResult> {
|
|
35
|
+
const linearApiKey = process.env.LINEAR_API_KEY;
|
|
36
|
+
if (!linearApiKey) {
|
|
37
|
+
return { passed: false, output: "LINEAR_API_KEY not set" };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const issueId = entity.refs?.linear?.id as string | undefined;
|
|
41
|
+
if (!issueId) {
|
|
42
|
+
return { passed: false, output: "Entity has no linear ref" };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const client = new LinearClient({ apiKey: linearApiKey });
|
|
46
|
+
let issue: Awaited<ReturnType<LinearClient["issue"]>>;
|
|
47
|
+
let inverseRelations: Awaited<ReturnType<typeof issue.inverseRelations>>;
|
|
48
|
+
try {
|
|
49
|
+
issue = await client.issue(issueId);
|
|
50
|
+
inverseRelations = await issue.inverseRelations();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
return { passed: false, output: `Linear API error: ${message}` };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const blockers = inverseRelations.nodes.filter((r) => r.type === "blocks");
|
|
57
|
+
|
|
58
|
+
if (blockers.length === 0) {
|
|
59
|
+
return { passed: true, output: "No blockers" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const unmerged: string[] = [];
|
|
63
|
+
|
|
64
|
+
for (const relation of blockers) {
|
|
65
|
+
try {
|
|
66
|
+
const blockerIssue = await relation.issue;
|
|
67
|
+
if (!blockerIssue) continue;
|
|
68
|
+
|
|
69
|
+
const identifier = blockerIssue.identifier;
|
|
70
|
+
|
|
71
|
+
// Resolve the PR via Linear attachment — the attachment URL tells us which repo
|
|
72
|
+
const attachments = await blockerIssue.attachments();
|
|
73
|
+
const prAttachment = attachments.nodes.find((a) => {
|
|
74
|
+
if (!a.url) return false;
|
|
75
|
+
let hostname: string;
|
|
76
|
+
try {
|
|
77
|
+
hostname = new URL(a.url).hostname;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return hostname === "github.com" && a.url.includes("/pull/");
|
|
82
|
+
});
|
|
83
|
+
if (!prAttachment?.url) {
|
|
84
|
+
unmerged.push(`${identifier} (no PR found)`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const match = prAttachment.url.match(
|
|
88
|
+
/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/,
|
|
89
|
+
);
|
|
90
|
+
if (!match) {
|
|
91
|
+
unmerged.push(`${identifier} (unrecognized PR URL)`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const [, repo, prNum] = match;
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await execFileAsync(
|
|
97
|
+
"gh",
|
|
98
|
+
["pr", "view", prNum, "--repo", repo, "--json", "state", "--jq", ".state"],
|
|
99
|
+
{ timeout: 10000 },
|
|
100
|
+
);
|
|
101
|
+
if (stdout.trim() !== "MERGED") {
|
|
102
|
+
unmerged.push(identifier);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
unmerged.push(`${identifier} (gh check failed)`);
|
|
106
|
+
}
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
109
|
+
unmerged.push(`(blocker check failed: ${errMsg})`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (unmerged.length > 0) {
|
|
114
|
+
return {
|
|
115
|
+
passed: false,
|
|
116
|
+
output: `Blocked by unmerged: ${unmerged.join(", ")}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { passed: true, output: `All ${blockers.length} blockers merged` };
|
|
121
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: check-design-posted — verify design spec comment on Linear issue
|
|
3
|
+
# Usage: gates/check-design-posted.sh <linear-issue-id>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
LINEAR_ID="${1:?Usage: check-design-posted.sh <linear-issue-id>}"
|
|
7
|
+
LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
|
|
8
|
+
|
|
9
|
+
PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{
|
|
10
|
+
"query": "query($id: String!) { issue(id: $id) { comments { nodes { body } } } }",
|
|
11
|
+
"variables": {"id": $id}
|
|
12
|
+
}')
|
|
13
|
+
RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
|
|
14
|
+
-H "Authorization: Bearer ${LINEAR_API_KEY}" \
|
|
15
|
+
-H "Content-Type: application/json" \
|
|
16
|
+
-d "$PAYLOAD" 2>&1) || {
|
|
17
|
+
echo "Failed to query Linear API: $RESPONSE"
|
|
18
|
+
exit 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
|
|
22
|
+
echo "GraphQL error: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown"')"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
if [ "$(echo "$RESPONSE" | jq '.data.issue')" = "null" ]; then
|
|
27
|
+
echo "ERROR: Linear issue $LINEAR_ID not found"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
COMMENTS=$(echo "$RESPONSE" | jq -r '.data.issue.comments.nodes[].body // empty')
|
|
32
|
+
|
|
33
|
+
if echo "$COMMENTS" | grep -qiE '(palette|typography|responsive|design spec|color scheme|breakpoint)'; then
|
|
34
|
+
echo "Design comment found on issue $LINEAR_ID"
|
|
35
|
+
exit 0
|
|
36
|
+
else
|
|
37
|
+
echo "No design comment found on issue $LINEAR_ID"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: check-merge — poll PR until merged, fail on CI failure in merge queue
|
|
3
|
+
# Usage: gates/check-merge.sh <pr-number> <repo>
|
|
4
|
+
# Timeout is handled by defcon's gate evaluator (execFile timeout).
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
PR="${1:?Usage: check-merge.sh <pr-number> <repo>}"
|
|
8
|
+
REPO="${2:?Usage: check-merge.sh <pr-number> <repo>}"
|
|
9
|
+
|
|
10
|
+
# Trap SIGTERM from gate evaluator timeout for clean exit
|
|
11
|
+
trap 'echo "Timed out waiting for PR #$PR to merge"; exit 1' TERM
|
|
12
|
+
|
|
13
|
+
while true; do
|
|
14
|
+
STATUS=$(gh pr view "$PR" --repo "$REPO" --json state,mergeStateStatus --jq '{state: .state, mergeStateStatus: .mergeStateStatus}' 2>/dev/null) || {
|
|
15
|
+
echo "Failed to query PR status"
|
|
16
|
+
sleep 30
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
if ! echo "$STATUS" | jq . >/dev/null 2>&1; then
|
|
20
|
+
echo "ERROR: invalid JSON from gh pr view"
|
|
21
|
+
sleep 30
|
|
22
|
+
continue
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
STATE=$(echo "$STATUS" | jq -r '.state')
|
|
26
|
+
MERGE_STATUS=$(echo "$STATUS" | jq -r '.mergeStateStatus')
|
|
27
|
+
|
|
28
|
+
case "$STATE" in
|
|
29
|
+
MERGED)
|
|
30
|
+
echo "PR #$PR merged in $REPO"
|
|
31
|
+
exit 0
|
|
32
|
+
;;
|
|
33
|
+
CLOSED)
|
|
34
|
+
echo "PR #$PR was closed without merging in $REPO"
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
|
|
39
|
+
# Check for CI failure in merge queue
|
|
40
|
+
if [ "$MERGE_STATUS" = "DIRTY" ] || [ "$MERGE_STATUS" = "BLOCKED" ] || [ "$MERGE_STATUS" = "UNSTABLE" ]; then
|
|
41
|
+
RESULT=$(gh pr view "$PR" --repo "$REPO" --json state,mergeStateStatus,statusCheckRollup \
|
|
42
|
+
--jq '{state: .state, mergeStatus: .mergeStateStatus, failing: [.statusCheckRollup // [] | .[] | select(.conclusion == "FAILURE") | .name]}' 2>/dev/null) || continue
|
|
43
|
+
FAILING=$(echo "$RESULT" | jq -r '.failing[]' 2>/dev/null)
|
|
44
|
+
if [ -n "$FAILING" ]; then
|
|
45
|
+
echo "CI failing in merge queue: $FAILING"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
sleep 30
|
|
51
|
+
done
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: check-pr-capacity — exit 0 if open PR count < max
|
|
3
|
+
# Usage: gates/check-pr-capacity.sh <repo> [max]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
REPO="${1:?Usage: check-pr-capacity.sh <repo>}"
|
|
7
|
+
MAX="${2:-4}"
|
|
8
|
+
|
|
9
|
+
COUNT=$(gh pr list --repo "$REPO" --state open --json number --jq 'length')
|
|
10
|
+
|
|
11
|
+
if [ "$COUNT" -lt "$MAX" ]; then
|
|
12
|
+
echo "PR capacity OK: $COUNT open (max $MAX)"
|
|
13
|
+
exit 0
|
|
14
|
+
else
|
|
15
|
+
echo "PR capacity exceeded: $COUNT open (max $MAX)"
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: check-review-ready — CI green AND bot reviewers posted
|
|
3
|
+
# Usage: gates/check-review-ready.sh <pr-number> <repo>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PR="${1:?Usage: check-review-ready.sh <pr-number> <repo>}"
|
|
7
|
+
REPO="${2:?Usage: check-review-ready.sh <pr-number> <repo>}"
|
|
8
|
+
|
|
9
|
+
# Check 1: All CI checks passing
|
|
10
|
+
echo "Checking CI status..."
|
|
11
|
+
CHECKS=$(gh pr view "$PR" --repo "$REPO" --json statusCheckRollup --jq '.statusCheckRollup' 2>/dev/null) || { echo "ERROR: failed to query statusCheckRollup for PR #$PR"; exit 1; }
|
|
12
|
+
FAILING=$(echo "$CHECKS" | jq -r '[.[] | select(.conclusion == "FAILURE") | .name] | .[]' 2>/dev/null)
|
|
13
|
+
if [ -n "$FAILING" ]; then
|
|
14
|
+
echo "CI checks failing for PR #$PR in $REPO: $FAILING"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
PENDING=$(echo "$CHECKS" | jq -r '[.[] | select(.status == "IN_PROGRESS" or .status == "QUEUED" or .status == "PENDING") | .name] | .[]' 2>/dev/null)
|
|
18
|
+
if [ -n "$PENDING" ]; then
|
|
19
|
+
echo "CI checks still pending for PR #$PR in $REPO: $PENDING"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
echo "CI checks passed"
|
|
23
|
+
|
|
24
|
+
# Check 2: Bot reviewers posted
|
|
25
|
+
echo "Checking for review bot comments..."
|
|
26
|
+
COMMENTS=$(gh api "repos/$REPO/issues/$PR/comments" --jq '.[].user.login' 2>/dev/null || echo "")
|
|
27
|
+
PR_COMMENTS=$(gh api "repos/$REPO/pulls/$PR/comments" --jq '.[].user.login' 2>/dev/null || echo "")
|
|
28
|
+
REVIEWS=$(gh api "repos/$REPO/pulls/$PR/reviews" --jq '.[].user.login' 2>/dev/null || echo "")
|
|
29
|
+
ALL_AUTHORS=$(printf '%s\n%s\n%s' "$COMMENTS" "$PR_COMMENTS" "$REVIEWS" | sort -u)
|
|
30
|
+
|
|
31
|
+
BOTS_FOUND=0
|
|
32
|
+
BOTS_MISSING=()
|
|
33
|
+
for BOT in "qodo-code-review[bot]" "coderabbitai[bot]" "sourcery-ai[bot]" "devin-ai-integration[bot]"; do
|
|
34
|
+
if echo "$ALL_AUTHORS" | grep -qF "$BOT"; then
|
|
35
|
+
BOTS_FOUND=$((BOTS_FOUND + 1))
|
|
36
|
+
else
|
|
37
|
+
BOTS_MISSING+=("$BOT")
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
if [ "$BOTS_FOUND" -eq 0 ]; then
|
|
42
|
+
echo "No review bots have posted on PR #$PR: missing ${BOTS_MISSING[*]}"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
echo "Review ready: CI green, $BOTS_FOUND bot(s) posted"
|
|
47
|
+
exit 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: check-spec-posted — verify implementation spec comment on Linear issue
|
|
3
|
+
# Usage: gates/check-spec-posted.sh <linear-issue-id>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
LINEAR_ID="${1:?Usage: check-spec-posted.sh <linear-issue-id>}"
|
|
7
|
+
LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
|
|
8
|
+
|
|
9
|
+
PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{
|
|
10
|
+
"query": "query($id: String!) { issue(id: $id) { comments { nodes { body } } } }",
|
|
11
|
+
"variables": {"id": $id}
|
|
12
|
+
}')
|
|
13
|
+
RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
|
|
14
|
+
-H "Authorization: Bearer ${LINEAR_API_KEY}" \
|
|
15
|
+
-H "Content-Type: application/json" \
|
|
16
|
+
-d "$PAYLOAD" 2>&1) || {
|
|
17
|
+
echo "Failed to query Linear API: $RESPONSE"
|
|
18
|
+
exit 1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
|
|
22
|
+
echo "GraphQL error: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown"')"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
COMMENTS=$(echo "$RESPONSE" | jq -r '.data.issue.comments.nodes[].body // empty')
|
|
27
|
+
|
|
28
|
+
if echo "$COMMENTS" | grep -qE '## (Implementation Spec|Implementation|File Changes)'; then
|
|
29
|
+
echo "Spec comment found on issue $LINEAR_ID"
|
|
30
|
+
exit 0
|
|
31
|
+
else
|
|
32
|
+
echo "No spec comment found on issue $LINEAR_ID"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: check-unblocked — verify all blockedBy issues have merged PRs
|
|
3
|
+
# Usage: gates/check-unblocked.sh <linear-issue-id>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
LINEAR_ID="${1:?Usage: check-unblocked.sh <linear-issue-id>}"
|
|
7
|
+
LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
|
|
8
|
+
|
|
9
|
+
# Fetch inverse relations (issues that block this one)
|
|
10
|
+
PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{
|
|
11
|
+
"query": "query($id: String!) { issue(id: $id) { relations { nodes { type relatedIssue { identifier attachments { nodes { url } } } } } } }",
|
|
12
|
+
"variables": {"id": $id}
|
|
13
|
+
}')
|
|
14
|
+
|
|
15
|
+
RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
|
|
16
|
+
-H "Authorization: Bearer ${LINEAR_API_KEY}" \
|
|
17
|
+
-H "Content-Type: application/json" \
|
|
18
|
+
-d "$PAYLOAD" 2>&1) || {
|
|
19
|
+
echo "Failed to query Linear API: $RESPONSE"
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
|
|
24
|
+
echo "GraphQL error: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown"')"
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
UNMERGED=()
|
|
29
|
+
while IFS= read -r blocker; do
|
|
30
|
+
IDENTIFIER=$(echo "$blocker" | jq -r '.identifier')
|
|
31
|
+
# Find GitHub PR URL in attachments
|
|
32
|
+
PR_URL=$(echo "$blocker" | jq -r '.attachments.nodes // [] | .[] | select(.url | test("github.com/.*/pull/")) | .url' | head -1)
|
|
33
|
+
if [ -z "$PR_URL" ]; then
|
|
34
|
+
UNMERGED+=("${IDENTIFIER} (no PR)")
|
|
35
|
+
continue
|
|
36
|
+
fi
|
|
37
|
+
# Extract repo and PR number from URL
|
|
38
|
+
REPO=$(echo "$PR_URL" | sed -n 's|.*github.com/\([^/]*/[^/]*\)/pull/.*|\1|p')
|
|
39
|
+
PR_NUM=$(echo "$PR_URL" | sed -n 's|.*/pull/\([0-9]*\).*|\1|p')
|
|
40
|
+
if [ -z "$REPO" ] || [ -z "$PR_NUM" ]; then
|
|
41
|
+
UNMERGED+=("${IDENTIFIER} (bad PR URL)")
|
|
42
|
+
continue
|
|
43
|
+
fi
|
|
44
|
+
STATE=$(gh pr view "$PR_NUM" --repo "$REPO" --json state --jq '.state' 2>/dev/null || echo "UNKNOWN")
|
|
45
|
+
if [ "$STATE" != "MERGED" ]; then
|
|
46
|
+
UNMERGED+=("${IDENTIFIER}")
|
|
47
|
+
fi
|
|
48
|
+
done < <(echo "$RESPONSE" | jq -c '.data.issue.relations.nodes[] | select(.type == "isBlockedBy") | .relatedIssue')
|
|
49
|
+
|
|
50
|
+
if [ ${#UNMERGED[@]} -gt 0 ]; then
|
|
51
|
+
echo "Blocked by unmerged: ${UNMERGED[*]}"
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
echo "All blockers merged"
|
|
56
|
+
exit 0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: ci-green — wait for CI checks to pass on a PR
|
|
3
|
+
# Usage: gates/ci-green.sh <pr-number> <repo>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PR="${1:?Usage: ci-green.sh <pr-number> <repo>}"
|
|
7
|
+
REPO="${2:?Usage: ci-green.sh <pr-number> <repo>}"
|
|
8
|
+
|
|
9
|
+
gh pr checks "$PR" --repo "$REPO" --watch --fail-fast 2>&1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: merge-queue — watch PR through merge queue
|
|
3
|
+
# Usage: gates/merge-queue.sh <pr-number> <repo>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PR="${1:?Usage: merge-queue.sh <pr-number> <repo>}"
|
|
7
|
+
REPO="${2:?Usage: merge-queue.sh <pr-number> <repo>}"
|
|
8
|
+
|
|
9
|
+
WATCH_SCRIPT="${WOPR_PR_WATCH_SCRIPT:-${WOPR_SCRIPTS_DIR:-$HOME}/wopr-pr-watch.sh}"
|
|
10
|
+
if [ ! -x "$WATCH_SCRIPT" ]; then
|
|
11
|
+
echo "ERROR: WOPR_PR_WATCH_SCRIPT not found or not executable: $WATCH_SCRIPT" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
"$WATCH_SCRIPT" "$PR" "$REPO" 2>&1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: review-bots-ready — wait for review bots to post
|
|
3
|
+
# Usage: gates/review-bots-ready.sh <pr-number> <repo>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PR="${1:?Usage: review-bots-ready.sh <pr-number> <repo>}"
|
|
7
|
+
REPO="${2:?Usage: review-bots-ready.sh <pr-number> <repo>}"
|
|
8
|
+
|
|
9
|
+
"${WOPR_AWAIT_REVIEWS_SCRIPT:-${WOPR_SCRIPTS_DIR:-$HOME}/wopr-await-reviews.sh}" "$PR" "$REPO" 2>&1
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Gate: spec-posted — verify architect spec comment exists on Linear issue
|
|
3
|
+
# Usage: gates/spec-posted.sh <linear-issue-id>
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
LINEAR_ID="${1:?Usage: spec-posted.sh <linear-issue-id>}"
|
|
7
|
+
|
|
8
|
+
# Query Linear API for comments on the issue containing "Implementation Spec"
|
|
9
|
+
LINEAR_API_KEY="${LINEAR_API_KEY:?LINEAR_API_KEY env var is required}"
|
|
10
|
+
PAYLOAD=$(jq -n --arg id "$LINEAR_ID" '{"query": "query { issue(id: \($id)) { comments { nodes { body } } } }"}')
|
|
11
|
+
RESPONSE=$(curl -s -f -X POST "https://api.linear.app/graphql" \
|
|
12
|
+
-H "Authorization: ${LINEAR_API_KEY}" \
|
|
13
|
+
-H "Content-Type: application/json" \
|
|
14
|
+
-d "$PAYLOAD" 2>&1) || {
|
|
15
|
+
echo "Failed to query Linear API: $RESPONSE"
|
|
16
|
+
exit 1
|
|
17
|
+
}
|
|
18
|
+
if echo "$RESPONSE" | jq -e '.errors' > /dev/null 2>&1; then
|
|
19
|
+
echo "GraphQL error from Linear API: $(echo "$RESPONSE" | jq -r '.errors[0].message // "unknown error"')"
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
COMMENTS=$(echo "$RESPONSE" | jq -r '.data.issue.comments.nodes[].body // empty')
|
|
24
|
+
|
|
25
|
+
if echo "$COMMENTS" | grep -q "Implementation Spec"; then
|
|
26
|
+
echo "Spec comment found on issue $LINEAR_ID"
|
|
27
|
+
exit 0
|
|
28
|
+
else
|
|
29
|
+
echo "No spec comment found on issue $LINEAR_ID"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function check(): unknown;
|