@renseiai/agentfactory 0.8.7 → 0.8.9
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/dist/src/config/index.d.ts +1 -1
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/config/repository-config.d.ts +37 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +47 -0
- package/dist/src/config/repository-config.test.js +140 -1
- package/dist/src/governor/decision-engine.d.ts +3 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +11 -0
- package/dist/src/governor/decision-engine.test.js +33 -0
- package/dist/src/governor/event-types.d.ts +18 -1
- package/dist/src/governor/event-types.d.ts.map +1 -1
- package/dist/src/governor/event-types.js +4 -0
- package/dist/src/governor/governor-types.d.ts +1 -1
- package/dist/src/governor/governor-types.d.ts.map +1 -1
- package/dist/src/governor/governor.d.ts +17 -1
- package/dist/src/governor/governor.d.ts.map +1 -1
- package/dist/src/governor/governor.js +112 -1
- package/dist/src/governor/governor.test.js +155 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.js +243 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
- package/dist/src/merge-queue/index.d.ts +18 -0
- package/dist/src/merge-queue/index.d.ts.map +1 -0
- package/dist/src/merge-queue/index.js +28 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
- package/dist/src/merge-queue/types.d.ts +48 -0
- package/dist/src/merge-queue/types.d.ts.map +1 -0
- package/dist/src/merge-queue/types.js +8 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.js +235 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
- package/dist/src/orchestrator/context-manager.d.ts +72 -0
- package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.js +120 -0
- package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
- package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.test.js +137 -0
- package/dist/src/orchestrator/index.d.ts +8 -2
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +8 -1
- package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.d.ts +12 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +282 -2
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +6 -0
- package/dist/src/orchestrator/parse-work-result.test.js +19 -0
- package/dist/src/orchestrator/state-recovery.d.ts +21 -2
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.js +54 -2
- package/dist/src/orchestrator/state-recovery.test.js +106 -2
- package/dist/src/orchestrator/state-types.d.ts +62 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -1
- package/dist/src/orchestrator/state-types.js +5 -1
- package/dist/src/orchestrator/summary-builder.d.ts +47 -0
- package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.js +240 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.test.js +236 -0
- package/dist/src/orchestrator/types.d.ts +2 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/orchestrator/work-types.d.ts +1 -1
- package/dist/src/orchestrator/work-types.d.ts.map +1 -1
- package/dist/src/providers/index.d.ts +64 -1
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +132 -1
- package/dist/src/providers/index.test.js +340 -2
- package/dist/src/routing/index.d.ts +7 -0
- package/dist/src/routing/index.d.ts.map +1 -0
- package/dist/src/routing/index.js +6 -0
- package/dist/src/routing/observation-recorder.d.ts +19 -0
- package/dist/src/routing/observation-recorder.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.js +73 -0
- package/dist/src/routing/observation-recorder.test.d.ts +2 -0
- package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.test.js +322 -0
- package/dist/src/routing/observation-store.d.ts +40 -0
- package/dist/src/routing/observation-store.d.ts.map +1 -0
- package/dist/src/routing/observation-store.js +1 -0
- package/dist/src/routing/observation-store.test.d.ts +2 -0
- package/dist/src/routing/observation-store.test.d.ts.map +1 -0
- package/dist/src/routing/observation-store.test.js +138 -0
- package/dist/src/routing/posterior-store.d.ts +12 -0
- package/dist/src/routing/posterior-store.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.js +13 -0
- package/dist/src/routing/posterior-store.test.d.ts +2 -0
- package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.test.js +37 -0
- package/dist/src/routing/reward.d.ts +16 -0
- package/dist/src/routing/reward.d.ts.map +1 -0
- package/dist/src/routing/reward.js +29 -0
- package/dist/src/routing/reward.test.d.ts +2 -0
- package/dist/src/routing/reward.test.d.ts.map +1 -0
- package/dist/src/routing/reward.test.js +210 -0
- package/dist/src/routing/routing-engine.d.ts +20 -0
- package/dist/src/routing/routing-engine.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.js +113 -0
- package/dist/src/routing/routing-engine.test.d.ts +2 -0
- package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.test.js +310 -0
- package/dist/src/routing/types.d.ts +157 -0
- package/dist/src/routing/types.d.ts.map +1 -0
- package/dist/src/routing/types.js +68 -0
- package/dist/src/routing/types.test.d.ts +2 -0
- package/dist/src/routing/types.test.d.ts.map +1 -0
- package/dist/src/routing/types.test.js +184 -0
- package/dist/src/templates/registry.test.js +2 -2
- package/dist/src/templates/types.d.ts +5 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +3 -0
- package/dist/src/workflow/agent-cancellation.d.ts +37 -0
- package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.js +41 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.test.js +86 -0
- package/dist/src/workflow/branching-router.d.ts +38 -0
- package/dist/src/workflow/branching-router.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.js +52 -0
- package/dist/src/workflow/branching-router.test.d.ts +2 -0
- package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.test.js +209 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.js +46 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
- package/dist/src/workflow/duration.d.ts +28 -0
- package/dist/src/workflow/duration.d.ts.map +1 -0
- package/dist/src/workflow/duration.js +57 -0
- package/dist/src/workflow/duration.test.d.ts +2 -0
- package/dist/src/workflow/duration.test.d.ts.map +1 -0
- package/dist/src/workflow/duration.test.js +74 -0
- package/dist/src/workflow/expression/ast.d.ts +53 -0
- package/dist/src/workflow/expression/ast.d.ts.map +1 -0
- package/dist/src/workflow/expression/ast.js +8 -0
- package/dist/src/workflow/expression/context.d.ts +40 -0
- package/dist/src/workflow/expression/context.d.ts.map +1 -0
- package/dist/src/workflow/expression/context.js +37 -0
- package/dist/src/workflow/expression/evaluator.d.ts +28 -0
- package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.js +165 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.test.js +792 -0
- package/dist/src/workflow/expression/expression.test.d.ts +2 -0
- package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/expression.test.js +516 -0
- package/dist/src/workflow/expression/helpers.d.ts +21 -0
- package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
- package/dist/src/workflow/expression/helpers.js +56 -0
- package/dist/src/workflow/expression/index.d.ts +55 -0
- package/dist/src/workflow/expression/index.d.ts.map +1 -0
- package/dist/src/workflow/expression/index.js +71 -0
- package/dist/src/workflow/expression/lexer.d.ts +37 -0
- package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
- package/dist/src/workflow/expression/lexer.js +166 -0
- package/dist/src/workflow/expression/parser.d.ts +23 -0
- package/dist/src/workflow/expression/parser.d.ts.map +1 -0
- package/dist/src/workflow/expression/parser.js +181 -0
- package/dist/src/workflow/gate-state.d.ts +115 -0
- package/dist/src/workflow/gate-state.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.js +185 -0
- package/dist/src/workflow/gate-state.test.d.ts +2 -0
- package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.test.js +251 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.js +243 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
- package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
- package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.js +216 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.test.js +199 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.js +162 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
- package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
- package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.js +381 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.test.js +211 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.js +216 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
- package/dist/src/workflow/index.d.ts +31 -3
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +20 -1
- package/dist/src/workflow/parallelism-executor.d.ts +25 -0
- package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.js +53 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.test.js +191 -0
- package/dist/src/workflow/parallelism-types.d.ts +80 -0
- package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-types.js +8 -0
- package/dist/src/workflow/phase-context-injector.d.ts +29 -0
- package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.js +43 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.test.js +123 -0
- package/dist/src/workflow/phase-output-collector.d.ts +39 -0
- package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.js +141 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.test.js +179 -0
- package/dist/src/workflow/retry-resolver.d.ts +51 -0
- package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.js +70 -0
- package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
- package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.test.js +149 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
- package/dist/src/workflow/strategies/index.d.ts +4 -0
- package/dist/src/workflow/strategies/index.d.ts.map +1 -0
- package/dist/src/workflow/strategies/index.js +3 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.js +92 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
- package/dist/src/workflow/transition-engine.d.ts +3 -1
- package/dist/src/workflow/transition-engine.d.ts.map +1 -1
- package/dist/src/workflow/transition-engine.js +26 -7
- package/dist/src/workflow/transition-engine.test.js +215 -11
- package/dist/src/workflow/workflow-registry.d.ts +46 -1
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
- package/dist/src/workflow/workflow-registry.js +74 -0
- package/dist/src/workflow/workflow-registry.test.js +54 -0
- package/dist/src/workflow/workflow-types.d.ts +330 -12
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +100 -5
- package/dist/src/workflow/workflow-types.test.js +293 -2
- package/package.json +2 -2
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
import { DEFAULT_GOVERNOR_CONFIG } from './governor-types.js';
|
|
12
12
|
import { decideAction } from './decision-engine.js';
|
|
13
13
|
import { WorkflowRegistry } from '../workflow/workflow-registry.js';
|
|
14
|
+
import { ParallelismExecutor } from '../workflow/parallelism-executor.js';
|
|
15
|
+
import { FanOutStrategy, FanInStrategy, RaceStrategy } from '../workflow/strategies/index.js';
|
|
16
|
+
import { evaluateTransitions } from '../workflow/transition-engine.js';
|
|
14
17
|
// ---------------------------------------------------------------------------
|
|
15
18
|
// Logging
|
|
16
19
|
// ---------------------------------------------------------------------------
|
|
@@ -41,15 +44,26 @@ export class WorkflowGovernor {
|
|
|
41
44
|
deps;
|
|
42
45
|
callbacks;
|
|
43
46
|
workflowRegistry;
|
|
47
|
+
parallelismExecutor;
|
|
44
48
|
intervalHandle = null;
|
|
45
49
|
running = false;
|
|
46
50
|
scanning = false;
|
|
47
|
-
constructor(config, deps, callbacks) {
|
|
51
|
+
constructor(config, deps, callbacks, parallelismExecutor) {
|
|
48
52
|
const { workflow: workflowConfig, ...governorConfig } = config;
|
|
49
53
|
this.config = { ...DEFAULT_GOVERNOR_CONFIG, ...governorConfig };
|
|
50
54
|
this.deps = deps;
|
|
51
55
|
this.callbacks = callbacks ?? {};
|
|
52
56
|
this.workflowRegistry = WorkflowRegistry.create(workflowConfig);
|
|
57
|
+
// Use the provided executor or create a default one with all strategies registered
|
|
58
|
+
if (parallelismExecutor) {
|
|
59
|
+
this.parallelismExecutor = parallelismExecutor;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.parallelismExecutor = new ParallelismExecutor();
|
|
63
|
+
this.parallelismExecutor.registerStrategy('fan-out', new FanOutStrategy());
|
|
64
|
+
this.parallelismExecutor.registerStrategy('fan-in', new FanInStrategy());
|
|
65
|
+
this.parallelismExecutor.registerStrategy('race', new RaceStrategy());
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
// -------------------------------------------------------------------------
|
|
55
69
|
// Lifecycle
|
|
@@ -207,6 +221,18 @@ export class WorkflowGovernor {
|
|
|
207
221
|
break;
|
|
208
222
|
}
|
|
209
223
|
try {
|
|
224
|
+
// Handle parallel group dispatch separately
|
|
225
|
+
if (item.action === 'trigger-parallel-group') {
|
|
226
|
+
await this.handleParallelGroupDispatch(item.issue);
|
|
227
|
+
result.actionsDispatched++;
|
|
228
|
+
log.info('Dispatched parallel group action', {
|
|
229
|
+
issueIdentifier: item.issue.identifier,
|
|
230
|
+
action: item.action,
|
|
231
|
+
reason: item.reason,
|
|
232
|
+
priority: item.priority ?? 'none',
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
210
236
|
await this.deps.dispatchWork(item.issue, item.action);
|
|
211
237
|
result.actionsDispatched++;
|
|
212
238
|
log.info('Dispatched action', {
|
|
@@ -264,4 +290,89 @@ export class WorkflowGovernor {
|
|
|
264
290
|
};
|
|
265
291
|
return decideAction(ctx);
|
|
266
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Handle dispatching work for a parallel group.
|
|
295
|
+
*
|
|
296
|
+
* Fetches sub-issues for the parent, finds the applicable parallelism group,
|
|
297
|
+
* and executes the group using the ParallelismExecutor.
|
|
298
|
+
*/
|
|
299
|
+
async handleParallelGroupDispatch(issue) {
|
|
300
|
+
if (!this.deps.getSubIssues) {
|
|
301
|
+
log.warn('getSubIssues dependency not provided, falling back to normal dispatch');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const workflow = this.workflowRegistry.getWorkflow();
|
|
305
|
+
if (!workflow?.parallelism)
|
|
306
|
+
return;
|
|
307
|
+
const subIssues = await this.deps.getSubIssues(issue.id);
|
|
308
|
+
// Find the applicable parallelism group.
|
|
309
|
+
// Use the first group whose phases include a phase that can be
|
|
310
|
+
// resolved from the current workflow transitions.
|
|
311
|
+
const group = workflow.parallelism.find(g => g.phases.length > 0);
|
|
312
|
+
if (!group)
|
|
313
|
+
return;
|
|
314
|
+
const tasks = subIssues.map(sub => ({
|
|
315
|
+
id: sub.id,
|
|
316
|
+
issueId: sub.identifier,
|
|
317
|
+
phaseName: group.phases[0],
|
|
318
|
+
}));
|
|
319
|
+
const result = await this.parallelismExecutor.execute(group, tasks, async (task) => {
|
|
320
|
+
await this.deps.dispatchWork(subIssues.find(s => s.id === task.id), `trigger-${task.phaseName}`);
|
|
321
|
+
return {
|
|
322
|
+
id: task.id,
|
|
323
|
+
issueId: task.issueId,
|
|
324
|
+
success: true,
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
log.info('Parallel group dispatched', {
|
|
328
|
+
group: group.name,
|
|
329
|
+
strategy: group.strategy,
|
|
330
|
+
completed: result.completed.length,
|
|
331
|
+
failed: result.failed.length,
|
|
332
|
+
cancelled: result.cancelled.length,
|
|
333
|
+
});
|
|
334
|
+
// Propagate result to advance parent issue if group completed successfully
|
|
335
|
+
await this.handleParallelGroupCompletion(issue, result, group);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Handle post-parallel-group completion: if all tasks succeeded,
|
|
339
|
+
* evaluate transitions to advance the parent issue to the next phase.
|
|
340
|
+
*/
|
|
341
|
+
async handleParallelGroupCompletion(parentIssue, result, group) {
|
|
342
|
+
// Only advance if the group completed with no failures
|
|
343
|
+
const allSucceeded = result.failed.length === 0 && result.completed.length > 0
|
|
344
|
+
&& result.completed.every(t => t.success);
|
|
345
|
+
if (!allSucceeded) {
|
|
346
|
+
log.info('Parallel group has failures, not advancing parent', {
|
|
347
|
+
group: group.name,
|
|
348
|
+
failedCount: result.failed.length,
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
// Evaluate the next transition for the parent issue.
|
|
353
|
+
// After a parallel development group completes, the parent
|
|
354
|
+
// should transition based on its next status mapping.
|
|
355
|
+
const nextPhaseResult = evaluateTransitions({
|
|
356
|
+
issue: { ...parentIssue, status: 'Finished' },
|
|
357
|
+
registry: this.workflowRegistry,
|
|
358
|
+
isParentIssue: true,
|
|
359
|
+
});
|
|
360
|
+
if (nextPhaseResult.action !== 'none') {
|
|
361
|
+
log.info('Advancing parent after parallel group completion', {
|
|
362
|
+
issueIdentifier: parentIssue.identifier,
|
|
363
|
+
nextAction: nextPhaseResult.action,
|
|
364
|
+
reason: nextPhaseResult.reason,
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
await this.deps.dispatchWork(parentIssue, nextPhaseResult.action);
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
371
|
+
log.error('Failed to advance parent after parallel group', {
|
|
372
|
+
issueIdentifier: parentIssue.identifier,
|
|
373
|
+
error: errorMsg,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
267
378
|
}
|
|
@@ -512,3 +512,158 @@ describe('WorkflowGovernor.scanOnce — PRIORITY override sorting', () => {
|
|
|
512
512
|
expect(dispatchWork.mock.calls[2][0]).toEqual(expect.objectContaining({ id: 'c' }));
|
|
513
513
|
});
|
|
514
514
|
});
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// Parallel group dispatch
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
describe('WorkflowGovernor.scanOnce — parallel group dispatch', () => {
|
|
519
|
+
function makeWorkflowWithParallelism() {
|
|
520
|
+
return {
|
|
521
|
+
apiVersion: 'v1.1',
|
|
522
|
+
kind: 'WorkflowDefinition',
|
|
523
|
+
metadata: { name: 'test-parallel' },
|
|
524
|
+
phases: [
|
|
525
|
+
{ name: 'development', template: 'development' },
|
|
526
|
+
{ name: 'qa', template: 'qa' },
|
|
527
|
+
],
|
|
528
|
+
transitions: [
|
|
529
|
+
{ from: 'Backlog', to: 'development' },
|
|
530
|
+
{ from: 'Finished', to: 'qa' },
|
|
531
|
+
],
|
|
532
|
+
parallelism: [
|
|
533
|
+
{
|
|
534
|
+
name: 'dev-parallel',
|
|
535
|
+
phases: ['development'],
|
|
536
|
+
strategy: 'fan-out',
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
it('handles trigger-parallel-group by dispatching sub-issues', async () => {
|
|
542
|
+
const parentIssue = makeIssue({
|
|
543
|
+
id: 'parent-1',
|
|
544
|
+
identifier: 'SUP-100',
|
|
545
|
+
status: 'Backlog',
|
|
546
|
+
});
|
|
547
|
+
const subIssues = [
|
|
548
|
+
makeIssue({ id: 'sub-1', identifier: 'SUP-101', status: 'Backlog', parentId: 'parent-1' }),
|
|
549
|
+
makeIssue({ id: 'sub-2', identifier: 'SUP-102', status: 'Backlog', parentId: 'parent-1' }),
|
|
550
|
+
];
|
|
551
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
552
|
+
const getSubIssues = vi.fn().mockResolvedValue(subIssues);
|
|
553
|
+
const deps = makeMockDeps({
|
|
554
|
+
listIssues: vi.fn().mockResolvedValue([parentIssue]),
|
|
555
|
+
isParentIssue: vi.fn().mockResolvedValue(true),
|
|
556
|
+
dispatchWork,
|
|
557
|
+
getSubIssues,
|
|
558
|
+
});
|
|
559
|
+
const governor = new WorkflowGovernor(makeConfig({
|
|
560
|
+
workflow: { workflow: makeWorkflowWithParallelism(), useBuiltinDefault: false },
|
|
561
|
+
}), deps);
|
|
562
|
+
const results = await governor.scanOnce();
|
|
563
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
564
|
+
expect(getSubIssues).toHaveBeenCalledWith('parent-1');
|
|
565
|
+
// dispatchWork is called for each sub-issue via the ParallelismExecutor,
|
|
566
|
+
// plus once more to advance the parent to the next phase (trigger-qa)
|
|
567
|
+
expect(dispatchWork).toHaveBeenCalledTimes(3);
|
|
568
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'sub-1' }), 'trigger-development');
|
|
569
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'sub-2' }), 'trigger-development');
|
|
570
|
+
// Parent advanced to QA after parallel group completion
|
|
571
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'parent-1' }), 'trigger-qa');
|
|
572
|
+
});
|
|
573
|
+
it('handles missing getSubIssues gracefully', async () => {
|
|
574
|
+
const parentIssue = makeIssue({
|
|
575
|
+
id: 'parent-1',
|
|
576
|
+
identifier: 'SUP-100',
|
|
577
|
+
status: 'Backlog',
|
|
578
|
+
});
|
|
579
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
580
|
+
const deps = makeMockDeps({
|
|
581
|
+
listIssues: vi.fn().mockResolvedValue([parentIssue]),
|
|
582
|
+
isParentIssue: vi.fn().mockResolvedValue(true),
|
|
583
|
+
dispatchWork,
|
|
584
|
+
// getSubIssues intentionally not provided
|
|
585
|
+
});
|
|
586
|
+
const governor = new WorkflowGovernor(makeConfig({
|
|
587
|
+
workflow: { workflow: makeWorkflowWithParallelism(), useBuiltinDefault: false },
|
|
588
|
+
}), deps);
|
|
589
|
+
const results = await governor.scanOnce();
|
|
590
|
+
// The action is counted as dispatched (the parallel group handler ran),
|
|
591
|
+
// but dispatchWork is NOT called for sub-issues since getSubIssues is missing
|
|
592
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
593
|
+
expect(dispatchWork).not.toHaveBeenCalled();
|
|
594
|
+
});
|
|
595
|
+
it('fan-in group completing advances parent to next phase', async () => {
|
|
596
|
+
const fanInWorkflow = {
|
|
597
|
+
apiVersion: 'v1.1',
|
|
598
|
+
kind: 'WorkflowDefinition',
|
|
599
|
+
metadata: { name: 'test-fan-in' },
|
|
600
|
+
phases: [
|
|
601
|
+
{ name: 'development', template: 'development' },
|
|
602
|
+
{ name: 'qa', template: 'qa' },
|
|
603
|
+
],
|
|
604
|
+
transitions: [
|
|
605
|
+
{ from: 'Backlog', to: 'development' },
|
|
606
|
+
{ from: 'Finished', to: 'qa' },
|
|
607
|
+
],
|
|
608
|
+
parallelism: [
|
|
609
|
+
{
|
|
610
|
+
name: 'dev-fan-in',
|
|
611
|
+
phases: ['development'],
|
|
612
|
+
strategy: 'fan-in',
|
|
613
|
+
waitForAll: true,
|
|
614
|
+
},
|
|
615
|
+
],
|
|
616
|
+
};
|
|
617
|
+
const parentIssue = makeIssue({
|
|
618
|
+
id: 'parent-1',
|
|
619
|
+
identifier: 'SUP-200',
|
|
620
|
+
status: 'Backlog',
|
|
621
|
+
});
|
|
622
|
+
const subIssues = [
|
|
623
|
+
makeIssue({ id: 'sub-1', identifier: 'SUP-201', status: 'Backlog', parentId: 'parent-1' }),
|
|
624
|
+
makeIssue({ id: 'sub-2', identifier: 'SUP-202', status: 'Backlog', parentId: 'parent-1' }),
|
|
625
|
+
makeIssue({ id: 'sub-3', identifier: 'SUP-203', status: 'Backlog', parentId: 'parent-1' }),
|
|
626
|
+
];
|
|
627
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
628
|
+
const getSubIssues = vi.fn().mockResolvedValue(subIssues);
|
|
629
|
+
const deps = makeMockDeps({
|
|
630
|
+
listIssues: vi.fn().mockResolvedValue([parentIssue]),
|
|
631
|
+
isParentIssue: vi.fn().mockResolvedValue(true),
|
|
632
|
+
dispatchWork,
|
|
633
|
+
getSubIssues,
|
|
634
|
+
});
|
|
635
|
+
const governor = new WorkflowGovernor(makeConfig({
|
|
636
|
+
workflow: { workflow: fanInWorkflow, useBuiltinDefault: false },
|
|
637
|
+
}), deps);
|
|
638
|
+
const results = await governor.scanOnce();
|
|
639
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
640
|
+
// All 3 sub-issues dispatched + 1 parent advancement to QA
|
|
641
|
+
expect(dispatchWork).toHaveBeenCalledTimes(4);
|
|
642
|
+
// Sub-issues dispatched for development
|
|
643
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'sub-1' }), 'trigger-development');
|
|
644
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'sub-2' }), 'trigger-development');
|
|
645
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'sub-3' }), 'trigger-development');
|
|
646
|
+
// Parent advanced to QA after all fan-in tasks completed
|
|
647
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'parent-1' }), 'trigger-qa');
|
|
648
|
+
});
|
|
649
|
+
it('non-parent issues with parallelism groups dispatch normally', async () => {
|
|
650
|
+
const issue = makeIssue({
|
|
651
|
+
id: 'issue-1',
|
|
652
|
+
identifier: 'SUP-100',
|
|
653
|
+
status: 'Backlog',
|
|
654
|
+
});
|
|
655
|
+
const dispatchWork = vi.fn().mockResolvedValue(undefined);
|
|
656
|
+
const deps = makeMockDeps({
|
|
657
|
+
listIssues: vi.fn().mockResolvedValue([issue]),
|
|
658
|
+
isParentIssue: vi.fn().mockResolvedValue(false),
|
|
659
|
+
dispatchWork,
|
|
660
|
+
});
|
|
661
|
+
const governor = new WorkflowGovernor(makeConfig({
|
|
662
|
+
workflow: { workflow: makeWorkflowWithParallelism(), useBuiltinDefault: false },
|
|
663
|
+
}), deps);
|
|
664
|
+
const results = await governor.scanOnce();
|
|
665
|
+
expect(results[0].actionsDispatched).toBe(1);
|
|
666
|
+
// Normal dispatch, not parallel
|
|
667
|
+
expect(dispatchWork).toHaveBeenCalledWith(expect.objectContaining({ id: 'issue-1' }), 'trigger-development');
|
|
668
|
+
});
|
|
669
|
+
});
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA;AACpC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA;AACjC,cAAc,qBAAqB,CAAA;AACnC,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA;AACpC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,mBAAmB,CAAA;AACjC,cAAc,qBAAqB,CAAA;AACnC,cAAc,kBAAkB,CAAA;AAChC,cAAc,qBAAqB,CAAA;AACnC,cAAc,oBAAoB,CAAA"}
|
package/dist/src/index.js
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Native Merge Queue Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements MergeQueueAdapter for GitHub's built-in merge queue feature.
|
|
5
|
+
* Uses `gh api graphql` CLI for all GitHub API interactions.
|
|
6
|
+
*/
|
|
7
|
+
import type { MergeQueueAdapter, MergeQueueStatus } from '../types.js';
|
|
8
|
+
export declare class GitHubNativeMergeQueueAdapter implements MergeQueueAdapter {
|
|
9
|
+
readonly name: "github-native";
|
|
10
|
+
canEnqueue(owner: string, repo: string, prNumber: number): Promise<boolean>;
|
|
11
|
+
enqueue(owner: string, repo: string, prNumber: number): Promise<MergeQueueStatus>;
|
|
12
|
+
getStatus(owner: string, repo: string, prNumber: number): Promise<MergeQueueStatus>;
|
|
13
|
+
dequeue(owner: string, repo: string, prNumber: number): Promise<void>;
|
|
14
|
+
isEnabled(owner: string, repo: string): Promise<boolean>;
|
|
15
|
+
/** Get the GraphQL node ID for a PR */
|
|
16
|
+
private getPRNodeId;
|
|
17
|
+
/** Execute a GraphQL query/mutation via gh CLI with retry */
|
|
18
|
+
private graphql;
|
|
19
|
+
/** Map GitHub merge queue entry to our MergeQueueStatus */
|
|
20
|
+
private mapEntryToStatus;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=github-native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-native.d.ts","sourceRoot":"","sources":["../../../../src/merge-queue/adapters/github-native.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAatE,qBAAa,6BAA8B,YAAW,iBAAiB;IACrE,QAAQ,CAAC,IAAI,EAAG,eAAe,CAAS;IAElC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA6C3E,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAwCjF,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+FnF,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBrE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmC9D,uCAAuC;YACzB,WAAW;IAiBzB,6DAA6D;YAC/C,OAAO;IAgCrB,2DAA2D;IAC3D,OAAO,CAAC,gBAAgB;CAgBzB"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Native Merge Queue Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements MergeQueueAdapter for GitHub's built-in merge queue feature.
|
|
5
|
+
* Uses `gh api graphql` CLI for all GitHub API interactions.
|
|
6
|
+
*/
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
/** Timeout for GitHub API calls (30s) */
|
|
11
|
+
const GH_API_TIMEOUT = 30000;
|
|
12
|
+
/** Maximum retries for transient failures */
|
|
13
|
+
const MAX_RETRIES = 2;
|
|
14
|
+
/** Backoff delay between retries (ms) */
|
|
15
|
+
const RETRY_DELAY = 1000;
|
|
16
|
+
export class GitHubNativeMergeQueueAdapter {
|
|
17
|
+
name = 'github-native';
|
|
18
|
+
async canEnqueue(owner, repo, prNumber) {
|
|
19
|
+
try {
|
|
20
|
+
const query = `
|
|
21
|
+
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
22
|
+
repository(owner: $owner, name: $repo) {
|
|
23
|
+
pullRequest(number: $prNumber) {
|
|
24
|
+
mergeable
|
|
25
|
+
reviewDecision
|
|
26
|
+
mergeQueueEntry { state }
|
|
27
|
+
baseRef {
|
|
28
|
+
branchProtectionRule {
|
|
29
|
+
requiresStatusChecks
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
const result = await this.graphql(query, { owner, repo, prNumber });
|
|
37
|
+
const pr = result.repository.pullRequest;
|
|
38
|
+
// Already in queue
|
|
39
|
+
if (pr.mergeQueueEntry)
|
|
40
|
+
return false;
|
|
41
|
+
// Must be mergeable
|
|
42
|
+
if (pr.mergeable !== 'MERGEABLE')
|
|
43
|
+
return false;
|
|
44
|
+
// Must be approved (if reviews are required)
|
|
45
|
+
if (pr.reviewDecision && pr.reviewDecision !== 'APPROVED')
|
|
46
|
+
return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async enqueue(owner, repo, prNumber) {
|
|
54
|
+
// First get the PR's node ID
|
|
55
|
+
const prId = await this.getPRNodeId(owner, repo, prNumber);
|
|
56
|
+
const mutation = `
|
|
57
|
+
mutation($prId: ID!) {
|
|
58
|
+
enqueuePullRequest(input: { pullRequestId: $prId }) {
|
|
59
|
+
mergeQueueEntry {
|
|
60
|
+
state
|
|
61
|
+
position
|
|
62
|
+
headCommit { oid }
|
|
63
|
+
enqueuedAt
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
try {
|
|
69
|
+
const result = await this.graphql(mutation, { prId });
|
|
70
|
+
const entry = result.enqueuePullRequest.mergeQueueEntry;
|
|
71
|
+
return this.mapEntryToStatus(entry);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return {
|
|
75
|
+
state: 'failed',
|
|
76
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
77
|
+
checksStatus: [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async getStatus(owner, repo, prNumber) {
|
|
82
|
+
const query = `
|
|
83
|
+
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
84
|
+
repository(owner: $owner, name: $repo) {
|
|
85
|
+
pullRequest(number: $prNumber) {
|
|
86
|
+
mergeQueueEntry {
|
|
87
|
+
state
|
|
88
|
+
position
|
|
89
|
+
headCommit { oid }
|
|
90
|
+
enqueuedAt
|
|
91
|
+
}
|
|
92
|
+
commits(last: 1) {
|
|
93
|
+
nodes {
|
|
94
|
+
commit {
|
|
95
|
+
statusCheckRollup {
|
|
96
|
+
contexts(first: 50) {
|
|
97
|
+
nodes {
|
|
98
|
+
... on CheckRun {
|
|
99
|
+
name
|
|
100
|
+
conclusion
|
|
101
|
+
status
|
|
102
|
+
}
|
|
103
|
+
... on StatusContext {
|
|
104
|
+
context
|
|
105
|
+
state
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
`;
|
|
117
|
+
const result = await this.graphql(query, { owner, repo, prNumber });
|
|
118
|
+
const pr = result.repository.pullRequest;
|
|
119
|
+
if (!pr.mergeQueueEntry) {
|
|
120
|
+
return { state: 'not-queued', checksStatus: [] };
|
|
121
|
+
}
|
|
122
|
+
const status = this.mapEntryToStatus(pr.mergeQueueEntry);
|
|
123
|
+
// Map check statuses
|
|
124
|
+
const commitNode = pr.commits.nodes[0];
|
|
125
|
+
if (commitNode?.commit.statusCheckRollup) {
|
|
126
|
+
status.checksStatus = commitNode.commit.statusCheckRollup.contexts.nodes.map((node) => {
|
|
127
|
+
if ('name' in node) {
|
|
128
|
+
return {
|
|
129
|
+
name: node.name,
|
|
130
|
+
status: node.conclusion === 'SUCCESS' ? 'pass'
|
|
131
|
+
: node.status === 'COMPLETED' ? 'fail'
|
|
132
|
+
: 'pending',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
name: node.context,
|
|
137
|
+
status: node.state === 'SUCCESS' ? 'pass'
|
|
138
|
+
: node.state === 'PENDING' ? 'pending'
|
|
139
|
+
: 'fail',
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return status;
|
|
144
|
+
}
|
|
145
|
+
async dequeue(owner, repo, prNumber) {
|
|
146
|
+
const prId = await this.getPRNodeId(owner, repo, prNumber);
|
|
147
|
+
const mutation = `
|
|
148
|
+
mutation($prId: ID!) {
|
|
149
|
+
dequeuePullRequest(input: { pullRequestId: $prId }) {
|
|
150
|
+
mergeQueueEntry {
|
|
151
|
+
state
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
await this.graphql(mutation, { prId });
|
|
157
|
+
}
|
|
158
|
+
async isEnabled(owner, repo) {
|
|
159
|
+
try {
|
|
160
|
+
const query = `
|
|
161
|
+
query($owner: String!, $repo: String!) {
|
|
162
|
+
repository(owner: $owner, name: $repo) {
|
|
163
|
+
defaultBranchRef {
|
|
164
|
+
branchProtectionRule {
|
|
165
|
+
requiresMergeQueue
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
// Note: requiresMergeQueue might not be available in all GitHub API versions.
|
|
172
|
+
// Fallback: check if merge queue entries exist
|
|
173
|
+
const result = await this.graphql(query, { owner, repo });
|
|
174
|
+
return result.repository.defaultBranchRef?.branchProtectionRule?.requiresMergeQueue ?? false;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Private helpers
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
/** Get the GraphQL node ID for a PR */
|
|
184
|
+
async getPRNodeId(owner, repo, prNumber) {
|
|
185
|
+
const query = `
|
|
186
|
+
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
|
187
|
+
repository(owner: $owner, name: $repo) {
|
|
188
|
+
pullRequest(number: $prNumber) {
|
|
189
|
+
id
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
`;
|
|
194
|
+
const result = await this.graphql(query, { owner, repo, prNumber });
|
|
195
|
+
return result.repository.pullRequest.id;
|
|
196
|
+
}
|
|
197
|
+
/** Execute a GraphQL query/mutation via gh CLI with retry */
|
|
198
|
+
async graphql(query, variables) {
|
|
199
|
+
const varsArgs = Object.entries(variables)
|
|
200
|
+
.map(([key, value]) => {
|
|
201
|
+
if (typeof value === 'number') {
|
|
202
|
+
return `-F ${key}=${value}`;
|
|
203
|
+
}
|
|
204
|
+
return `-f ${key}=${String(value)}`;
|
|
205
|
+
})
|
|
206
|
+
.join(' ');
|
|
207
|
+
const command = `gh api graphql -f query='${query.replace(/'/g, "'\\''")}' ${varsArgs}`;
|
|
208
|
+
let lastError = null;
|
|
209
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
210
|
+
try {
|
|
211
|
+
const { stdout } = await execAsync(command, { timeout: GH_API_TIMEOUT });
|
|
212
|
+
const response = JSON.parse(stdout);
|
|
213
|
+
if (response.errors?.length) {
|
|
214
|
+
throw new Error(response.errors.map((e) => e.message).join('; '));
|
|
215
|
+
}
|
|
216
|
+
return response.data;
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
220
|
+
if (attempt < MAX_RETRIES) {
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY * (attempt + 1)));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
throw lastError;
|
|
226
|
+
}
|
|
227
|
+
/** Map GitHub merge queue entry to our MergeQueueStatus */
|
|
228
|
+
mapEntryToStatus(entry) {
|
|
229
|
+
const stateMap = {
|
|
230
|
+
QUEUED: 'queued',
|
|
231
|
+
AWAITING_CHECKS: 'queued',
|
|
232
|
+
MERGEABLE: 'merging',
|
|
233
|
+
MERGED: 'merged',
|
|
234
|
+
UNMERGEABLE: 'failed',
|
|
235
|
+
LOCKED: 'blocked',
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
state: stateMap[entry.state] ?? 'not-queued',
|
|
239
|
+
position: entry.position,
|
|
240
|
+
checksStatus: [],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-native.test.d.ts","sourceRoot":"","sources":["../../../../src/merge-queue/adapters/github-native.test.ts"],"names":[],"mappings":""}
|