@diff-review-system/drs 4.0.0-rc.4 → 4.0.1
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/.pi/agents/describe/pr-describer.md +14 -0
- package/.pi/agents/review/unified-reviewer.md +31 -1
- package/.pi/agents/task/agents-md-updater.md +3 -1
- package/.pi/agents/task/review-issue-fixer.md +18 -1
- package/.pi/agents/visual/pr-explainer.md +205 -0
- package/.pi/workflows/github-pr-describe.yaml +10 -7
- package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +148 -0
- package/.pi/workflows/github-pr-post-comment.yaml +10 -10
- package/.pi/workflows/github-pr-review-post.yaml +19 -8
- package/.pi/workflows/github-pr-review.yaml +348 -7
- package/.pi/workflows/github-pr-show-changes.yaml +8 -8
- package/.pi/workflows/github-pr-update-agents-md-stacked.yaml +103 -0
- package/.pi/workflows/github-pr-visual-explain.yaml +35 -0
- package/.pi/workflows/gitlab-mr-describe.yaml +8 -5
- package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +144 -0
- package/.pi/workflows/gitlab-mr-post-comment.yaml +8 -8
- package/.pi/workflows/gitlab-mr-review.yaml +348 -5
- package/.pi/workflows/gitlab-mr-show-changes.yaml +6 -6
- package/.pi/workflows/gitlab-mr-update-agents-md-stacked.yaml +100 -0
- package/.pi/workflows/gitlab-mr-visual-explain.yaml +33 -0
- package/.pi/workflows/local-fix-review-issues.yaml +82 -13
- package/.pi/workflows/local-review.yaml +9 -2
- package/.pi/workflows/local-update-agents-md.yaml +1 -1
- package/.pi/workflows/local-visual-explain.yaml +31 -0
- package/.pi/workflows/release-changelog-finalize.yaml +47 -0
- package/.pi/workflows/tag-changelog-update.yaml +4 -4
- package/README.md +91 -27
- package/dist/ci/runner.d.ts.map +1 -1
- package/dist/ci/runner.js +3 -1
- package/dist/ci/runner.js.map +1 -1
- package/dist/cli/index.js +48 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/run-agent.d.ts +2 -0
- package/dist/cli/run-agent.d.ts.map +1 -1
- package/dist/cli/run-agent.js +4 -0
- package/dist/cli/run-agent.js.map +1 -1
- package/dist/cli/workflow.d.ts +56 -2
- package/dist/cli/workflow.d.ts.map +1 -1
- package/dist/cli/workflow.js +2165 -85
- package/dist/cli/workflow.js.map +1 -1
- package/dist/github/client.d.ts +12 -0
- package/dist/github/client.d.ts.map +1 -1
- package/dist/github/client.js +27 -0
- package/dist/github/client.js.map +1 -1
- package/dist/github/platform-adapter.d.ts +6 -1
- package/dist/github/platform-adapter.d.ts.map +1 -1
- package/dist/github/platform-adapter.js +84 -8
- package/dist/github/platform-adapter.js.map +1 -1
- package/dist/gitlab/client.d.ts +11 -0
- package/dist/gitlab/client.d.ts.map +1 -1
- package/dist/gitlab/client.js +11 -0
- package/dist/gitlab/client.js.map +1 -1
- package/dist/gitlab/platform-adapter.d.ts +3 -1
- package/dist/gitlab/platform-adapter.d.ts.map +1 -1
- package/dist/gitlab/platform-adapter.js +32 -1
- package/dist/gitlab/platform-adapter.js.map +1 -1
- package/dist/lib/comment-formatter.d.ts +8 -0
- package/dist/lib/comment-formatter.d.ts.map +1 -1
- package/dist/lib/comment-formatter.js +12 -4
- package/dist/lib/comment-formatter.js.map +1 -1
- package/dist/lib/comment-poster.d.ts.map +1 -1
- package/dist/lib/comment-poster.js +28 -1
- package/dist/lib/comment-poster.js.map +1 -1
- package/dist/lib/config.d.ts +50 -11
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +163 -28
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/context-compression.d.ts +10 -0
- package/dist/lib/context-compression.d.ts.map +1 -1
- package/dist/lib/context-compression.js +101 -13
- package/dist/lib/context-compression.js.map +1 -1
- package/dist/lib/context-loader.d.ts +2 -1
- package/dist/lib/context-loader.d.ts.map +1 -1
- package/dist/lib/context-loader.js +70 -1
- package/dist/lib/context-loader.js.map +1 -1
- package/dist/lib/describe-core.d.ts.map +1 -1
- package/dist/lib/describe-core.js +3 -2
- package/dist/lib/describe-core.js.map +1 -1
- package/dist/lib/diff-lines.d.ts +9 -0
- package/dist/lib/diff-lines.d.ts.map +1 -1
- package/dist/lib/diff-lines.js +17 -9
- package/dist/lib/diff-lines.js.map +1 -1
- package/dist/lib/exit.js +4 -4
- package/dist/lib/exit.js.map +1 -1
- package/dist/lib/html-artifact.d.ts +14 -0
- package/dist/lib/html-artifact.d.ts.map +1 -0
- package/dist/lib/html-artifact.js +59 -0
- package/dist/lib/html-artifact.js.map +1 -0
- package/dist/lib/issue-parser.js +3 -3
- package/dist/lib/issue-parser.js.map +1 -1
- package/dist/lib/json-output-schema.d.ts +70 -0
- package/dist/lib/json-output-schema.d.ts.map +1 -1
- package/dist/lib/json-output-schema.js +40 -0
- package/dist/lib/json-output-schema.js.map +1 -1
- package/dist/lib/platform-client.d.ts +26 -0
- package/dist/lib/platform-client.d.ts.map +1 -1
- package/dist/lib/review-artifact.d.ts +69 -0
- package/dist/lib/review-artifact.d.ts.map +1 -0
- package/dist/lib/review-artifact.js +171 -0
- package/dist/lib/review-artifact.js.map +1 -0
- package/dist/lib/review-core.d.ts +6 -4
- package/dist/lib/review-core.d.ts.map +1 -1
- package/dist/lib/review-core.js +71 -151
- package/dist/lib/review-core.js.map +1 -1
- package/dist/lib/review-orchestrator.d.ts +23 -0
- package/dist/lib/review-orchestrator.d.ts.map +1 -1
- package/dist/lib/review-orchestrator.js +20 -13
- package/dist/lib/review-orchestrator.js.map +1 -1
- package/dist/lib/review-usage.d.ts +4 -0
- package/dist/lib/review-usage.d.ts.map +1 -1
- package/dist/lib/review-usage.js +25 -0
- package/dist/lib/review-usage.js.map +1 -1
- package/dist/lib/trace-collector.d.ts +105 -0
- package/dist/lib/trace-collector.d.ts.map +1 -0
- package/dist/lib/trace-collector.js +255 -0
- package/dist/lib/trace-collector.js.map +1 -0
- package/dist/lib/trace-html.d.ts +3 -0
- package/dist/lib/trace-html.d.ts.map +1 -0
- package/dist/lib/trace-html.js +349 -0
- package/dist/lib/trace-html.js.map +1 -0
- package/dist/lib/workflow-artifacts.d.ts +54 -0
- package/dist/lib/workflow-artifacts.d.ts.map +1 -0
- package/dist/lib/workflow-artifacts.js +150 -0
- package/dist/lib/workflow-artifacts.js.map +1 -0
- package/dist/pi/sdk.d.ts.map +1 -1
- package/dist/pi/sdk.js +570 -6
- package/dist/pi/sdk.js.map +1 -1
- package/dist/runtime/agent-loader.js +2 -2
- package/dist/runtime/client.d.ts +2 -0
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +11 -5
- package/dist/runtime/client.js.map +1 -1
- package/package.json +21 -15
- package/.pi/workflows/github-pr-describe-post.yaml +0 -24
- package/.pi/workflows/gitlab-mr-describe-post.yaml +0 -22
- package/.pi/workflows/gitlab-mr-review-code-quality.yaml +0 -31
- package/.pi/workflows/gitlab-mr-review-post-code-quality.yaml +0 -40
- package/.pi/workflows/gitlab-mr-review-post.yaml +0 -30
- package/.pi/workflows/local-staged-review.yaml +0 -17
- package/dist/cli/run-agent.test.d.ts +0 -2
- package/dist/cli/run-agent.test.d.ts.map +0 -1
- package/dist/cli/run-agent.test.js +0 -204
- package/dist/cli/run-agent.test.js.map +0 -1
- package/dist/cli/workflow.test.d.ts +0 -2
- package/dist/cli/workflow.test.d.ts.map +0 -1
- package/dist/cli/workflow.test.js +0 -1410
- package/dist/cli/workflow.test.js.map +0 -1
- package/dist/github/client.test.d.ts +0 -2
- package/dist/github/client.test.d.ts.map +0 -1
- package/dist/github/client.test.js +0 -206
- package/dist/github/client.test.js.map +0 -1
- package/dist/github/platform-adapter.test.d.ts +0 -2
- package/dist/github/platform-adapter.test.d.ts.map +0 -1
- package/dist/github/platform-adapter.test.js +0 -40
- package/dist/github/platform-adapter.test.js.map +0 -1
- package/dist/gitlab/diff-parser.test.d.ts +0 -2
- package/dist/gitlab/diff-parser.test.d.ts.map +0 -1
- package/dist/gitlab/diff-parser.test.js +0 -315
- package/dist/gitlab/diff-parser.test.js.map +0 -1
- package/dist/gitlab/platform-adapter.test.d.ts +0 -2
- package/dist/gitlab/platform-adapter.test.d.ts.map +0 -1
- package/dist/gitlab/platform-adapter.test.js +0 -21
- package/dist/gitlab/platform-adapter.test.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -7
- package/dist/index.test.js.map +0 -1
- package/dist/lib/code-quality-report.test.d.ts +0 -2
- package/dist/lib/code-quality-report.test.d.ts.map +0 -1
- package/dist/lib/code-quality-report.test.js +0 -327
- package/dist/lib/code-quality-report.test.js.map +0 -1
- package/dist/lib/comment-formatter.test.d.ts +0 -2
- package/dist/lib/comment-formatter.test.d.ts.map +0 -1
- package/dist/lib/comment-formatter.test.js +0 -727
- package/dist/lib/comment-formatter.test.js.map +0 -1
- package/dist/lib/comment-manager.test.d.ts +0 -2
- package/dist/lib/comment-manager.test.d.ts.map +0 -1
- package/dist/lib/comment-manager.test.js +0 -680
- package/dist/lib/comment-manager.test.js.map +0 -1
- package/dist/lib/comment-poster.test.d.ts +0 -5
- package/dist/lib/comment-poster.test.d.ts.map +0 -1
- package/dist/lib/comment-poster.test.js +0 -255
- package/dist/lib/comment-poster.test.js.map +0 -1
- package/dist/lib/config-model-overrides.test.d.ts +0 -2
- package/dist/lib/config-model-overrides.test.d.ts.map +0 -1
- package/dist/lib/config-model-overrides.test.js +0 -218
- package/dist/lib/config-model-overrides.test.js.map +0 -1
- package/dist/lib/config.test.d.ts +0 -2
- package/dist/lib/config.test.d.ts.map +0 -1
- package/dist/lib/config.test.js +0 -353
- package/dist/lib/config.test.js.map +0 -1
- package/dist/lib/context-compression.test.d.ts +0 -2
- package/dist/lib/context-compression.test.d.ts.map +0 -1
- package/dist/lib/context-compression.test.js +0 -337
- package/dist/lib/context-compression.test.js.map +0 -1
- package/dist/lib/context-loader.test.d.ts +0 -2
- package/dist/lib/context-loader.test.d.ts.map +0 -1
- package/dist/lib/context-loader.test.js +0 -212
- package/dist/lib/context-loader.test.js.map +0 -1
- package/dist/lib/cursor-fix-link.test.d.ts +0 -2
- package/dist/lib/cursor-fix-link.test.d.ts.map +0 -1
- package/dist/lib/cursor-fix-link.test.js +0 -70
- package/dist/lib/cursor-fix-link.test.js.map +0 -1
- package/dist/lib/describe-core.test.d.ts +0 -2
- package/dist/lib/describe-core.test.d.ts.map +0 -1
- package/dist/lib/describe-core.test.js +0 -208
- package/dist/lib/describe-core.test.js.map +0 -1
- package/dist/lib/describe-output-path.test.d.ts +0 -2
- package/dist/lib/describe-output-path.test.d.ts.map +0 -1
- package/dist/lib/describe-output-path.test.js +0 -51
- package/dist/lib/describe-output-path.test.js.map +0 -1
- package/dist/lib/describe-parser.test.d.ts +0 -2
- package/dist/lib/describe-parser.test.d.ts.map +0 -1
- package/dist/lib/describe-parser.test.js +0 -282
- package/dist/lib/describe-parser.test.js.map +0 -1
- package/dist/lib/description-executor.test.d.ts +0 -2
- package/dist/lib/description-executor.test.d.ts.map +0 -1
- package/dist/lib/description-executor.test.js +0 -135
- package/dist/lib/description-executor.test.js.map +0 -1
- package/dist/lib/description-formatter.test.d.ts +0 -2
- package/dist/lib/description-formatter.test.d.ts.map +0 -1
- package/dist/lib/description-formatter.test.js +0 -57
- package/dist/lib/description-formatter.test.js.map +0 -1
- package/dist/lib/diff-lines.test.d.ts +0 -2
- package/dist/lib/diff-lines.test.d.ts.map +0 -1
- package/dist/lib/diff-lines.test.js +0 -13
- package/dist/lib/diff-lines.test.js.map +0 -1
- package/dist/lib/diff-parser.test.d.ts +0 -2
- package/dist/lib/diff-parser.test.d.ts.map +0 -1
- package/dist/lib/diff-parser.test.js +0 -335
- package/dist/lib/diff-parser.test.js.map +0 -1
- package/dist/lib/error-comment-poster.test.d.ts +0 -2
- package/dist/lib/error-comment-poster.test.d.ts.map +0 -1
- package/dist/lib/error-comment-poster.test.js +0 -128
- package/dist/lib/error-comment-poster.test.js.map +0 -1
- package/dist/lib/exit.test.d.ts +0 -2
- package/dist/lib/exit.test.d.ts.map +0 -1
- package/dist/lib/exit.test.js +0 -120
- package/dist/lib/exit.test.js.map +0 -1
- package/dist/lib/issue-parser.test.d.ts +0 -2
- package/dist/lib/issue-parser.test.d.ts.map +0 -1
- package/dist/lib/issue-parser.test.js +0 -281
- package/dist/lib/issue-parser.test.js.map +0 -1
- package/dist/lib/json-output-schema.test.d.ts +0 -2
- package/dist/lib/json-output-schema.test.d.ts.map +0 -1
- package/dist/lib/json-output-schema.test.js +0 -92
- package/dist/lib/json-output-schema.test.js.map +0 -1
- package/dist/lib/json-output.test.d.ts +0 -2
- package/dist/lib/json-output.test.d.ts.map +0 -1
- package/dist/lib/json-output.test.js +0 -141
- package/dist/lib/json-output.test.js.map +0 -1
- package/dist/lib/logger.test.d.ts +0 -2
- package/dist/lib/logger.test.d.ts.map +0 -1
- package/dist/lib/logger.test.js +0 -324
- package/dist/lib/logger.test.js.map +0 -1
- package/dist/lib/position-validator.test.d.ts +0 -2
- package/dist/lib/position-validator.test.d.ts.map +0 -1
- package/dist/lib/position-validator.test.js +0 -128
- package/dist/lib/position-validator.test.js.map +0 -1
- package/dist/lib/prompt-budget.test.d.ts +0 -2
- package/dist/lib/prompt-budget.test.d.ts.map +0 -1
- package/dist/lib/prompt-budget.test.js +0 -55
- package/dist/lib/prompt-budget.test.js.map +0 -1
- package/dist/lib/repository-validator.test.d.ts +0 -5
- package/dist/lib/repository-validator.test.d.ts.map +0 -1
- package/dist/lib/repository-validator.test.js +0 -341
- package/dist/lib/repository-validator.test.js.map +0 -1
- package/dist/lib/review-core.test.d.ts +0 -2
- package/dist/lib/review-core.test.d.ts.map +0 -1
- package/dist/lib/review-core.test.js +0 -614
- package/dist/lib/review-core.test.js.map +0 -1
- package/dist/lib/review-orchestrator.test.d.ts +0 -2
- package/dist/lib/review-orchestrator.test.d.ts.map +0 -1
- package/dist/lib/review-orchestrator.test.js +0 -552
- package/dist/lib/review-orchestrator.test.js.map +0 -1
- package/dist/lib/review-output-path.test.d.ts +0 -2
- package/dist/lib/review-output-path.test.d.ts.map +0 -1
- package/dist/lib/review-output-path.test.js +0 -83
- package/dist/lib/review-output-path.test.js.map +0 -1
- package/dist/lib/review-parser.test.d.ts +0 -2
- package/dist/lib/review-parser.test.d.ts.map +0 -1
- package/dist/lib/review-parser.test.js +0 -130
- package/dist/lib/review-parser.test.js.map +0 -1
- package/dist/lib/review-usage.test.d.ts +0 -2
- package/dist/lib/review-usage.test.d.ts.map +0 -1
- package/dist/lib/review-usage.test.js +0 -83
- package/dist/lib/review-usage.test.js.map +0 -1
- package/dist/lib/unified-review-executor.d.ts +0 -58
- package/dist/lib/unified-review-executor.d.ts.map +0 -1
- package/dist/lib/unified-review-executor.js +0 -201
- package/dist/lib/unified-review-executor.js.map +0 -1
- package/dist/lib/unified-review-executor.test.d.ts +0 -5
- package/dist/lib/unified-review-executor.test.d.ts.map +0 -1
- package/dist/lib/unified-review-executor.test.js +0 -472
- package/dist/lib/unified-review-executor.test.js.map +0 -1
- package/dist/lib/write-json-output.test.d.ts +0 -2
- package/dist/lib/write-json-output.test.d.ts.map +0 -1
- package/dist/lib/write-json-output.test.js +0 -259
- package/dist/lib/write-json-output.test.js.map +0 -1
- package/dist/pi/sdk.test.d.ts +0 -2
- package/dist/pi/sdk.test.d.ts.map +0 -1
- package/dist/pi/sdk.test.js +0 -488
- package/dist/pi/sdk.test.js.map +0 -1
- package/dist/runtime/agent-loader.test.d.ts +0 -2
- package/dist/runtime/agent-loader.test.d.ts.map +0 -1
- package/dist/runtime/agent-loader.test.js +0 -277
- package/dist/runtime/agent-loader.test.js.map +0 -1
- package/dist/runtime/client.test.d.ts +0 -2
- package/dist/runtime/client.test.d.ts.map +0 -1
- package/dist/runtime/client.test.js +0 -772
- package/dist/runtime/client.test.js.map +0 -1
- package/dist/runtime/path-config.test.d.ts +0 -2
- package/dist/runtime/path-config.test.d.ts.map +0 -1
- package/dist/runtime/path-config.test.js +0 -112
- package/dist/runtime/path-config.test.js.map +0 -1
package/dist/cli/workflow.js
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
2
|
-
import { dirname } from 'path';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
3
|
import simpleGit from 'simple-git';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { getDescriberModelOverride, loadWorkflowSourceInfo, normalizeAgentConfig, resolveAgentRunConfig, } from '../lib/config.js';
|
|
5
|
+
import { getDescriberModelOverride, getReviewAgentIds, loadWorkflowSourceInfo, normalizeAgentConfig, resolveAgentRunConfig, } from '../lib/config.js';
|
|
6
6
|
import { resolveWithinWorkingDir } from '../lib/path-utils.js';
|
|
7
7
|
import { parseDiff, getChangedFiles, getFilesWithDiffs } from '../lib/diff-parser.js';
|
|
8
|
-
import {
|
|
8
|
+
import { parseDiffLineInfo } from '../lib/diff-lines.js';
|
|
9
9
|
import { connectToRuntime, executeReview, filterIgnoredFiles, } from '../lib/review-orchestrator.js';
|
|
10
|
-
import { ExitError, setExitHandler } from '../lib/exit.js';
|
|
11
10
|
import { postReviewComments } from '../lib/comment-poster.js';
|
|
12
|
-
import { findExistingCommentById } from '../lib/comment-manager.js';
|
|
11
|
+
import { findExistingCommentById, createIssueFingerprint } from '../lib/comment-manager.js';
|
|
13
12
|
import { removeErrorComment } from '../lib/error-comment-poster.js';
|
|
14
13
|
import { runDescribeIfEnabled } from '../lib/description-executor.js';
|
|
15
14
|
import { buildBaseInstructions } from '../lib/review-core.js';
|
|
16
15
|
import { resolveCursorFixLinkOptions } from '../lib/cursor-fix-link.js';
|
|
17
|
-
import {
|
|
16
|
+
import { extractHtmlDocument, parseArtifactOutputPointer, readArtifactOutputPointer, validateHtmlArtifact, } from '../lib/html-artifact.js';
|
|
17
|
+
import { getCanonicalDiffCommand, resolveBaseBranch } from '../lib/repository-validator.js';
|
|
18
18
|
import { formatCodeQualityReport, generateCodeQualityReport } from '../lib/code-quality-report.js';
|
|
19
19
|
import { createGitHubClient } from '../github/client.js';
|
|
20
20
|
import { GitHubPlatformAdapter } from '../github/platform-adapter.js';
|
|
21
21
|
import { createGitLabClient } from '../gitlab/client.js';
|
|
22
22
|
import { GitLabPlatformAdapter } from '../gitlab/platform-adapter.js';
|
|
23
|
+
import { loadWorkflowArtifact, saveWorkflowArtifact, updateWorkflowArtifact, workflowArtifactExists, } from '../lib/workflow-artifacts.js';
|
|
24
|
+
import { addReviewArtifactFinding, createReviewArtifactPayload, getReviewArtifactStatus, isReviewArtifactPayload, updateReviewArtifactFindings, } from '../lib/review-artifact.js';
|
|
23
25
|
import { runAgent } from './run-agent.js';
|
|
26
|
+
import { TraceCollector } from '../lib/trace-collector.js';
|
|
27
|
+
import { renderTraceHtml } from '../lib/trace-html.js';
|
|
24
28
|
function createWorkflowLock() {
|
|
25
29
|
return { current: Promise.resolve() };
|
|
26
30
|
}
|
|
@@ -85,6 +89,321 @@ function getNodeNeeds(node) {
|
|
|
85
89
|
}
|
|
86
90
|
return node.needs;
|
|
87
91
|
}
|
|
92
|
+
function getControlTargets(node) {
|
|
93
|
+
const targets = [];
|
|
94
|
+
if (node.target)
|
|
95
|
+
targets.push(node.target);
|
|
96
|
+
if (node.exit)
|
|
97
|
+
targets.push(node.exit);
|
|
98
|
+
if (node.default)
|
|
99
|
+
targets.push(node.default);
|
|
100
|
+
if (node.cases)
|
|
101
|
+
targets.push(...Object.values(node.cases));
|
|
102
|
+
return targets;
|
|
103
|
+
}
|
|
104
|
+
function validateWorkflowControlTargets(nodes) {
|
|
105
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
106
|
+
for (const target of getControlTargets(node)) {
|
|
107
|
+
if (!nodes[target]) {
|
|
108
|
+
throw new Error(`Workflow node "${nodeId}" targets unknown node "${target}".`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function validateWorkflowControlRouteDirection(nodes, executionOrder) {
|
|
114
|
+
const segments = splitWorkflowSegments(nodes, executionOrder);
|
|
115
|
+
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
|
|
116
|
+
const segment = segments[segmentIndex];
|
|
117
|
+
if (segment.type !== 'control') {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const node = nodes[segment.nodeId];
|
|
121
|
+
if (!node || node.control === 'loop') {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
for (const target of getControlTargets(node)) {
|
|
125
|
+
const targetIndex = findWorkflowSegmentIndex(segments, target);
|
|
126
|
+
if (targetIndex <= segmentIndex) {
|
|
127
|
+
throw new Error(`Workflow control node "${segment.nodeId}" cannot jump backward to "${target}". Use control: loop with maxIterations for repeated execution.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function validateWorkflowPassThroughShape(nodeId, node) {
|
|
133
|
+
if (node.control !== 'passThrough') {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!node.target) {
|
|
137
|
+
throw new Error(`Workflow passThrough node "${nodeId}" must define "target".`);
|
|
138
|
+
}
|
|
139
|
+
const forbiddenKeys = [
|
|
140
|
+
'if',
|
|
141
|
+
'then',
|
|
142
|
+
'else',
|
|
143
|
+
'exit',
|
|
144
|
+
'cases',
|
|
145
|
+
'default',
|
|
146
|
+
'maxIterations',
|
|
147
|
+
'onMaxIterations',
|
|
148
|
+
'value',
|
|
149
|
+
];
|
|
150
|
+
const extraFields = forbiddenKeys.filter((key) => key in node);
|
|
151
|
+
if (extraFields.length > 0) {
|
|
152
|
+
throw new Error(`Workflow passThrough node "${nodeId}" must not define extra control logic: ${extraFields.join(', ')}.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const WORKFLOW_NODE_FIELDS = new Set([
|
|
156
|
+
'agent',
|
|
157
|
+
'agentsFrom',
|
|
158
|
+
'control',
|
|
159
|
+
'action',
|
|
160
|
+
'with',
|
|
161
|
+
'needs',
|
|
162
|
+
'if',
|
|
163
|
+
'target',
|
|
164
|
+
'exit',
|
|
165
|
+
'maxIterations',
|
|
166
|
+
'onMaxIterations',
|
|
167
|
+
'value',
|
|
168
|
+
'cases',
|
|
169
|
+
'default',
|
|
170
|
+
'input',
|
|
171
|
+
'output',
|
|
172
|
+
'writes',
|
|
173
|
+
'json',
|
|
174
|
+
]);
|
|
175
|
+
const EXECUTABLE_NODE_FIELDS = new Set([
|
|
176
|
+
'agent',
|
|
177
|
+
'agentsFrom',
|
|
178
|
+
'action',
|
|
179
|
+
'with',
|
|
180
|
+
'needs',
|
|
181
|
+
'if',
|
|
182
|
+
'input',
|
|
183
|
+
'output',
|
|
184
|
+
'writes',
|
|
185
|
+
'json',
|
|
186
|
+
]);
|
|
187
|
+
const CONTROL_NODE_FIELDS = {
|
|
188
|
+
loop: new Set([
|
|
189
|
+
'control',
|
|
190
|
+
'needs',
|
|
191
|
+
'if',
|
|
192
|
+
'target',
|
|
193
|
+
'exit',
|
|
194
|
+
'maxIterations',
|
|
195
|
+
'onMaxIterations',
|
|
196
|
+
'output',
|
|
197
|
+
]),
|
|
198
|
+
switch: new Set(['control', 'needs', 'value', 'cases', 'default', 'output']),
|
|
199
|
+
end: new Set(['control', 'needs', 'output']),
|
|
200
|
+
passThrough: new Set(['control', 'needs', 'target', 'output']),
|
|
201
|
+
};
|
|
202
|
+
const ACTION_OPTION_FIELDS = {
|
|
203
|
+
write: new Set(),
|
|
204
|
+
'git-diff': new Set(['staged']),
|
|
205
|
+
'git-add': new Set(['path', 'paths']),
|
|
206
|
+
'git-branch': new Set(['name', 'from', 'force']),
|
|
207
|
+
'git-commit': new Set(['message', 'path', 'paths']),
|
|
208
|
+
'git-push': new Set(['remote', 'branch', 'remoteBranch', 'setUpstream', 'force']),
|
|
209
|
+
'has-diff': new Set(['path', 'paths']),
|
|
210
|
+
'stack-guard': new Set(['source', 'allowStackedSource', 'reservedPrefixes']),
|
|
211
|
+
'review-threshold': new Set(['review', 'severity', 'minIssues']),
|
|
212
|
+
'save-artifact': new Set([
|
|
213
|
+
'kind',
|
|
214
|
+
'source',
|
|
215
|
+
'artifact',
|
|
216
|
+
'payload',
|
|
217
|
+
'platform',
|
|
218
|
+
'project',
|
|
219
|
+
'projectId',
|
|
220
|
+
'owner',
|
|
221
|
+
'repo',
|
|
222
|
+
'subject',
|
|
223
|
+
'changeKind',
|
|
224
|
+
'changeNumber',
|
|
225
|
+
'pr',
|
|
226
|
+
'mr',
|
|
227
|
+
'branch',
|
|
228
|
+
]),
|
|
229
|
+
'load-artifact': new Set([
|
|
230
|
+
'kind',
|
|
231
|
+
'source',
|
|
232
|
+
'id',
|
|
233
|
+
'platform',
|
|
234
|
+
'project',
|
|
235
|
+
'projectId',
|
|
236
|
+
'owner',
|
|
237
|
+
'repo',
|
|
238
|
+
'subject',
|
|
239
|
+
'changeKind',
|
|
240
|
+
'changeNumber',
|
|
241
|
+
'pr',
|
|
242
|
+
'mr',
|
|
243
|
+
'branch',
|
|
244
|
+
]),
|
|
245
|
+
'artifact-exists': new Set([
|
|
246
|
+
'kind',
|
|
247
|
+
'source',
|
|
248
|
+
'id',
|
|
249
|
+
'platform',
|
|
250
|
+
'project',
|
|
251
|
+
'projectId',
|
|
252
|
+
'owner',
|
|
253
|
+
'repo',
|
|
254
|
+
'subject',
|
|
255
|
+
'changeKind',
|
|
256
|
+
'changeNumber',
|
|
257
|
+
'pr',
|
|
258
|
+
'mr',
|
|
259
|
+
'branch',
|
|
260
|
+
]),
|
|
261
|
+
'create-review-artifact': new Set(['source', 'review']),
|
|
262
|
+
'review-artifact-status': new Set(['artifact']),
|
|
263
|
+
'review-artifact-add-finding': new Set(['artifact', 'issue', 'source']),
|
|
264
|
+
'review-artifact-update-findings': new Set([
|
|
265
|
+
'artifact',
|
|
266
|
+
'state',
|
|
267
|
+
'disposition',
|
|
268
|
+
'ids',
|
|
269
|
+
'fingerprints',
|
|
270
|
+
'severity',
|
|
271
|
+
]),
|
|
272
|
+
'review-artifact-promote-finding': new Set(['artifact', 'ids', 'fingerprints', 'severity']),
|
|
273
|
+
'review-artifact-resolve-finding': new Set(['artifact', 'ids', 'fingerprints', 'severity']),
|
|
274
|
+
'verify-fix': new Set(['artifact', 'review', 'fixChange', 'severity', 'minIssues']),
|
|
275
|
+
'create-change-request': new Set([
|
|
276
|
+
'platform',
|
|
277
|
+
'owner',
|
|
278
|
+
'repo',
|
|
279
|
+
'project',
|
|
280
|
+
'projectId',
|
|
281
|
+
'sourceBranch',
|
|
282
|
+
'head',
|
|
283
|
+
'targetBranch',
|
|
284
|
+
'base',
|
|
285
|
+
'title',
|
|
286
|
+
'body',
|
|
287
|
+
'draft',
|
|
288
|
+
'reuseExisting',
|
|
289
|
+
]),
|
|
290
|
+
'create-pr': new Set([
|
|
291
|
+
'platform',
|
|
292
|
+
'owner',
|
|
293
|
+
'repo',
|
|
294
|
+
'project',
|
|
295
|
+
'projectId',
|
|
296
|
+
'sourceBranch',
|
|
297
|
+
'head',
|
|
298
|
+
'targetBranch',
|
|
299
|
+
'base',
|
|
300
|
+
'title',
|
|
301
|
+
'body',
|
|
302
|
+
'draft',
|
|
303
|
+
'reuseExisting',
|
|
304
|
+
]),
|
|
305
|
+
'create-mr': new Set([
|
|
306
|
+
'platform',
|
|
307
|
+
'owner',
|
|
308
|
+
'repo',
|
|
309
|
+
'project',
|
|
310
|
+
'projectId',
|
|
311
|
+
'sourceBranch',
|
|
312
|
+
'head',
|
|
313
|
+
'targetBranch',
|
|
314
|
+
'base',
|
|
315
|
+
'title',
|
|
316
|
+
'body',
|
|
317
|
+
'draft',
|
|
318
|
+
'reuseExisting',
|
|
319
|
+
]),
|
|
320
|
+
'change-source': new Set([
|
|
321
|
+
'type',
|
|
322
|
+
'staged',
|
|
323
|
+
'from',
|
|
324
|
+
'to',
|
|
325
|
+
'includePrereleaseFrom',
|
|
326
|
+
'owner',
|
|
327
|
+
'repo',
|
|
328
|
+
'pr',
|
|
329
|
+
'project',
|
|
330
|
+
'projectId',
|
|
331
|
+
'mr',
|
|
332
|
+
'mrIid',
|
|
333
|
+
'source',
|
|
334
|
+
'fixChange',
|
|
335
|
+
]),
|
|
336
|
+
review: new Set(['source', 'reviewArtifact', 'severity', 'artifact']),
|
|
337
|
+
'review-context': new Set(['source', 'file', 'baseBranch']),
|
|
338
|
+
describe: new Set(['source', 'post', 'postDescription']),
|
|
339
|
+
'code-quality-report': new Set(['review', 'path']),
|
|
340
|
+
'post-comment': new Set([
|
|
341
|
+
'source',
|
|
342
|
+
'platform',
|
|
343
|
+
'owner',
|
|
344
|
+
'repo',
|
|
345
|
+
'project',
|
|
346
|
+
'projectId',
|
|
347
|
+
'pr',
|
|
348
|
+
'mr',
|
|
349
|
+
'prNumber',
|
|
350
|
+
'mrIid',
|
|
351
|
+
'body',
|
|
352
|
+
'marker',
|
|
353
|
+
]),
|
|
354
|
+
'post-review-comments': new Set(['source', 'review', 'removeErrorComment']),
|
|
355
|
+
'post-fix-status': new Set([
|
|
356
|
+
'platform',
|
|
357
|
+
'owner',
|
|
358
|
+
'repo',
|
|
359
|
+
'project',
|
|
360
|
+
'projectId',
|
|
361
|
+
'pr',
|
|
362
|
+
'mr',
|
|
363
|
+
'source',
|
|
364
|
+
'reviewArtifact',
|
|
365
|
+
'fixReview',
|
|
366
|
+
'fixChange',
|
|
367
|
+
'severity',
|
|
368
|
+
'stackedPrUrl',
|
|
369
|
+
'marker',
|
|
370
|
+
]),
|
|
371
|
+
};
|
|
372
|
+
function validateAllowedFields(nodeId, value, allowed, subject) {
|
|
373
|
+
const unknownFields = Object.keys(value).filter((key) => !allowed.has(key));
|
|
374
|
+
if (unknownFields.length > 0) {
|
|
375
|
+
throw new Error(`Workflow ${subject} "${nodeId}" has unsupported field(s): ${unknownFields.join(', ')}.`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function validateWorkflowNodeShape(nodeId, node) {
|
|
379
|
+
validateAllowedFields(nodeId, node, WORKFLOW_NODE_FIELDS, 'node');
|
|
380
|
+
const kind = getNodeKind(node);
|
|
381
|
+
if (kind === 'control') {
|
|
382
|
+
const control = node.control;
|
|
383
|
+
if (!control || !CONTROL_NODE_FIELDS[control]) {
|
|
384
|
+
throw new Error(`Workflow control node "${nodeId}" has unsupported control "${String(control)}".`);
|
|
385
|
+
}
|
|
386
|
+
validateAllowedFields(nodeId, node, CONTROL_NODE_FIELDS[control], 'control node');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
validateAllowedFields(nodeId, node, EXECUTABLE_NODE_FIELDS, 'node');
|
|
390
|
+
if (node.action && node.with) {
|
|
391
|
+
const allowed = ACTION_OPTION_FIELDS[node.action];
|
|
392
|
+
if (!allowed) {
|
|
393
|
+
throw new Error(`Workflow action node "${nodeId}" has unsupported action "${node.action}".`);
|
|
394
|
+
}
|
|
395
|
+
validateAllowedFields(nodeId, node.with, allowed, `node "${nodeId}" with`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function validateWorkflowNodeKinds(nodes) {
|
|
399
|
+
for (const [nodeId, node] of Object.entries(nodes)) {
|
|
400
|
+
validateWorkflowNodeShape(nodeId, node);
|
|
401
|
+
validateWorkflowPassThroughShape(nodeId, node);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function hasWorkflowControlNodes(nodes) {
|
|
405
|
+
return Object.values(nodes).some((node) => node.control !== undefined);
|
|
406
|
+
}
|
|
88
407
|
function getWorkflowExecutionOrder(nodes) {
|
|
89
408
|
const nodeIds = Object.keys(nodes);
|
|
90
409
|
const visiting = new Set();
|
|
@@ -115,7 +434,13 @@ function getWorkflowExecutionOrder(nodes) {
|
|
|
115
434
|
for (const nodeId of nodeIds) {
|
|
116
435
|
visit(nodeId);
|
|
117
436
|
}
|
|
118
|
-
|
|
437
|
+
validateWorkflowNodeKinds(nodes);
|
|
438
|
+
validateWorkflowControlTargets(nodes);
|
|
439
|
+
const standaloneEndNodes = order.filter((nodeId) => nodes[nodeId]?.control === 'end' && getNodeNeeds(nodes[nodeId] ?? {}).length === 0);
|
|
440
|
+
const nonStandaloneEndNodes = order.filter((nodeId) => !standaloneEndNodes.includes(nodeId));
|
|
441
|
+
const executionOrder = [...nonStandaloneEndNodes, ...standaloneEndNodes];
|
|
442
|
+
validateWorkflowControlRouteDirection(nodes, executionOrder);
|
|
443
|
+
return executionOrder;
|
|
119
444
|
}
|
|
120
445
|
function getWorkflowNodes(workflowName, workflow) {
|
|
121
446
|
const nodes = workflow.nodes;
|
|
@@ -174,23 +499,83 @@ function renderTemplate(template, context) {
|
|
|
174
499
|
return stringifyTemplateValue(value);
|
|
175
500
|
});
|
|
176
501
|
}
|
|
502
|
+
async function flushWorkflowTrace(traceCollector, workflowName, inputs, startedAt, workingDir, options) {
|
|
503
|
+
const workflowTrace = traceCollector.buildWorkflowTrace(workflowName, inputs, startedAt);
|
|
504
|
+
const scope = {
|
|
505
|
+
platform: 'local',
|
|
506
|
+
projectId: workflowName,
|
|
507
|
+
subject: 'trace',
|
|
508
|
+
};
|
|
509
|
+
const savedJson = await saveWorkflowArtifact(workingDir, {
|
|
510
|
+
kind: 'trace',
|
|
511
|
+
scope,
|
|
512
|
+
payload: workflowTrace,
|
|
513
|
+
});
|
|
514
|
+
const traceHtmlPath = join(dirname(savedJson.path), 'trace.html');
|
|
515
|
+
const html = renderTraceHtml(workflowTrace);
|
|
516
|
+
await writeWorkflowFile(workingDir, traceHtmlPath, html);
|
|
517
|
+
if (!options.jsonOutput) {
|
|
518
|
+
console.log(chalk.green(`\n✓ Trace saved to ${savedJson.latestPath}`));
|
|
519
|
+
console.log(chalk.green(`✓ Trace viewer saved to ${traceHtmlPath}`));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
177
522
|
async function resolveWorkflowInput(key, input, workingDir) {
|
|
178
523
|
if (typeof input === 'string') {
|
|
179
524
|
return input;
|
|
180
525
|
}
|
|
181
|
-
const hasValue = input.value !== undefined;
|
|
526
|
+
const hasValue = input.value !== undefined || input.default !== undefined;
|
|
182
527
|
const hasFile = input.file !== undefined;
|
|
183
528
|
if (hasValue && hasFile) {
|
|
184
|
-
throw new Error(`Workflow input "${key}" cannot define both value and file.`);
|
|
529
|
+
throw new Error(`Workflow input "${key}" cannot define both value/default and file.`);
|
|
185
530
|
}
|
|
186
531
|
if (hasValue) {
|
|
187
|
-
return input.value ?? '';
|
|
532
|
+
return String(input.value ?? input.default ?? '');
|
|
188
533
|
}
|
|
189
534
|
if (hasFile) {
|
|
190
535
|
const inputPath = resolveWithinWorkingDir(workingDir, input.file ?? '', 'read');
|
|
191
536
|
return readFile(inputPath, 'utf-8');
|
|
192
537
|
}
|
|
193
|
-
|
|
538
|
+
if (input.required === true) {
|
|
539
|
+
return '';
|
|
540
|
+
}
|
|
541
|
+
return '';
|
|
542
|
+
}
|
|
543
|
+
function getWorkflowInputConfigType(input) {
|
|
544
|
+
return typeof input === 'string' ? 'string' : (input.type ?? 'string');
|
|
545
|
+
}
|
|
546
|
+
function validateResolvedWorkflowInput(key, input, value) {
|
|
547
|
+
if (typeof input === 'string') {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (input.required === true && value.trim() === '') {
|
|
551
|
+
throw new Error(`Workflow input "${key}" is required.`);
|
|
552
|
+
}
|
|
553
|
+
const type = getWorkflowInputConfigType(input);
|
|
554
|
+
if (type === 'boolean') {
|
|
555
|
+
if (normalizeWorkflowBooleanLike(value) === undefined) {
|
|
556
|
+
throw new Error(`Workflow input "${key}" must be a boolean value.`);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (type === 'number') {
|
|
561
|
+
if (value.trim() === '' || !Number.isFinite(Number(value))) {
|
|
562
|
+
throw new Error(`Workflow input "${key}" must be a number.`);
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (type === 'enum') {
|
|
567
|
+
const allowedValues = input.values?.map(String) ?? [];
|
|
568
|
+
if (allowedValues.length === 0) {
|
|
569
|
+
throw new Error(`Workflow input "${key}" with type enum must define values.`);
|
|
570
|
+
}
|
|
571
|
+
if (!allowedValues.includes(value)) {
|
|
572
|
+
throw new Error(`Workflow input "${key}" must be one of: ${allowedValues.join(', ')}.`);
|
|
573
|
+
}
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (type !== 'string') {
|
|
577
|
+
throw new Error(`Workflow input "${key}" has unsupported type "${type}".`);
|
|
578
|
+
}
|
|
194
579
|
}
|
|
195
580
|
async function resolveWorkflowInputs(workflow, options, workingDir) {
|
|
196
581
|
const values = {};
|
|
@@ -204,6 +589,9 @@ async function resolveWorkflowInputs(workflow, options, workingDir) {
|
|
|
204
589
|
const resolvedPath = resolveWithinWorkingDir(workingDir, filePath, 'read');
|
|
205
590
|
values[key] = await readFile(resolvedPath, 'utf-8');
|
|
206
591
|
}
|
|
592
|
+
for (const [key, input] of Object.entries(workflow.inputs ?? {})) {
|
|
593
|
+
validateResolvedWorkflowInput(key, input, values[key] ?? '');
|
|
594
|
+
}
|
|
207
595
|
return values;
|
|
208
596
|
}
|
|
209
597
|
function resolveAgentsFrom(config, agentsFrom) {
|
|
@@ -213,14 +601,16 @@ function resolveAgentsFrom(config, agentsFrom) {
|
|
|
213
601
|
throw new Error(`Unsupported workflow agentsFrom "${agentsFrom}". ` + 'Currently supported: review.agents.');
|
|
214
602
|
}
|
|
215
603
|
function getNodeKind(node) {
|
|
216
|
-
const configuredKinds = [node.agent, node.agentsFrom, node.action].filter((value) => value !== undefined).length;
|
|
604
|
+
const configuredKinds = [node.agent, node.agentsFrom, node.action, node.control].filter((value) => value !== undefined).length;
|
|
217
605
|
if (configuredKinds !== 1) {
|
|
218
|
-
throw new Error('Workflow node must define exactly one of agent, agentsFrom, or
|
|
606
|
+
throw new Error('Workflow node must define exactly one of agent, agentsFrom, action, or control.');
|
|
219
607
|
}
|
|
220
608
|
if (node.agent !== undefined)
|
|
221
609
|
return 'agent';
|
|
222
610
|
if (node.agentsFrom !== undefined)
|
|
223
611
|
return 'agents';
|
|
612
|
+
if (node.control !== undefined)
|
|
613
|
+
return 'control';
|
|
224
614
|
return 'action';
|
|
225
615
|
}
|
|
226
616
|
function hasConfiguredAgentPrompt(config, agentId) {
|
|
@@ -247,6 +637,35 @@ async function writeWorkflowFile(workingDir, relativeOutputPath, content) {
|
|
|
247
637
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
248
638
|
await writeFile(outputPath, content, 'utf-8');
|
|
249
639
|
}
|
|
640
|
+
async function formatWorkflowNodeWriteContent(workingDir, nodeId, writes, content) {
|
|
641
|
+
if (!/\.html?$/i.test(writes)) {
|
|
642
|
+
return content;
|
|
643
|
+
}
|
|
644
|
+
const pointer = parseArtifactOutputPointer(content);
|
|
645
|
+
if (pointer) {
|
|
646
|
+
if (pointer.outputPath !== writes) {
|
|
647
|
+
throw new Error(`Workflow node "${nodeId}" artifact pointer wrote "${pointer.outputPath}" but workflow expected "${writes}".`);
|
|
648
|
+
}
|
|
649
|
+
return readArtifactOutputPointer(workingDir, pointer);
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const html = extractHtmlDocument(content);
|
|
653
|
+
validateHtmlArtifact(html);
|
|
654
|
+
return html;
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
try {
|
|
658
|
+
return await readArtifactOutputPointer(workingDir, {
|
|
659
|
+
outputType: 'artifact_output',
|
|
660
|
+
outputPath: writes,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// If the agent did not write a valid artifact itself, surface the response validation error.
|
|
665
|
+
}
|
|
666
|
+
throw new Error(`Workflow node "${nodeId}" produced invalid HTML output: ${error instanceof Error ? error.message : String(error)}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
250
669
|
function renderNodeWritesPath(nodeId, node, context) {
|
|
251
670
|
if (!node.writes) {
|
|
252
671
|
return undefined;
|
|
@@ -257,7 +676,7 @@ function renderNodeWritesPath(nodeId, node, context) {
|
|
|
257
676
|
}
|
|
258
677
|
return writes;
|
|
259
678
|
}
|
|
260
|
-
async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, context) {
|
|
679
|
+
async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
261
680
|
const agentId = node.agent;
|
|
262
681
|
if (!agentId) {
|
|
263
682
|
throw new Error(`Workflow node "${nodeId}" is missing agent.`);
|
|
@@ -267,17 +686,25 @@ async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, c
|
|
|
267
686
|
throw new Error(`Workflow agent node "${nodeId}" must define input or configure ` +
|
|
268
687
|
`agents.overrides.${agentId}.run.prompt/promptFile.`);
|
|
269
688
|
}
|
|
270
|
-
const
|
|
689
|
+
const agentOptions = createAgentOptions(prompt, options, workingDir);
|
|
690
|
+
if (executionContext?.traceCollector && prompt) {
|
|
691
|
+
agentOptions.traceCollector = executionContext.traceCollector;
|
|
692
|
+
executionContext.traceCollector.setContext(nodeId, agentId, prompt);
|
|
693
|
+
}
|
|
694
|
+
const result = await runAgent(config, agentId, agentOptions);
|
|
271
695
|
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
696
|
+
const output = writes
|
|
697
|
+
? await formatWorkflowNodeWriteContent(workingDir, nodeId, writes, node.json === true ? JSON.stringify(result, null, 2) : result.response)
|
|
698
|
+
: result.response;
|
|
272
699
|
if (writes) {
|
|
273
|
-
await writeWorkflowFile(workingDir, writes,
|
|
700
|
+
await writeWorkflowFile(workingDir, writes, output);
|
|
274
701
|
}
|
|
275
702
|
return {
|
|
276
703
|
id: nodeId,
|
|
277
704
|
type: 'agent',
|
|
278
705
|
agent: agentId,
|
|
279
706
|
response: result.response,
|
|
280
|
-
output
|
|
707
|
+
output,
|
|
281
708
|
writes,
|
|
282
709
|
};
|
|
283
710
|
}
|
|
@@ -323,9 +750,64 @@ async function runActionWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
323
750
|
if (node.action === 'git-add') {
|
|
324
751
|
return runGitAddWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
325
752
|
}
|
|
753
|
+
if (node.action === 'git-branch') {
|
|
754
|
+
return runGitBranchWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
755
|
+
}
|
|
326
756
|
if (node.action === 'git-commit') {
|
|
327
757
|
return runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
328
758
|
}
|
|
759
|
+
if (node.action === 'git-push') {
|
|
760
|
+
return runGitPushWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
761
|
+
}
|
|
762
|
+
if (node.action === 'has-diff') {
|
|
763
|
+
return runHasDiffWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
764
|
+
}
|
|
765
|
+
if (node.action === 'stack-guard') {
|
|
766
|
+
return runStackGuardWorkflowNode(nodeId, node, context);
|
|
767
|
+
}
|
|
768
|
+
if (node.action === 'review-threshold') {
|
|
769
|
+
return runReviewThresholdWorkflowNode(nodeId, node, context);
|
|
770
|
+
}
|
|
771
|
+
if (node.action === 'save-artifact') {
|
|
772
|
+
return runSaveArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
773
|
+
}
|
|
774
|
+
if (node.action === 'load-artifact') {
|
|
775
|
+
return runLoadArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
776
|
+
}
|
|
777
|
+
if (node.action === 'artifact-exists') {
|
|
778
|
+
return runArtifactExistsWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
779
|
+
}
|
|
780
|
+
if (node.action === 'create-review-artifact') {
|
|
781
|
+
return runCreateReviewArtifactWorkflowNode(nodeId, node, context);
|
|
782
|
+
}
|
|
783
|
+
if (node.action === 'review-artifact-status') {
|
|
784
|
+
return runReviewArtifactStatusWorkflowNode(nodeId, node, context);
|
|
785
|
+
}
|
|
786
|
+
if (node.action === 'review-artifact-add-finding') {
|
|
787
|
+
return runReviewArtifactAddFindingWorkflowNode(nodeId, node, workingDir, context);
|
|
788
|
+
}
|
|
789
|
+
if (node.action === 'review-artifact-update-findings') {
|
|
790
|
+
return runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context);
|
|
791
|
+
}
|
|
792
|
+
if (node.action === 'verify-fix') {
|
|
793
|
+
return runVerifyFixWorkflowNode(nodeId, node, workingDir, context);
|
|
794
|
+
}
|
|
795
|
+
if (node.action === 'review-artifact-promote-finding') {
|
|
796
|
+
return runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context, {
|
|
797
|
+
disposition: 'confirmed',
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
if (node.action === 'review-artifact-resolve-finding') {
|
|
801
|
+
return runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context, {
|
|
802
|
+
state: 'resolved',
|
|
803
|
+
disposition: 'resolved',
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
if (node.action === 'create-change-request' ||
|
|
807
|
+
node.action === 'create-pr' ||
|
|
808
|
+
node.action === 'create-mr') {
|
|
809
|
+
return runCreateChangeRequestWorkflowNode(nodeId, node, context, executionContext);
|
|
810
|
+
}
|
|
329
811
|
if (node.action === 'change-source') {
|
|
330
812
|
return runChangeSourceWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
331
813
|
}
|
|
@@ -347,6 +829,9 @@ async function runActionWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
347
829
|
if (node.action === 'post-review-comments') {
|
|
348
830
|
return runPostReviewCommentsWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
349
831
|
}
|
|
832
|
+
if (node.action === 'post-fix-status') {
|
|
833
|
+
return runPostFixStatusWorkflowNode(nodeId, node, options, workingDir, context, executionContext);
|
|
834
|
+
}
|
|
350
835
|
throw new Error(`Unsupported workflow action "${node.action}" in node "${nodeId}".`);
|
|
351
836
|
}
|
|
352
837
|
async function runWriteWorkflowNode(nodeId, node, workingDir, context) {
|
|
@@ -371,9 +856,13 @@ async function runWriteWorkflowNode(nodeId, node, workingDir, context) {
|
|
|
371
856
|
writes: relativeOutputPath,
|
|
372
857
|
};
|
|
373
858
|
}
|
|
374
|
-
function getBooleanActionOption(node, key) {
|
|
859
|
+
function getBooleanActionOption(node, key, context) {
|
|
375
860
|
const value = node.with?.[key];
|
|
376
|
-
|
|
861
|
+
if (typeof value === 'string' && context) {
|
|
862
|
+
const rendered = renderTemplate(value, context).trim().toLowerCase();
|
|
863
|
+
return rendered === 'true' || rendered === '1' || rendered === 'yes';
|
|
864
|
+
}
|
|
865
|
+
return value === true || value === 'true' || value === 1;
|
|
377
866
|
}
|
|
378
867
|
function getStringActionOption(node, key, context) {
|
|
379
868
|
const value = node.with?.[key];
|
|
@@ -419,6 +908,12 @@ function getPathActionOption(nodeId, node, context, workingDir) {
|
|
|
419
908
|
}
|
|
420
909
|
return paths;
|
|
421
910
|
}
|
|
911
|
+
function getOptionalPathActionOption(nodeId, node, context, workingDir) {
|
|
912
|
+
if (!hasActionOption(node, 'paths') && !hasActionOption(node, 'path')) {
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
return getPathActionOption(nodeId, node, context, workingDir);
|
|
916
|
+
}
|
|
422
917
|
async function requireWorkflowGitRepo(nodeId, workingDir, executionContext) {
|
|
423
918
|
const git = getWorkflowGitClient(executionContext, workingDir);
|
|
424
919
|
const isRepo = await git.checkIsRepo();
|
|
@@ -433,7 +928,7 @@ async function runGitDiffWorkflowNode(nodeId, node, workingDir, context, executi
|
|
|
433
928
|
if (!isRepo) {
|
|
434
929
|
throw new Error(`Workflow git-diff node "${nodeId}" must run from a git repository.`);
|
|
435
930
|
}
|
|
436
|
-
const staged = getBooleanActionOption(node, 'staged');
|
|
931
|
+
const staged = getBooleanActionOption(node, 'staged', context);
|
|
437
932
|
const diff = staged ? await git.diff(['--cached']) : await git.diff();
|
|
438
933
|
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
439
934
|
if (writes) {
|
|
@@ -460,6 +955,24 @@ async function runGitAddWorkflowNode(nodeId, node, workingDir, context, executio
|
|
|
460
955
|
output: paths,
|
|
461
956
|
};
|
|
462
957
|
}
|
|
958
|
+
async function runGitBranchWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
959
|
+
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
960
|
+
const name = requireStringActionOption(nodeId, node, 'name', context);
|
|
961
|
+
const from = getStringActionOption(node, 'from', context)?.trim();
|
|
962
|
+
const force = getBooleanActionOption(node, 'force', context);
|
|
963
|
+
const args = ['checkout', force ? '-B' : '-b', name];
|
|
964
|
+
if (from) {
|
|
965
|
+
args.push(from);
|
|
966
|
+
}
|
|
967
|
+
await git.raw(args);
|
|
968
|
+
return {
|
|
969
|
+
id: nodeId,
|
|
970
|
+
type: 'action',
|
|
971
|
+
action: node.action,
|
|
972
|
+
response: `checked out branch ${name}`,
|
|
973
|
+
output: { branch: name, from, force },
|
|
974
|
+
};
|
|
975
|
+
}
|
|
463
976
|
async function runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
464
977
|
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
465
978
|
const message = requireStringActionOption(nodeId, node, 'message', context);
|
|
@@ -484,13 +997,464 @@ async function runGitCommitWorkflowNode(nodeId, node, workingDir, context, execu
|
|
|
484
997
|
output,
|
|
485
998
|
};
|
|
486
999
|
}
|
|
487
|
-
async function
|
|
1000
|
+
async function runGitPushWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
1001
|
+
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
1002
|
+
const configuredRemote = getStringActionOption(node, 'remote', context)?.trim();
|
|
1003
|
+
const remote = configuredRemote && configuredRemote.length > 0 ? configuredRemote : 'origin';
|
|
1004
|
+
const branch = requireStringActionOption(nodeId, node, 'branch', context);
|
|
1005
|
+
const configuredRemoteBranch = getStringActionOption(node, 'remoteBranch', context)?.trim();
|
|
1006
|
+
const remoteBranch = configuredRemoteBranch && configuredRemoteBranch.length > 0 ? configuredRemoteBranch : branch;
|
|
1007
|
+
const setUpstream = !hasActionOption(node, 'setUpstream') || getBooleanActionOption(node, 'setUpstream', context);
|
|
1008
|
+
const force = getBooleanActionOption(node, 'force', context);
|
|
1009
|
+
const args = ['push'];
|
|
1010
|
+
if (setUpstream) {
|
|
1011
|
+
args.push('-u');
|
|
1012
|
+
}
|
|
1013
|
+
if (force) {
|
|
1014
|
+
args.push('--force-with-lease');
|
|
1015
|
+
}
|
|
1016
|
+
args.push(remote, `${branch}:${remoteBranch}`);
|
|
1017
|
+
await git.raw(args);
|
|
1018
|
+
return {
|
|
1019
|
+
id: nodeId,
|
|
1020
|
+
type: 'action',
|
|
1021
|
+
action: node.action,
|
|
1022
|
+
response: `pushed ${branch} to ${remote}/${remoteBranch}`,
|
|
1023
|
+
output: { remote, branch, remoteBranch, setUpstream, force },
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
async function runHasDiffWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
1027
|
+
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
1028
|
+
const paths = getOptionalPathActionOption(nodeId, node, context, workingDir);
|
|
1029
|
+
const diff = paths ? await git.diff(['--', ...paths]) : await git.diff();
|
|
1030
|
+
const files = paths ?? [];
|
|
1031
|
+
const changed = diff.trim().length > 0;
|
|
1032
|
+
return {
|
|
1033
|
+
id: nodeId,
|
|
1034
|
+
type: 'action',
|
|
1035
|
+
action: node.action,
|
|
1036
|
+
response: changed ? 'changes found' : 'no changes found',
|
|
1037
|
+
output: { changed, files, bytes: Buffer.byteLength(diff, 'utf8') },
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function runStackGuardWorkflowNode(nodeId, node, context) {
|
|
1041
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
1042
|
+
const source = context.artifacts[sourceArtifact];
|
|
1043
|
+
if (!isReviewSource(source)) {
|
|
1044
|
+
throw new Error(`Workflow stack-guard node "${nodeId}" needs a ReviewSource artifact.`);
|
|
1045
|
+
}
|
|
1046
|
+
const pullRequest = isPullRequest(source.context.pullRequest)
|
|
1047
|
+
? source.context.pullRequest
|
|
1048
|
+
: undefined;
|
|
1049
|
+
const sourceBranch = pullRequest?.sourceBranch ?? '';
|
|
1050
|
+
const allowStackedSource = getBooleanActionOption(node, 'allowStackedSource', context);
|
|
1051
|
+
const rawPrefixes = getStringActionOption(node, 'reservedPrefixes', context) ?? 'drs-fix/,drs-guidance/,drs-stack/';
|
|
1052
|
+
const reservedPrefixes = rawPrefixes
|
|
1053
|
+
.split(/[,\n]/)
|
|
1054
|
+
.map((prefix) => prefix.trim())
|
|
1055
|
+
.filter(Boolean);
|
|
1056
|
+
const matchingPrefix = reservedPrefixes.find((prefix) => sourceBranch.startsWith(prefix));
|
|
1057
|
+
const allowed = allowStackedSource || !matchingPrefix;
|
|
1058
|
+
const reason = allowed ? 'allowed' : `source branch uses reserved DRS prefix "${matchingPrefix}"`;
|
|
1059
|
+
return {
|
|
1060
|
+
id: nodeId,
|
|
1061
|
+
type: 'action',
|
|
1062
|
+
action: node.action,
|
|
1063
|
+
response: reason,
|
|
1064
|
+
output: { allowed, reason, sourceBranch, reservedPrefixes },
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
function severityRank(severity) {
|
|
1068
|
+
const normalized = severity.trim().toUpperCase();
|
|
1069
|
+
if (normalized === 'CRITICAL')
|
|
1070
|
+
return 4;
|
|
1071
|
+
if (normalized === 'HIGH')
|
|
1072
|
+
return 3;
|
|
1073
|
+
if (normalized === 'MEDIUM')
|
|
1074
|
+
return 2;
|
|
1075
|
+
if (normalized === 'LOW')
|
|
1076
|
+
return 1;
|
|
1077
|
+
return 0;
|
|
1078
|
+
}
|
|
1079
|
+
function runReviewThresholdWorkflowNode(nodeId, node, context) {
|
|
1080
|
+
const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
|
|
1081
|
+
const reviewResult = context.artifacts[reviewArtifact];
|
|
1082
|
+
if (!isReviewResult(reviewResult)) {
|
|
1083
|
+
throw new Error(`Workflow review-threshold node "${nodeId}" needs a ReviewResult artifact.`);
|
|
1084
|
+
}
|
|
1085
|
+
const severity = (getStringActionOption(node, 'severity', context) ?? 'high').toUpperCase();
|
|
1086
|
+
const minIssues = hasActionOption(node, 'minIssues')
|
|
1087
|
+
? requireNumberActionOption(nodeId, node, 'minIssues', context)
|
|
1088
|
+
: 1;
|
|
1089
|
+
const thresholdRank = severityRank(severity);
|
|
1090
|
+
if (thresholdRank === 0) {
|
|
1091
|
+
throw new Error(`Workflow review-threshold node "${nodeId}" has unsupported severity "${severity}".`);
|
|
1092
|
+
}
|
|
1093
|
+
const matchingIssues = reviewResult.issues.filter((issue) => severityRank(issue.severity) >= thresholdRank);
|
|
1094
|
+
const matched = matchingIssues.length >= minIssues;
|
|
1095
|
+
return {
|
|
1096
|
+
id: nodeId,
|
|
1097
|
+
type: 'action',
|
|
1098
|
+
action: node.action,
|
|
1099
|
+
response: matched ? `${matchingIssues.length} matching issue(s)` : 'threshold not met',
|
|
1100
|
+
output: { matched, count: matchingIssues.length, severity, minIssues },
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
async function getCurrentBranch(workingDir, executionContext) {
|
|
1104
|
+
const git = await requireWorkflowGitRepo('artifact-scope', workingDir, executionContext);
|
|
1105
|
+
const branch = await git.branch();
|
|
1106
|
+
return branch.current ?? 'unknown';
|
|
1107
|
+
}
|
|
1108
|
+
async function resolveArtifactScope(nodeId, node, workingDir, context, executionContext) {
|
|
1109
|
+
const sourceArtifact = getStringActionOption(node, 'source', context);
|
|
1110
|
+
const source = sourceArtifact ? context.artifacts[sourceArtifact] : undefined;
|
|
1111
|
+
const reviewSource = isReviewSource(source) ? source : undefined;
|
|
1112
|
+
const sourceTarget = readSourcePostTarget(reviewSource);
|
|
1113
|
+
const explicitPlatform = getStringActionOption(node, 'platform', context);
|
|
1114
|
+
const platform = explicitPlatform ?? sourceTarget.platform ?? 'local';
|
|
1115
|
+
const projectId = getStringActionOption(node, 'project', context) ??
|
|
1116
|
+
getStringActionOption(node, 'projectId', context) ??
|
|
1117
|
+
(hasActionOption(node, 'owner') || hasActionOption(node, 'repo')
|
|
1118
|
+
? `${requireStringActionOption(nodeId, node, 'owner', context)}/${requireStringActionOption(nodeId, node, 'repo', context)}`
|
|
1119
|
+
: undefined) ??
|
|
1120
|
+
sourceTarget.projectId ??
|
|
1121
|
+
'local';
|
|
1122
|
+
const explicitSubject = getStringActionOption(node, 'subject', context)?.trim();
|
|
1123
|
+
if (explicitSubject) {
|
|
1124
|
+
return { platform, projectId, subject: explicitSubject };
|
|
1125
|
+
}
|
|
1126
|
+
const explicitChangeKind = getStringActionOption(node, 'changeKind', context)?.trim();
|
|
1127
|
+
const explicitChangeNumber = getStringActionOption(node, 'changeNumber', context)?.trim() ??
|
|
1128
|
+
getStringActionOption(node, 'pr', context)?.trim() ??
|
|
1129
|
+
getStringActionOption(node, 'mr', context)?.trim();
|
|
1130
|
+
const sourceChangeKind = sourceTarget.platform === 'gitlab' ? 'mr' : sourceTarget.platform ? 'pr' : undefined;
|
|
1131
|
+
const sourceChangeNumber = sourceTarget.prNumber;
|
|
1132
|
+
const changeKind = explicitChangeKind && explicitChangeKind.length > 0 ? explicitChangeKind : sourceChangeKind;
|
|
1133
|
+
const changeNumber = explicitChangeNumber ?? sourceChangeNumber;
|
|
1134
|
+
if (changeKind && changeNumber !== undefined) {
|
|
1135
|
+
return { platform, projectId, changeKind, changeNumber };
|
|
1136
|
+
}
|
|
1137
|
+
const configuredBranch = getStringActionOption(node, 'branch', context)?.trim();
|
|
1138
|
+
const branch = configuredBranch && configuredBranch.length > 0
|
|
1139
|
+
? configuredBranch
|
|
1140
|
+
: await getCurrentBranch(workingDir, executionContext);
|
|
1141
|
+
return { platform, projectId, branch };
|
|
1142
|
+
}
|
|
1143
|
+
function getArtifactPayloadFromNode(nodeId, node, context) {
|
|
1144
|
+
const artifactName = getStringActionOption(node, 'artifact', context);
|
|
1145
|
+
if (artifactName) {
|
|
1146
|
+
if (!Object.prototype.hasOwnProperty.call(context.artifacts, artifactName)) {
|
|
1147
|
+
throw new Error(`Workflow node "${nodeId}" references unknown artifact "${artifactName}".`);
|
|
1148
|
+
}
|
|
1149
|
+
return context.artifacts[artifactName];
|
|
1150
|
+
}
|
|
1151
|
+
const payloadName = getStringActionOption(node, 'payload', context);
|
|
1152
|
+
if (payloadName && Object.prototype.hasOwnProperty.call(context.artifacts, payloadName)) {
|
|
1153
|
+
return context.artifacts[payloadName];
|
|
1154
|
+
}
|
|
1155
|
+
if (payloadName) {
|
|
1156
|
+
try {
|
|
1157
|
+
return JSON.parse(payloadName);
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
return payloadName;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
throw new Error(`Workflow node "${nodeId}" must define with.artifact or with.payload.`);
|
|
1164
|
+
}
|
|
1165
|
+
async function runSaveArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
1166
|
+
const kind = requireStringActionOption(nodeId, node, 'kind', context);
|
|
1167
|
+
const payload = getArtifactPayloadFromNode(nodeId, node, context);
|
|
1168
|
+
const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
|
|
1169
|
+
const saved = await saveWorkflowArtifact(workingDir, { kind, scope, payload });
|
|
1170
|
+
return {
|
|
1171
|
+
id: nodeId,
|
|
1172
|
+
type: 'action',
|
|
1173
|
+
action: node.action,
|
|
1174
|
+
response: `saved ${kind} artifact ${saved.artifact.id}`,
|
|
1175
|
+
output: { ...saved.artifact, path: saved.path, latestPath: saved.latestPath },
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
async function runLoadArtifactWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
1179
|
+
const kind = requireStringActionOption(nodeId, node, 'kind', context);
|
|
1180
|
+
const rawId = getStringActionOption(node, 'id', context)?.trim();
|
|
1181
|
+
const id = rawId && rawId.length > 0 ? rawId : undefined;
|
|
1182
|
+
const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
|
|
1183
|
+
const loaded = await loadWorkflowArtifact(workingDir, kind, scope, id);
|
|
1184
|
+
return {
|
|
1185
|
+
id: nodeId,
|
|
1186
|
+
type: 'action',
|
|
1187
|
+
action: node.action,
|
|
1188
|
+
response: `loaded ${kind} artifact ${loaded.artifact.id}`,
|
|
1189
|
+
output: { ...loaded.artifact, path: loaded.path },
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
async function runArtifactExistsWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
1193
|
+
const kind = requireStringActionOption(nodeId, node, 'kind', context);
|
|
1194
|
+
const rawId = getStringActionOption(node, 'id', context)?.trim();
|
|
1195
|
+
const id = rawId && rawId.length > 0 ? rawId : undefined;
|
|
1196
|
+
const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
|
|
1197
|
+
const exists = await workflowArtifactExists(workingDir, kind, scope, id);
|
|
1198
|
+
return {
|
|
1199
|
+
id: nodeId,
|
|
1200
|
+
type: 'action',
|
|
1201
|
+
action: node.action,
|
|
1202
|
+
response: exists ? 'artifact exists' : 'artifact missing',
|
|
1203
|
+
output: { exists, kind, scope, id },
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
function runCreateReviewArtifactWorkflowNode(nodeId, node, context) {
|
|
1207
|
+
const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
|
|
1208
|
+
const review = context.artifacts[reviewArtifact];
|
|
1209
|
+
if (!isReviewResult(review)) {
|
|
1210
|
+
throw new Error(`Workflow create-review-artifact node "${nodeId}" needs a ReviewResult artifact.`);
|
|
1211
|
+
}
|
|
1212
|
+
const sourceArtifact = getStringActionOption(node, 'source', context);
|
|
1213
|
+
const source = sourceArtifact ? context.artifacts[sourceArtifact] : undefined;
|
|
1214
|
+
const reviewSource = isReviewSource(source) ? source : undefined;
|
|
1215
|
+
const artifact = createReviewArtifactPayload(review, reviewSource);
|
|
1216
|
+
return {
|
|
1217
|
+
id: nodeId,
|
|
1218
|
+
type: 'action',
|
|
1219
|
+
action: node.action,
|
|
1220
|
+
response: `created review artifact ${artifact.reviewId}`,
|
|
1221
|
+
output: artifact,
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function getReviewArtifactPayloadFromValue(nodeId, value) {
|
|
1225
|
+
const envelope = value;
|
|
1226
|
+
const payload = envelope && typeof envelope === 'object' && 'payload' in envelope ? envelope.payload : value;
|
|
1227
|
+
if (!isReviewArtifactPayload(payload)) {
|
|
1228
|
+
throw new Error(`Workflow review-artifact-status node "${nodeId}" needs a review artifact payload.`);
|
|
1229
|
+
}
|
|
1230
|
+
return payload;
|
|
1231
|
+
}
|
|
1232
|
+
function getReviewArtifactEnvelopeFromValue(value) {
|
|
1233
|
+
if (!value || typeof value !== 'object') {
|
|
1234
|
+
return undefined;
|
|
1235
|
+
}
|
|
1236
|
+
const envelope = value;
|
|
1237
|
+
if (envelope.schemaVersion === 1 &&
|
|
1238
|
+
envelope.kind === 'review' &&
|
|
1239
|
+
typeof envelope.id === 'string' &&
|
|
1240
|
+
typeof envelope.createdAt === 'string' &&
|
|
1241
|
+
typeof envelope.updatedAt === 'string' &&
|
|
1242
|
+
envelope.scope &&
|
|
1243
|
+
typeof envelope.scope === 'object' &&
|
|
1244
|
+
isReviewArtifactPayload(envelope.payload)) {
|
|
1245
|
+
return envelope;
|
|
1246
|
+
}
|
|
1247
|
+
return undefined;
|
|
1248
|
+
}
|
|
1249
|
+
function getReviewArtifactInput(nodeId, node, context) {
|
|
1250
|
+
const artifactName = getStringActionOption(node, 'artifact', context) ?? 'reviewArtifact';
|
|
1251
|
+
const artifactValue = context.artifacts[artifactName];
|
|
1252
|
+
const artifact = getReviewArtifactPayloadFromValue(nodeId, artifactValue);
|
|
1253
|
+
return { artifact, envelope: getReviewArtifactEnvelopeFromValue(artifactValue) };
|
|
1254
|
+
}
|
|
1255
|
+
async function persistMutatedReviewArtifact(workingDir, payload, envelope) {
|
|
1256
|
+
if (!envelope) {
|
|
1257
|
+
return { output: payload, responseSuffix: '' };
|
|
1258
|
+
}
|
|
1259
|
+
const saved = await updateWorkflowArtifact(workingDir, { artifact: envelope, payload });
|
|
1260
|
+
return {
|
|
1261
|
+
output: { ...saved.artifact, path: saved.path, latestPath: saved.latestPath },
|
|
1262
|
+
responseSuffix: ` and saved ${saved.artifact.id}`,
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function parseListActionOption(node, key, context) {
|
|
1266
|
+
const value = getStringActionOption(node, key, context)?.trim();
|
|
1267
|
+
if (!value) {
|
|
1268
|
+
return undefined;
|
|
1269
|
+
}
|
|
1270
|
+
return value
|
|
1271
|
+
.split(/[,\n]/)
|
|
1272
|
+
.map((item) => item.trim())
|
|
1273
|
+
.filter(Boolean);
|
|
1274
|
+
}
|
|
1275
|
+
function parseReviewFindingState(nodeId, value) {
|
|
1276
|
+
if (!value) {
|
|
1277
|
+
return undefined;
|
|
1278
|
+
}
|
|
1279
|
+
if (value === 'open' || value === 'attempted' || value === 'resolved') {
|
|
1280
|
+
return value;
|
|
1281
|
+
}
|
|
1282
|
+
throw new Error(`Workflow node "${nodeId}" has invalid review finding state "${value}".`);
|
|
1283
|
+
}
|
|
1284
|
+
function parseReviewFindingDisposition(nodeId, value) {
|
|
1285
|
+
if (!value) {
|
|
1286
|
+
return undefined;
|
|
1287
|
+
}
|
|
1288
|
+
if (value === 'confirmed' ||
|
|
1289
|
+
value === 'uncertain' ||
|
|
1290
|
+
value === 'pre_existing' ||
|
|
1291
|
+
value === 'partial' ||
|
|
1292
|
+
value === 'still_open' ||
|
|
1293
|
+
value === 'regression' ||
|
|
1294
|
+
value === 'resolved') {
|
|
1295
|
+
return value;
|
|
1296
|
+
}
|
|
1297
|
+
throw new Error(`Workflow node "${nodeId}" has invalid review finding disposition "${value}".`);
|
|
1298
|
+
}
|
|
1299
|
+
function parseReviewFindingSource(nodeId, value) {
|
|
1300
|
+
if (!value) {
|
|
1301
|
+
return 'manual';
|
|
1302
|
+
}
|
|
1303
|
+
if (value === 'agent' || value === 'manual' || value === 'external') {
|
|
1304
|
+
return value;
|
|
1305
|
+
}
|
|
1306
|
+
throw new Error(`Workflow node "${nodeId}" has invalid review finding source "${value}".`);
|
|
1307
|
+
}
|
|
1308
|
+
function isReviewIssue(value) {
|
|
1309
|
+
if (!value || typeof value !== 'object') {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
const issue = value;
|
|
1313
|
+
return (typeof issue.severity === 'string' &&
|
|
1314
|
+
typeof issue.category === 'string' &&
|
|
1315
|
+
typeof issue.title === 'string' &&
|
|
1316
|
+
typeof issue.file === 'string' &&
|
|
1317
|
+
typeof issue.problem === 'string' &&
|
|
1318
|
+
typeof issue.solution === 'string');
|
|
1319
|
+
}
|
|
1320
|
+
function getReviewIssueFromNode(nodeId, node, context) {
|
|
1321
|
+
const issueName = requireStringActionOption(nodeId, node, 'issue', context);
|
|
1322
|
+
const issue = Object.prototype.hasOwnProperty.call(context.artifacts, issueName)
|
|
1323
|
+
? context.artifacts[issueName]
|
|
1324
|
+
: JSON.parse(issueName);
|
|
1325
|
+
if (!isReviewIssue(issue)) {
|
|
1326
|
+
throw new Error(`Workflow review-artifact-add-finding node "${nodeId}" needs a ReviewIssue.`);
|
|
1327
|
+
}
|
|
1328
|
+
return issue;
|
|
1329
|
+
}
|
|
1330
|
+
function runReviewArtifactStatusWorkflowNode(nodeId, node, context) {
|
|
1331
|
+
const artifactName = getStringActionOption(node, 'artifact', context) ?? 'reviewArtifact';
|
|
1332
|
+
const artifactValue = context.artifacts[artifactName];
|
|
1333
|
+
const artifact = getReviewArtifactPayloadFromValue(nodeId, artifactValue);
|
|
1334
|
+
const status = getReviewArtifactStatus(artifact);
|
|
1335
|
+
return {
|
|
1336
|
+
id: nodeId,
|
|
1337
|
+
type: 'action',
|
|
1338
|
+
action: node.action,
|
|
1339
|
+
response: `${status.totalFindings} finding(s), ${status.openFindings} open`,
|
|
1340
|
+
output: status,
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
async function runReviewArtifactAddFindingWorkflowNode(nodeId, node, workingDir, context) {
|
|
1344
|
+
const { artifact, envelope } = getReviewArtifactInput(nodeId, node, context);
|
|
1345
|
+
const issue = getReviewIssueFromNode(nodeId, node, context);
|
|
1346
|
+
const source = parseReviewFindingSource(nodeId, getStringActionOption(node, 'source', context));
|
|
1347
|
+
const updated = addReviewArtifactFinding(artifact, issue, source);
|
|
1348
|
+
const persisted = await persistMutatedReviewArtifact(workingDir, updated, envelope);
|
|
1349
|
+
const addedFinding = updated.findings[updated.findings.length - 1];
|
|
1350
|
+
return {
|
|
1351
|
+
id: nodeId,
|
|
1352
|
+
type: 'action',
|
|
1353
|
+
action: node.action,
|
|
1354
|
+
response: `added review finding ${addedFinding?.id ?? 'unknown'}${persisted.responseSuffix}`,
|
|
1355
|
+
output: persisted.output,
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
async function runReviewArtifactUpdateFindingsWorkflowNode(nodeId, node, workingDir, context, defaults = {}) {
|
|
1359
|
+
const { artifact, envelope } = getReviewArtifactInput(nodeId, node, context);
|
|
1360
|
+
const state = parseReviewFindingState(nodeId, getStringActionOption(node, 'state', context) ?? defaults.state);
|
|
1361
|
+
const disposition = parseReviewFindingDisposition(nodeId, getStringActionOption(node, 'disposition', context) ?? defaults.disposition);
|
|
1362
|
+
if (!state && !disposition) {
|
|
1363
|
+
throw new Error(`Workflow node "${nodeId}" must define with.state or with.disposition.`);
|
|
1364
|
+
}
|
|
1365
|
+
const { artifact: updated, updatedIds } = updateReviewArtifactFindings(artifact, {
|
|
1366
|
+
ids: parseListActionOption(node, 'ids', context),
|
|
1367
|
+
fingerprints: parseListActionOption(node, 'fingerprints', context),
|
|
1368
|
+
severity: getStringActionOption(node, 'severity', context)?.trim().toUpperCase(),
|
|
1369
|
+
state,
|
|
1370
|
+
disposition,
|
|
1371
|
+
});
|
|
1372
|
+
const persisted = await persistMutatedReviewArtifact(workingDir, updated, envelope);
|
|
1373
|
+
return {
|
|
1374
|
+
id: nodeId,
|
|
1375
|
+
type: 'action',
|
|
1376
|
+
action: node.action,
|
|
1377
|
+
response: `updated ${updatedIds.length} review finding(s)${persisted.responseSuffix}`,
|
|
1378
|
+
output: {
|
|
1379
|
+
...(typeof persisted.output === 'object' && persisted.output !== null
|
|
1380
|
+
? persisted.output
|
|
1381
|
+
: {}),
|
|
1382
|
+
updatedIds,
|
|
1383
|
+
},
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
async function runVerifyFixWorkflowNode(nodeId, node, workingDir, context) {
|
|
1387
|
+
const { artifact, envelope } = getReviewArtifactInput(nodeId, node, context);
|
|
1388
|
+
const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'reReview';
|
|
1389
|
+
const review = context.artifacts[reviewArtifact];
|
|
1390
|
+
if (!isReviewResult(review)) {
|
|
1391
|
+
throw new Error(`Workflow verify-fix node "${nodeId}" needs a ReviewResult.`);
|
|
1392
|
+
}
|
|
1393
|
+
const severity = (getStringActionOption(node, 'severity', context) ?? 'high').toUpperCase();
|
|
1394
|
+
if (severityRank(severity) === 0) {
|
|
1395
|
+
throw new Error(`Workflow verify-fix node "${nodeId}" has unsupported severity "${severity}".`);
|
|
1396
|
+
}
|
|
1397
|
+
const minIssues = hasActionOption(node, 'minIssues')
|
|
1398
|
+
? requireNumberActionOption(nodeId, node, 'minIssues', context)
|
|
1399
|
+
: 1;
|
|
1400
|
+
const fixChangeName = getStringActionOption(node, 'fixChange', context);
|
|
1401
|
+
const fixChange = fixChangeName ? context.artifacts[fixChangeName] : undefined;
|
|
1402
|
+
const fixSource = isReviewSource(fixChange) ? fixChange : undefined;
|
|
1403
|
+
const reconciliation = reconcileReviewArtifactFindings(artifact, review, {
|
|
1404
|
+
severity,
|
|
1405
|
+
minIssues,
|
|
1406
|
+
fixSource,
|
|
1407
|
+
});
|
|
1408
|
+
const persisted = await persistMutatedReviewArtifact(workingDir, reconciliation.artifact, envelope);
|
|
1409
|
+
const status = getReviewArtifactStatus(reconciliation.artifact);
|
|
1410
|
+
const output = {
|
|
1411
|
+
...(typeof persisted.output === 'object' && persisted.output !== null ? persisted.output : {}),
|
|
1412
|
+
payload: reconciliation.artifact,
|
|
1413
|
+
verification: {
|
|
1414
|
+
severity,
|
|
1415
|
+
minIssues,
|
|
1416
|
+
shouldContinue: reconciliation.shouldContinue,
|
|
1417
|
+
actionableOpen: reconciliation.actionableOpen,
|
|
1418
|
+
fixFiles: fixSource?.files.length ?? 0,
|
|
1419
|
+
resolved: reconciliation.resolved,
|
|
1420
|
+
partial: reconciliation.partial,
|
|
1421
|
+
stillOpen: reconciliation.stillOpen,
|
|
1422
|
+
regression: reconciliation.regression,
|
|
1423
|
+
statuses: reconciliation.statuses.map((statusItem) => ({
|
|
1424
|
+
id: statusItem.finding.id,
|
|
1425
|
+
fingerprint: statusItem.finding.fingerprint,
|
|
1426
|
+
disposition: statusItem.disposition,
|
|
1427
|
+
severity: statusItem.finding.issue.severity,
|
|
1428
|
+
file: statusItem.finding.issue.file,
|
|
1429
|
+
line: statusItem.finding.issue.line,
|
|
1430
|
+
title: statusItem.finding.issue.title,
|
|
1431
|
+
verificationMissing: statusItem.verificationMissing === true,
|
|
1432
|
+
verificationRationale: statusItem.finding.verification?.rationale,
|
|
1433
|
+
})),
|
|
1434
|
+
},
|
|
1435
|
+
shouldContinue: reconciliation.shouldContinue,
|
|
1436
|
+
actionableOpen: reconciliation.actionableOpen,
|
|
1437
|
+
fixFiles: fixSource?.files.length ?? 0,
|
|
1438
|
+
updatedIds: reconciliation.statuses.map((statusItem) => statusItem.finding.id),
|
|
1439
|
+
status,
|
|
1440
|
+
};
|
|
1441
|
+
return {
|
|
1442
|
+
id: nodeId,
|
|
1443
|
+
type: 'action',
|
|
1444
|
+
action: node.action,
|
|
1445
|
+
response: reconciliation.shouldContinue
|
|
1446
|
+
? `${reconciliation.actionableOpen} actionable finding(s) remain${persisted.responseSuffix}`
|
|
1447
|
+
: `fix verification converged${persisted.responseSuffix}`,
|
|
1448
|
+
output,
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
async function loadLocalChangeSource(nodeId, node, workingDir, context) {
|
|
488
1452
|
const git = simpleGit({ baseDir: workingDir });
|
|
489
1453
|
const isRepo = await git.checkIsRepo();
|
|
490
1454
|
if (!isRepo) {
|
|
491
1455
|
throw new Error(`Workflow change-source node "${nodeId}" must run from a git repository.`);
|
|
492
1456
|
}
|
|
493
|
-
const staged = getBooleanActionOption(node, 'staged');
|
|
1457
|
+
const staged = getBooleanActionOption(node, 'staged', context);
|
|
494
1458
|
const diffText = staged ? await git.diff(['--cached']) : await git.diff();
|
|
495
1459
|
const diffs = parseDiff(diffText);
|
|
496
1460
|
const changedFiles = getChangedFiles(diffs);
|
|
@@ -528,8 +1492,8 @@ async function resolveGitRangeToRef(git) {
|
|
|
528
1492
|
function isStableSemverTag(tag) {
|
|
529
1493
|
return /^v?\d+\.\d+\.\d+$/.test(tag);
|
|
530
1494
|
}
|
|
531
|
-
async function resolvePreviousGitRangeTag(nodeId, node, git, toRef) {
|
|
532
|
-
const includePrerelease = getBooleanActionOption(node, 'includePrereleaseFrom');
|
|
1495
|
+
async function resolvePreviousGitRangeTag(nodeId, node, git, toRef, context) {
|
|
1496
|
+
const includePrerelease = getBooleanActionOption(node, 'includePrereleaseFrom', context);
|
|
533
1497
|
const tagOutput = await git.raw(['tag', '--merged', toRef, '--sort=-v:refname']);
|
|
534
1498
|
const tags = tagOutput
|
|
535
1499
|
.split('\n')
|
|
@@ -544,11 +1508,9 @@ async function resolvePreviousGitRangeTag(nodeId, node, git, toRef) {
|
|
|
544
1508
|
}
|
|
545
1509
|
async function resolveGitRangeRefs(nodeId, node, context, git) {
|
|
546
1510
|
const configuredToRef = getStringActionOption(node, 'to', context)?.trim();
|
|
547
|
-
const toRef = configuredToRef
|
|
1511
|
+
const toRef = configuredToRef ?? (await resolveGitRangeToRef(git));
|
|
548
1512
|
const configuredFromRef = getStringActionOption(node, 'from', context)?.trim();
|
|
549
|
-
const fromRef = configuredFromRef
|
|
550
|
-
? configuredFromRef
|
|
551
|
-
: await resolvePreviousGitRangeTag(nodeId, node, git, toRef);
|
|
1513
|
+
const fromRef = configuredFromRef ?? (await resolvePreviousGitRangeTag(nodeId, node, git, toRef, context));
|
|
552
1514
|
if (!fromRef) {
|
|
553
1515
|
throw new Error(`Workflow node "${nodeId}" could not infer the previous tag for ${toRef}. ` +
|
|
554
1516
|
'Provide with.from explicitly.');
|
|
@@ -619,11 +1581,68 @@ async function loadGitLabChangeSource(nodeId, node, workingDir, context, executi
|
|
|
619
1581
|
]);
|
|
620
1582
|
return createPlatformChangeSource('gitlab', `GitLab MR ${projectId}!${mrIid}`, projectId, pullRequest, changedFiles, workingDir);
|
|
621
1583
|
}
|
|
1584
|
+
function combineVerificationPatch(originalPatch, fixPatch) {
|
|
1585
|
+
const sections = [];
|
|
1586
|
+
if (originalPatch) {
|
|
1587
|
+
sections.push(`# Original PR/MR diff\n${originalPatch}`);
|
|
1588
|
+
}
|
|
1589
|
+
if (fixPatch) {
|
|
1590
|
+
sections.push(`# Local fix diff\n${fixPatch}`);
|
|
1591
|
+
}
|
|
1592
|
+
return sections.join('\n\n');
|
|
1593
|
+
}
|
|
1594
|
+
async function loadFixVerificationChangeSource(nodeId, node, workingDir, context) {
|
|
1595
|
+
const sourceName = getStringActionOption(node, 'source', context) ?? 'change';
|
|
1596
|
+
const source = context.artifacts[sourceName];
|
|
1597
|
+
if (!isReviewSource(source)) {
|
|
1598
|
+
throw new Error(`Workflow fix-verification change-source node "${nodeId}" needs a source ReviewSource artifact.`);
|
|
1599
|
+
}
|
|
1600
|
+
const fixChangeName = getStringActionOption(node, 'fixChange', context) ?? 'fixChange';
|
|
1601
|
+
const fixChange = context.artifacts[fixChangeName];
|
|
1602
|
+
if (!isReviewSource(fixChange)) {
|
|
1603
|
+
throw new Error(`Workflow fix-verification change-source node "${nodeId}" needs a fixChange ReviewSource artifact.`);
|
|
1604
|
+
}
|
|
1605
|
+
const files = [...new Set([...source.files, ...fixChange.files])];
|
|
1606
|
+
const pullRequest = isPullRequest(source.context.pullRequest) ? source.context.pullRequest : null;
|
|
1607
|
+
const baseRef = pullRequest?.targetBranch ?? source.context.baseBranch ?? 'HEAD~1';
|
|
1608
|
+
const realDiffs = await tryGetRealPostFixDiff(workingDir, files, baseRef);
|
|
1609
|
+
return {
|
|
1610
|
+
...source,
|
|
1611
|
+
name: `${source.name} with local fixes`,
|
|
1612
|
+
files,
|
|
1613
|
+
filesWithDiffs: realDiffs ??
|
|
1614
|
+
files.map((filename) => ({
|
|
1615
|
+
filename,
|
|
1616
|
+
patch: combineVerificationPatch((source.filesWithDiffs ?? []).find((f) => f.filename === filename)?.patch, (fixChange.filesWithDiffs ?? []).find((f) => f.filename === filename)?.patch),
|
|
1617
|
+
})),
|
|
1618
|
+
context: {
|
|
1619
|
+
...source.context,
|
|
1620
|
+
sourceType: 'fix-verification',
|
|
1621
|
+
fixFiles: fixChange.files,
|
|
1622
|
+
},
|
|
1623
|
+
staged: fixChange.staged,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
async function tryGetRealPostFixDiff(workingDir, files, baseRef) {
|
|
1627
|
+
try {
|
|
1628
|
+
const git = simpleGit({ baseDir: workingDir });
|
|
1629
|
+
const isRepo = await git.checkIsRepo();
|
|
1630
|
+
if (!isRepo)
|
|
1631
|
+
return undefined;
|
|
1632
|
+
const diffText = await git.diff(['--no-ext-diff', '-M', baseRef, '--', ...files]);
|
|
1633
|
+
if (!diffText.trim())
|
|
1634
|
+
return undefined;
|
|
1635
|
+
return getFilesWithDiffs(parseDiff(diffText));
|
|
1636
|
+
}
|
|
1637
|
+
catch {
|
|
1638
|
+
return undefined;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
622
1641
|
async function runChangeSourceWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
623
1642
|
const type = getStringActionOption(node, 'type', context) ?? 'local';
|
|
624
1643
|
let source;
|
|
625
1644
|
if (type === 'local') {
|
|
626
|
-
source = await loadLocalChangeSource(nodeId, node, workingDir);
|
|
1645
|
+
source = await loadLocalChangeSource(nodeId, node, workingDir, context);
|
|
627
1646
|
}
|
|
628
1647
|
else if (type === 'git-range') {
|
|
629
1648
|
source = await loadGitRangeChangeSource(nodeId, node, workingDir, context, executionContext);
|
|
@@ -634,9 +1653,12 @@ async function runChangeSourceWorkflowNode(nodeId, node, workingDir, context, ex
|
|
|
634
1653
|
else if (type === 'gitlab-mr') {
|
|
635
1654
|
source = await loadGitLabChangeSource(nodeId, node, workingDir, context, executionContext);
|
|
636
1655
|
}
|
|
1656
|
+
else if (type === 'fix-verification') {
|
|
1657
|
+
source = await loadFixVerificationChangeSource(nodeId, node, workingDir, context);
|
|
1658
|
+
}
|
|
637
1659
|
else {
|
|
638
1660
|
throw new Error(`Unsupported workflow change-source type "${type}" in node "${nodeId}". ` +
|
|
639
|
-
'Currently supported: local, git-range, github-pr, gitlab-mr.');
|
|
1661
|
+
'Currently supported: local, git-range, github-pr, gitlab-mr, fix-verification.');
|
|
640
1662
|
}
|
|
641
1663
|
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
642
1664
|
if (writes) {
|
|
@@ -724,9 +1746,84 @@ function resolvePostTarget(nodeId, node, context, executionContext, source) {
|
|
|
724
1746
|
changedFiles: sourceTarget.changedFiles,
|
|
725
1747
|
};
|
|
726
1748
|
}
|
|
727
|
-
function
|
|
728
|
-
|
|
729
|
-
|
|
1749
|
+
async function runCreateChangeRequestWorkflowNode(nodeId, node, context, executionContext) {
|
|
1750
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
1751
|
+
const source = isReviewSource(context.artifacts[sourceArtifact])
|
|
1752
|
+
? context.artifacts[sourceArtifact]
|
|
1753
|
+
: undefined;
|
|
1754
|
+
const sourceTarget = readSourcePostTarget(source);
|
|
1755
|
+
const aliasPlatform = node.action === 'create-pr' ? 'github' : node.action === 'create-mr' ? 'gitlab' : undefined;
|
|
1756
|
+
const explicitPlatform = getStringActionOption(node, 'platform', context);
|
|
1757
|
+
const platform = aliasPlatform ?? explicitPlatform ?? sourceTarget.platform;
|
|
1758
|
+
if (!isWorkflowPlatform(platform)) {
|
|
1759
|
+
throw new Error(`Workflow change-request node "${nodeId}" must resolve with.platform to github or gitlab.`);
|
|
1760
|
+
}
|
|
1761
|
+
const projectId = resolvePostProjectId(nodeId, node, context, sourceTarget);
|
|
1762
|
+
const configuredSourceBranch = getStringActionOption(node, 'sourceBranch', context)?.trim();
|
|
1763
|
+
const configuredHead = getStringActionOption(node, 'head', context)?.trim();
|
|
1764
|
+
const sourceBranch = configuredSourceBranch && configuredSourceBranch.length > 0
|
|
1765
|
+
? configuredSourceBranch
|
|
1766
|
+
: configuredHead;
|
|
1767
|
+
const configuredTargetBranch = getStringActionOption(node, 'targetBranch', context)?.trim();
|
|
1768
|
+
const configuredBase = getStringActionOption(node, 'base', context)?.trim();
|
|
1769
|
+
const targetBranch = configuredTargetBranch && configuredTargetBranch.length > 0
|
|
1770
|
+
? configuredTargetBranch
|
|
1771
|
+
: configuredBase;
|
|
1772
|
+
if (!sourceBranch) {
|
|
1773
|
+
throw new Error(`Workflow change-request node "${nodeId}" must define with.sourceBranch.`);
|
|
1774
|
+
}
|
|
1775
|
+
if (!targetBranch) {
|
|
1776
|
+
throw new Error(`Workflow change-request node "${nodeId}" must define with.targetBranch.`);
|
|
1777
|
+
}
|
|
1778
|
+
const title = requireStringActionOption(nodeId, node, 'title', context);
|
|
1779
|
+
const body = getStringActionOption(node, 'body', context);
|
|
1780
|
+
const draft = getBooleanActionOption(node, 'draft', context);
|
|
1781
|
+
const reuseExisting = !hasActionOption(node, 'reuseExisting') ||
|
|
1782
|
+
getBooleanActionOption(node, 'reuseExisting', context);
|
|
1783
|
+
const platformClient = getWorkflowPlatformClient(executionContext, platform);
|
|
1784
|
+
const input = {
|
|
1785
|
+
sourceBranch,
|
|
1786
|
+
targetBranch,
|
|
1787
|
+
title,
|
|
1788
|
+
body,
|
|
1789
|
+
draft,
|
|
1790
|
+
};
|
|
1791
|
+
const existing = reuseExisting
|
|
1792
|
+
? await platformClient.findChangeRequest?.(projectId, sourceBranch, targetBranch)
|
|
1793
|
+
: undefined;
|
|
1794
|
+
let operation = existing ? 'reused' : 'created';
|
|
1795
|
+
const changeRequest = existing ??
|
|
1796
|
+
(await platformClient.createChangeRequest(projectId, input).catch(async (error) => {
|
|
1797
|
+
if (reuseExisting) {
|
|
1798
|
+
try {
|
|
1799
|
+
const retryExisting = await platformClient.findChangeRequest?.(projectId, sourceBranch, targetBranch);
|
|
1800
|
+
if (retryExisting) {
|
|
1801
|
+
operation = 'reused';
|
|
1802
|
+
return retryExisting;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
catch {
|
|
1806
|
+
// ignore retry failure; fall through to throw original error
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
throw error;
|
|
1810
|
+
}));
|
|
1811
|
+
return {
|
|
1812
|
+
id: nodeId,
|
|
1813
|
+
type: 'action',
|
|
1814
|
+
action: node.action,
|
|
1815
|
+
response: `${operation} ${platform} change request #${changeRequest.number}`,
|
|
1816
|
+
output: {
|
|
1817
|
+
platform,
|
|
1818
|
+
projectId,
|
|
1819
|
+
operation,
|
|
1820
|
+
...changeRequest,
|
|
1821
|
+
},
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function isPullRequest(value) {
|
|
1825
|
+
if (!value || typeof value !== 'object') {
|
|
1826
|
+
return false;
|
|
730
1827
|
}
|
|
731
1828
|
const candidate = value;
|
|
732
1829
|
return (typeof candidate.number === 'number' &&
|
|
@@ -755,19 +1852,24 @@ function createWorkflowLineValidator(platform, source) {
|
|
|
755
1852
|
: [];
|
|
756
1853
|
const patchSources = fileChanges.length > 0 ? fileChanges : (source.filesWithDiffs ?? []);
|
|
757
1854
|
const validLinesMap = new Map();
|
|
1855
|
+
const changedLinesMap = new Map();
|
|
758
1856
|
for (const file of patchSources) {
|
|
759
1857
|
if ('status' in file && file.status === 'removed') {
|
|
760
1858
|
continue;
|
|
761
1859
|
}
|
|
762
1860
|
const patch = file.patch;
|
|
763
1861
|
if (patch) {
|
|
764
|
-
|
|
1862
|
+
const lineInfo = parseDiffLineInfo(patch);
|
|
1863
|
+
validLinesMap.set(file.filename, lineInfo.commentableLines);
|
|
1864
|
+
changedLinesMap.set(file.filename, lineInfo.addedLines);
|
|
765
1865
|
}
|
|
766
1866
|
}
|
|
767
1867
|
return {
|
|
768
1868
|
isValidLine(file, line) {
|
|
769
|
-
|
|
770
|
-
|
|
1869
|
+
return validLinesMap.get(file)?.has(line) ?? false;
|
|
1870
|
+
},
|
|
1871
|
+
isChangedLine(file, line) {
|
|
1872
|
+
return changedLinesMap.get(file)?.has(line) ?? false;
|
|
771
1873
|
},
|
|
772
1874
|
};
|
|
773
1875
|
}
|
|
@@ -896,7 +1998,8 @@ async function runDescribeWorkflowNode(config, nodeId, node, options, workingDir
|
|
|
896
1998
|
if (!target.pullRequest) {
|
|
897
1999
|
throw new Error(`Workflow describe node "${nodeId}" needs a platform change-source target.`);
|
|
898
2000
|
}
|
|
899
|
-
const shouldPostDescription = getBooleanActionOption(node, 'post') ||
|
|
2001
|
+
const shouldPostDescription = getBooleanActionOption(node, 'post', context) ||
|
|
2002
|
+
getBooleanActionOption(node, 'postDescription', context);
|
|
900
2003
|
const runtimeClient = await connectToRuntime(config, source.workingDir ?? workingDir, {
|
|
901
2004
|
debug: options.debug,
|
|
902
2005
|
modelOverrides: getDescriberModelOverride(config),
|
|
@@ -988,7 +2091,7 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
|
|
|
988
2091
|
? createWorkflowInlinePosition(target.platform, source)
|
|
989
2092
|
: undefined;
|
|
990
2093
|
const shouldRemoveErrorComment = !hasActionOption(node, 'removeErrorComment') ||
|
|
991
|
-
getBooleanActionOption(node, 'removeErrorComment');
|
|
2094
|
+
getBooleanActionOption(node, 'removeErrorComment', context);
|
|
992
2095
|
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, async () => {
|
|
993
2096
|
if (shouldRemoveErrorComment) {
|
|
994
2097
|
await removeErrorComment(target.platformClient, target.projectId, target.prNumber);
|
|
@@ -1015,6 +2118,256 @@ async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options,
|
|
|
1015
2118
|
},
|
|
1016
2119
|
};
|
|
1017
2120
|
}
|
|
2121
|
+
function getFixStatusDisposition(finding) {
|
|
2122
|
+
const disposition = finding.disposition;
|
|
2123
|
+
if (disposition === 'resolved')
|
|
2124
|
+
return 'resolved';
|
|
2125
|
+
if (disposition === 'partial')
|
|
2126
|
+
return 'partial';
|
|
2127
|
+
if (disposition === 'regression')
|
|
2128
|
+
return 'regression';
|
|
2129
|
+
return 'still-open';
|
|
2130
|
+
}
|
|
2131
|
+
function getVerificationDisposition(finding, verdict, thresholdRank) {
|
|
2132
|
+
if (severityRank(finding.issue.severity) < thresholdRank) {
|
|
2133
|
+
return finding.disposition;
|
|
2134
|
+
}
|
|
2135
|
+
if (!verdict) {
|
|
2136
|
+
if (finding.disposition === 'regression') {
|
|
2137
|
+
return 'regression';
|
|
2138
|
+
}
|
|
2139
|
+
return 'still_open';
|
|
2140
|
+
}
|
|
2141
|
+
return verdict.disposition === 'still_open' ? 'still_open' : verdict.disposition;
|
|
2142
|
+
}
|
|
2143
|
+
function reconcileReviewArtifactFindings(artifact, reReview, options) {
|
|
2144
|
+
const now = new Date().toISOString();
|
|
2145
|
+
const thresholdRank = severityRank(options.severity);
|
|
2146
|
+
const verdicts = new Map((reReview.verification?.findings ?? []).map((finding) => [finding.id, finding]));
|
|
2147
|
+
const existingFingerprints = new Set(artifact.findings.map((finding) => finding.fingerprint));
|
|
2148
|
+
const reconciledFindings = artifact.findings.map((finding) => {
|
|
2149
|
+
const verdict = verdicts.get(finding.id);
|
|
2150
|
+
const disposition = getVerificationDisposition(finding, verdict, thresholdRank);
|
|
2151
|
+
const issue = isReviewIssue(verdict?.issue) ? verdict.issue : finding.issue;
|
|
2152
|
+
const fingerprint = issue === finding.issue ? finding.fingerprint : createIssueFingerprint(issue);
|
|
2153
|
+
const shouldVerify = severityRank(finding.issue.severity) >= thresholdRank;
|
|
2154
|
+
const verification = shouldVerify
|
|
2155
|
+
? {
|
|
2156
|
+
disposition: verdict?.disposition ?? 'missing',
|
|
2157
|
+
rationale: verdict?.rationale,
|
|
2158
|
+
verifiedAt: now,
|
|
2159
|
+
}
|
|
2160
|
+
: finding.verification;
|
|
2161
|
+
return {
|
|
2162
|
+
...finding,
|
|
2163
|
+
issue,
|
|
2164
|
+
fingerprint,
|
|
2165
|
+
state: disposition === 'resolved' ? 'resolved' : 'open',
|
|
2166
|
+
disposition,
|
|
2167
|
+
verification,
|
|
2168
|
+
updatedAt: now,
|
|
2169
|
+
};
|
|
2170
|
+
});
|
|
2171
|
+
for (const issue of reReview.issues) {
|
|
2172
|
+
const fingerprint = createIssueFingerprint(issue);
|
|
2173
|
+
if (existingFingerprints.has(fingerprint)) {
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
existingFingerprints.add(fingerprint);
|
|
2177
|
+
reconciledFindings.push({
|
|
2178
|
+
id: `R${reconciledFindings.length + 1}`,
|
|
2179
|
+
fingerprint,
|
|
2180
|
+
issue,
|
|
2181
|
+
state: 'open',
|
|
2182
|
+
disposition: 'regression',
|
|
2183
|
+
verification: undefined,
|
|
2184
|
+
source: 'agent',
|
|
2185
|
+
createdAt: now,
|
|
2186
|
+
updatedAt: now,
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
const updatedArtifact = { ...artifact, findings: reconciledFindings };
|
|
2190
|
+
const statuses = updatedArtifact.findings.map((finding) => {
|
|
2191
|
+
const disposition = getFixStatusDisposition(finding);
|
|
2192
|
+
const verificationMissing = severityRank(finding.issue.severity) >= thresholdRank && !verdicts.has(finding.id);
|
|
2193
|
+
const diffSnippet = disposition === 'resolved' || disposition === 'regression'
|
|
2194
|
+
? extractDiffSnippet(options.fixSource, finding.issue.file, finding.issue.line)
|
|
2195
|
+
: undefined;
|
|
2196
|
+
return { finding, disposition, diffSnippet, verificationMissing };
|
|
2197
|
+
});
|
|
2198
|
+
const actionableOpen = updatedArtifact.findings.filter((finding) => finding.state === 'open' &&
|
|
2199
|
+
severityRank(finding.issue.severity) >= thresholdRank &&
|
|
2200
|
+
(finding.disposition === 'still_open' ||
|
|
2201
|
+
finding.disposition === 'partial' ||
|
|
2202
|
+
finding.disposition === 'regression')).length;
|
|
2203
|
+
return {
|
|
2204
|
+
artifact: updatedArtifact,
|
|
2205
|
+
statuses,
|
|
2206
|
+
shouldContinue: actionableOpen >= options.minIssues,
|
|
2207
|
+
actionableOpen,
|
|
2208
|
+
resolved: updatedArtifact.findings.filter((finding) => finding.state === 'resolved').length,
|
|
2209
|
+
partial: updatedArtifact.findings.filter((finding) => finding.disposition === 'partial').length,
|
|
2210
|
+
stillOpen: updatedArtifact.findings.filter((finding) => finding.disposition === 'still_open')
|
|
2211
|
+
.length,
|
|
2212
|
+
regression: updatedArtifact.findings.filter((finding) => finding.disposition === 'regression')
|
|
2213
|
+
.length,
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function extractDiffSnippet(fixChange, filePath, line) {
|
|
2217
|
+
if (!fixChange?.filesWithDiffs) {
|
|
2218
|
+
return undefined;
|
|
2219
|
+
}
|
|
2220
|
+
const fileWithDiff = fixChange.filesWithDiffs.find((f) => f.filename === filePath);
|
|
2221
|
+
if (!fileWithDiff?.patch) {
|
|
2222
|
+
return undefined;
|
|
2223
|
+
}
|
|
2224
|
+
const lines = fileWithDiff.patch.split('\n');
|
|
2225
|
+
if (!line) {
|
|
2226
|
+
return lines.slice(0, 15).join('\n');
|
|
2227
|
+
}
|
|
2228
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2229
|
+
const hunkMatch = lines[i]?.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
2230
|
+
if (!hunkMatch) {
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
const newStart = Number.parseInt(hunkMatch[1], 10);
|
|
2234
|
+
const newLineCount = hunkMatch[2] ? Number.parseInt(hunkMatch[2], 10) : 1;
|
|
2235
|
+
if (line < newStart || line >= newStart + newLineCount) {
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
let end = i + 1;
|
|
2239
|
+
while (end < lines.length && !lines[end]?.startsWith('@@')) {
|
|
2240
|
+
end++;
|
|
2241
|
+
}
|
|
2242
|
+
return lines.slice(i, end).join('\n');
|
|
2243
|
+
}
|
|
2244
|
+
return lines.slice(0, 15).join('\n');
|
|
2245
|
+
}
|
|
2246
|
+
function formatFixStatusComment(statuses, stackedPrUrl) {
|
|
2247
|
+
const lines = ['## Fix Status', ''];
|
|
2248
|
+
if (statuses.length === 0) {
|
|
2249
|
+
lines.push('No findings to report.');
|
|
2250
|
+
return lines.join('\n');
|
|
2251
|
+
}
|
|
2252
|
+
lines.push('| # | Severity | File | Issue | Status |');
|
|
2253
|
+
lines.push('|---|----------|------|-------|--------|');
|
|
2254
|
+
for (let i = 0; i < statuses.length; i++) {
|
|
2255
|
+
const s = statuses[i];
|
|
2256
|
+
const statusIcon = s.disposition === 'resolved'
|
|
2257
|
+
? '✅ Resolved'
|
|
2258
|
+
: s.disposition === 'partial'
|
|
2259
|
+
? '🟡 Partial'
|
|
2260
|
+
: s.disposition === 'regression'
|
|
2261
|
+
? '🔴 Regression'
|
|
2262
|
+
: s.disposition === 'attempted'
|
|
2263
|
+
? '🔧 Attempted'
|
|
2264
|
+
: s.verificationMissing
|
|
2265
|
+
? '⚠️ Verification Missing'
|
|
2266
|
+
: '⚪ Still Open';
|
|
2267
|
+
const file = `${s.finding.issue.file}${s.finding.issue.line ? `:${s.finding.issue.line}` : ''}`;
|
|
2268
|
+
lines.push(`| ${i + 1} | ${s.finding.issue.severity} | ${file} | ${s.finding.issue.title} | ${statusIcon} |`);
|
|
2269
|
+
}
|
|
2270
|
+
const resolved = statuses.filter((s) => s.disposition === 'resolved' && s.diffSnippet);
|
|
2271
|
+
if (resolved.length > 0) {
|
|
2272
|
+
lines.push('');
|
|
2273
|
+
lines.push('### Fix Details');
|
|
2274
|
+
for (let i = 0; i < statuses.length; i++) {
|
|
2275
|
+
const s = statuses[i];
|
|
2276
|
+
if (s.disposition === 'resolved' && s.diffSnippet) {
|
|
2277
|
+
lines.push('');
|
|
2278
|
+
lines.push(`**#${i + 1} — ${s.finding.issue.title} (${s.disposition})**`);
|
|
2279
|
+
lines.push('```diff');
|
|
2280
|
+
lines.push(s.diffSnippet);
|
|
2281
|
+
lines.push('```');
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
if (stackedPrUrl) {
|
|
2286
|
+
lines.push('');
|
|
2287
|
+
lines.push(`Stacked fix PR: ${stackedPrUrl}`);
|
|
2288
|
+
}
|
|
2289
|
+
return lines.join('\n');
|
|
2290
|
+
}
|
|
2291
|
+
async function runPostFixStatusWorkflowNode(nodeId, node, options, workingDir, context, executionContext) {
|
|
2292
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
2293
|
+
const source = isReviewSource(context.artifacts[sourceArtifact])
|
|
2294
|
+
? context.artifacts[sourceArtifact]
|
|
2295
|
+
: undefined;
|
|
2296
|
+
const target = resolvePostTarget(nodeId, node, context, executionContext, source);
|
|
2297
|
+
const reviewArtifactName = getStringActionOption(node, 'reviewArtifact', context) ?? 'reviewArtifact';
|
|
2298
|
+
const reviewArtifactValue = context.artifacts[reviewArtifactName];
|
|
2299
|
+
let artifactPayload;
|
|
2300
|
+
if (isReviewArtifactPayload(reviewArtifactValue)) {
|
|
2301
|
+
artifactPayload = reviewArtifactValue;
|
|
2302
|
+
}
|
|
2303
|
+
else if (reviewArtifactValue &&
|
|
2304
|
+
typeof reviewArtifactValue === 'object' &&
|
|
2305
|
+
'payload' in reviewArtifactValue) {
|
|
2306
|
+
const payload = reviewArtifactValue.payload;
|
|
2307
|
+
if (isReviewArtifactPayload(payload)) {
|
|
2308
|
+
artifactPayload = payload;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
if (!artifactPayload) {
|
|
2312
|
+
throw new Error(`Workflow post-fix-status node "${nodeId}" needs a review artifact (with.reviewArtifact).`);
|
|
2313
|
+
}
|
|
2314
|
+
const fixReviewName = getStringActionOption(node, 'fixReview', context);
|
|
2315
|
+
const fixReviewResult = fixReviewName ? context.artifacts[fixReviewName] : undefined;
|
|
2316
|
+
const hasReReview = isReviewResult(fixReviewResult);
|
|
2317
|
+
const fixChangeName = getStringActionOption(node, 'fixChange', context);
|
|
2318
|
+
const fixChange = fixChangeName ? context.artifacts[fixChangeName] : undefined;
|
|
2319
|
+
const fixSource = isReviewSource(fixChange) ? fixChange : undefined;
|
|
2320
|
+
const stackedPrUrl = getStringActionOption(node, 'stackedPrUrl', context);
|
|
2321
|
+
const marker = getStringActionOption(node, 'marker', context)?.trim() ?? 'drs-fix-status';
|
|
2322
|
+
const severity = (getStringActionOption(node, 'severity', context) ?? 'high').toUpperCase();
|
|
2323
|
+
const thresholdRank = severityRank(severity);
|
|
2324
|
+
const originalFindings = artifactPayload.findings.filter((finding) => severityRank(finding.issue.severity) >= thresholdRank);
|
|
2325
|
+
const statuses = originalFindings.map((finding) => {
|
|
2326
|
+
if (hasReReview) {
|
|
2327
|
+
const disposition = getFixStatusDisposition(finding);
|
|
2328
|
+
const diffSnippet = disposition === 'resolved' || disposition === 'regression'
|
|
2329
|
+
? extractDiffSnippet(fixSource, finding.issue.file, finding.issue.line)
|
|
2330
|
+
: undefined;
|
|
2331
|
+
return { finding, disposition, diffSnippet };
|
|
2332
|
+
}
|
|
2333
|
+
const diffSnippet = extractDiffSnippet(fixSource, finding.issue.file, finding.issue.line);
|
|
2334
|
+
return { finding, disposition: 'attempted', diffSnippet };
|
|
2335
|
+
});
|
|
2336
|
+
const body = formatFixStatusComment(statuses, stackedPrUrl);
|
|
2337
|
+
const markedBody = formatMarkedComment(body, marker);
|
|
2338
|
+
let operation = 'created';
|
|
2339
|
+
try {
|
|
2340
|
+
const comments = await target.platformClient.getComments(target.projectId, target.prNumber);
|
|
2341
|
+
const existingComment = findExistingCommentById(comments, marker);
|
|
2342
|
+
if (existingComment) {
|
|
2343
|
+
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, () => target.platformClient.updateComment(target.projectId, target.prNumber, existingComment.id, markedBody));
|
|
2344
|
+
operation = 'updated';
|
|
2345
|
+
}
|
|
2346
|
+
else {
|
|
2347
|
+
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, () => target.platformClient.createComment(target.projectId, target.prNumber, markedBody));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
catch (error) {
|
|
2351
|
+
throw new Error(`Workflow post-fix-status node "${nodeId}" failed to post comment: ${error instanceof Error ? error.message : String(error)}`);
|
|
2352
|
+
}
|
|
2353
|
+
return {
|
|
2354
|
+
id: nodeId,
|
|
2355
|
+
type: 'action',
|
|
2356
|
+
action: node.action,
|
|
2357
|
+
response: `${operation} fix-status comment on ${target.platform} ${target.projectId}#${target.prNumber}`,
|
|
2358
|
+
output: {
|
|
2359
|
+
platform: target.platform,
|
|
2360
|
+
projectId: target.projectId,
|
|
2361
|
+
prNumber: target.prNumber,
|
|
2362
|
+
operation,
|
|
2363
|
+
resolved: statuses.filter((s) => s.disposition === 'resolved').length,
|
|
2364
|
+
partial: statuses.filter((s) => s.disposition === 'partial').length,
|
|
2365
|
+
stillOpen: statuses.filter((s) => s.disposition === 'still-open').length,
|
|
2366
|
+
regression: statuses.filter((s) => s.disposition === 'regression').length,
|
|
2367
|
+
attempted: statuses.filter((s) => s.disposition === 'attempted').length,
|
|
2368
|
+
},
|
|
2369
|
+
};
|
|
2370
|
+
}
|
|
1018
2371
|
function isReviewSource(value) {
|
|
1019
2372
|
if (!value || typeof value !== 'object') {
|
|
1020
2373
|
return false;
|
|
@@ -1025,23 +2378,6 @@ function isReviewSource(value) {
|
|
|
1025
2378
|
typeof candidate.context === 'object' &&
|
|
1026
2379
|
candidate.context !== null);
|
|
1027
2380
|
}
|
|
1028
|
-
async function enforceWorkflowReviewTarget(config, source, workingDir) {
|
|
1029
|
-
const platform = typeof source.context.platform === 'string' ? source.context.platform : undefined;
|
|
1030
|
-
if (!isWorkflowPlatform(platform)) {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const projectId = typeof source.context.projectId === 'string' ? source.context.projectId : undefined;
|
|
1034
|
-
const pullRequest = isPullRequest(source.context.pullRequest)
|
|
1035
|
-
? source.context.pullRequest
|
|
1036
|
-
: undefined;
|
|
1037
|
-
if (!projectId || !pullRequest) {
|
|
1038
|
-
throw new Error('Workflow platform review source is missing project or PR/MR metadata.');
|
|
1039
|
-
}
|
|
1040
|
-
await enforceRepoBranchMatch(workingDir, projectId, pullRequest, {
|
|
1041
|
-
skipRepoCheck: config.review.skipRepoCheck,
|
|
1042
|
-
skipBranchCheck: config.review.skipBranchCheck,
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
2381
|
async function runReviewWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
1046
2382
|
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
1047
2383
|
const source = context.artifacts[sourceArtifact];
|
|
@@ -1049,10 +2385,46 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
1049
2385
|
throw new Error(`Workflow review node "${nodeId}" needs a ReviewSource artifact. ` +
|
|
1050
2386
|
'Set with.source to a change-source output.');
|
|
1051
2387
|
}
|
|
2388
|
+
const reviewArtifactName = getStringActionOption(node, 'reviewArtifact', context);
|
|
2389
|
+
const reviewArtifactEnvelope = reviewArtifactName
|
|
2390
|
+
? context.artifacts[reviewArtifactName]
|
|
2391
|
+
: undefined;
|
|
2392
|
+
const reviewArtifact = reviewArtifactName
|
|
2393
|
+
? getReviewArtifactPayloadFromValue(nodeId, reviewArtifactEnvelope)
|
|
2394
|
+
: undefined;
|
|
2395
|
+
const reviewArtifactPath = reviewArtifactEnvelope && typeof reviewArtifactEnvelope === 'object'
|
|
2396
|
+
? reviewArtifactEnvelope.path
|
|
2397
|
+
: undefined;
|
|
2398
|
+
const severity = getStringActionOption(node, 'severity', context)?.toUpperCase();
|
|
2399
|
+
const traceCollector = executionContext.traceCollector;
|
|
2400
|
+
const sourceForReview = reviewArtifact
|
|
2401
|
+
? {
|
|
2402
|
+
...source,
|
|
2403
|
+
context: {
|
|
2404
|
+
...source.context,
|
|
2405
|
+
verification: {
|
|
2406
|
+
artifact: {
|
|
2407
|
+
reviewId: reviewArtifact.reviewId,
|
|
2408
|
+
findings: reviewArtifact.findings,
|
|
2409
|
+
},
|
|
2410
|
+
artifactPath: reviewArtifactPath,
|
|
2411
|
+
severity,
|
|
2412
|
+
},
|
|
2413
|
+
traceCollector,
|
|
2414
|
+
},
|
|
2415
|
+
}
|
|
2416
|
+
: {
|
|
2417
|
+
...source,
|
|
2418
|
+
context: {
|
|
2419
|
+
...source.context,
|
|
2420
|
+
traceCollector,
|
|
2421
|
+
},
|
|
2422
|
+
};
|
|
2423
|
+
if (traceCollector) {
|
|
2424
|
+
const agentIds = getReviewAgentIds(config);
|
|
2425
|
+
traceCollector.setContext(nodeId, agentIds[0] ?? 'review/unified-reviewer', '');
|
|
2426
|
+
}
|
|
1052
2427
|
const reviewResult = await withWorkflowLock(executionContext.locks.exit, async () => {
|
|
1053
|
-
const restoreExit = setExitHandler((code) => {
|
|
1054
|
-
throw new ExitError(code);
|
|
1055
|
-
});
|
|
1056
2428
|
const originalLog = console.log;
|
|
1057
2429
|
const originalWarn = console.warn;
|
|
1058
2430
|
if (options.jsonOutput) {
|
|
@@ -1060,38 +2432,45 @@ async function runReviewWorkflowNode(config, nodeId, node, options, workingDir,
|
|
|
1060
2432
|
console.warn = () => undefined;
|
|
1061
2433
|
}
|
|
1062
2434
|
try {
|
|
1063
|
-
await enforceWorkflowReviewTarget(config, source, source.workingDir ?? workingDir);
|
|
1064
2435
|
return await executeReview(config, {
|
|
1065
|
-
...
|
|
1066
|
-
workingDir:
|
|
2436
|
+
...sourceForReview,
|
|
2437
|
+
workingDir: sourceForReview.workingDir ?? workingDir,
|
|
1067
2438
|
debug: options.debug,
|
|
1068
2439
|
thinkingLevel: options.thinkingLevel,
|
|
1069
2440
|
});
|
|
1070
2441
|
}
|
|
1071
|
-
catch (error) {
|
|
1072
|
-
if (error instanceof ExitError) {
|
|
1073
|
-
throw new Error(`Workflow review node "${nodeId}" failed: all review agents failed.`);
|
|
1074
|
-
}
|
|
1075
|
-
throw error;
|
|
1076
|
-
}
|
|
1077
2442
|
finally {
|
|
1078
2443
|
if (options.jsonOutput) {
|
|
1079
2444
|
console.log = originalLog;
|
|
1080
2445
|
console.warn = originalWarn;
|
|
1081
2446
|
}
|
|
1082
|
-
restoreExit();
|
|
1083
2447
|
}
|
|
1084
2448
|
});
|
|
1085
2449
|
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
1086
2450
|
if (writes) {
|
|
1087
2451
|
await writeWorkflowFile(workingDir, writes, JSON.stringify(reviewResult, null, 2));
|
|
1088
2452
|
}
|
|
2453
|
+
const artifactOutput = getStringActionOption(node, 'artifact', context)?.trim();
|
|
2454
|
+
const outputs = {};
|
|
2455
|
+
let artifactResponse = '';
|
|
2456
|
+
if (artifactOutput) {
|
|
2457
|
+
const reviewArtifact = createReviewArtifactPayload(reviewResult, source);
|
|
2458
|
+
const scope = await resolveArtifactScope(nodeId, node, workingDir, context, executionContext);
|
|
2459
|
+
const saved = await saveWorkflowArtifact(workingDir, {
|
|
2460
|
+
kind: 'review',
|
|
2461
|
+
scope,
|
|
2462
|
+
payload: reviewArtifact,
|
|
2463
|
+
});
|
|
2464
|
+
outputs[artifactOutput] = { ...saved.artifact, path: saved.path, latestPath: saved.latestPath };
|
|
2465
|
+
artifactResponse = `\nSaved review artifact ${saved.artifact.id}.`;
|
|
2466
|
+
}
|
|
1089
2467
|
return {
|
|
1090
2468
|
id: nodeId,
|
|
1091
2469
|
type: 'action',
|
|
1092
2470
|
action: node.action,
|
|
1093
|
-
response: JSON.stringify(reviewResult.summary, null, 2)
|
|
2471
|
+
response: `${JSON.stringify(reviewResult.summary, null, 2)}${artifactResponse}`,
|
|
1094
2472
|
output: reviewResult,
|
|
2473
|
+
outputs,
|
|
1095
2474
|
writes,
|
|
1096
2475
|
};
|
|
1097
2476
|
}
|
|
@@ -1101,18 +2480,522 @@ function recordNodeArtifact(nodeId, node, result, artifacts) {
|
|
|
1101
2480
|
if (node.output) {
|
|
1102
2481
|
artifacts[node.output] = artifactValue;
|
|
1103
2482
|
}
|
|
2483
|
+
if (result.outputs) {
|
|
2484
|
+
for (const [name, value] of Object.entries(result.outputs)) {
|
|
2485
|
+
artifacts[name] = value;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function parseWorkflowExpressionValue(value, context) {
|
|
2490
|
+
const trimmed = value.trim();
|
|
2491
|
+
if (!trimmed)
|
|
2492
|
+
return '';
|
|
2493
|
+
const templateReference = trimmed.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
2494
|
+
if (templateReference && context) {
|
|
2495
|
+
const path = templateReference[1]?.trim() ?? '';
|
|
2496
|
+
const resolved = getPathValue(context, path);
|
|
2497
|
+
if (resolved === undefined) {
|
|
2498
|
+
throw new Error(`Unknown workflow template value "{{${path}}}".`);
|
|
2499
|
+
}
|
|
2500
|
+
return resolved;
|
|
2501
|
+
}
|
|
2502
|
+
if (trimmed === 'true')
|
|
2503
|
+
return true;
|
|
2504
|
+
if (trimmed === 'false')
|
|
2505
|
+
return false;
|
|
2506
|
+
if (trimmed === 'null')
|
|
2507
|
+
return null;
|
|
2508
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed))
|
|
2509
|
+
return Number(trimmed);
|
|
2510
|
+
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
2511
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
2512
|
+
const inner = trimmed.slice(1, -1);
|
|
2513
|
+
return context ? renderTemplate(inner, context) : inner;
|
|
2514
|
+
}
|
|
2515
|
+
if (context && /^(inputs|nodes|artifacts|loop)\.[A-Za-z0-9_.-]+$/.test(trimmed)) {
|
|
2516
|
+
const resolved = getPathValue(context, trimmed);
|
|
2517
|
+
if (resolved === undefined) {
|
|
2518
|
+
throw new Error(`Unknown workflow expression value "${trimmed}".`);
|
|
2519
|
+
}
|
|
2520
|
+
return resolved;
|
|
2521
|
+
}
|
|
2522
|
+
try {
|
|
2523
|
+
return JSON.parse(trimmed);
|
|
2524
|
+
}
|
|
2525
|
+
catch {
|
|
2526
|
+
return trimmed;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
function isWorkflowTruthy(value) {
|
|
2530
|
+
if (value === undefined || value === null)
|
|
2531
|
+
return false;
|
|
2532
|
+
if (typeof value === 'boolean')
|
|
2533
|
+
return value;
|
|
2534
|
+
if (typeof value === 'number')
|
|
2535
|
+
return value !== 0;
|
|
2536
|
+
if (typeof value === 'string') {
|
|
2537
|
+
const normalized = value.trim().toLowerCase();
|
|
2538
|
+
return (normalized.length > 0 && normalized !== 'false' && normalized !== '0' && normalized !== 'no');
|
|
2539
|
+
}
|
|
2540
|
+
if (Array.isArray(value))
|
|
2541
|
+
return value.length > 0;
|
|
2542
|
+
if (typeof value === 'object')
|
|
2543
|
+
return Object.keys(value).length > 0;
|
|
2544
|
+
return true;
|
|
2545
|
+
}
|
|
2546
|
+
function normalizeWorkflowBooleanLike(value) {
|
|
2547
|
+
if (typeof value === 'boolean')
|
|
2548
|
+
return value;
|
|
2549
|
+
if (typeof value === 'number') {
|
|
2550
|
+
if (value === 1)
|
|
2551
|
+
return true;
|
|
2552
|
+
if (value === 0)
|
|
2553
|
+
return false;
|
|
2554
|
+
}
|
|
2555
|
+
if (typeof value === 'string') {
|
|
2556
|
+
const normalized = value.trim().toLowerCase();
|
|
2557
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes')
|
|
2558
|
+
return true;
|
|
2559
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no')
|
|
2560
|
+
return false;
|
|
2561
|
+
}
|
|
2562
|
+
return undefined;
|
|
2563
|
+
}
|
|
2564
|
+
function compareWorkflowValues(left, operator, right) {
|
|
2565
|
+
if (operator === '==' || operator === '!=') {
|
|
2566
|
+
const leftBoolean = normalizeWorkflowBooleanLike(left);
|
|
2567
|
+
const rightBoolean = normalizeWorkflowBooleanLike(right);
|
|
2568
|
+
const matches = leftBoolean !== undefined || rightBoolean !== undefined
|
|
2569
|
+
? leftBoolean === rightBoolean
|
|
2570
|
+
: String(left) === String(right);
|
|
2571
|
+
return operator === '==' ? matches : !matches;
|
|
2572
|
+
}
|
|
2573
|
+
const leftNumber = Number(left);
|
|
2574
|
+
const rightNumber = Number(right);
|
|
2575
|
+
if (!Number.isFinite(leftNumber) || !Number.isFinite(rightNumber)) {
|
|
2576
|
+
throw new Error(`Workflow expression operator "${operator}" requires numeric values.`);
|
|
2577
|
+
}
|
|
2578
|
+
if (operator === '>')
|
|
2579
|
+
return leftNumber > rightNumber;
|
|
2580
|
+
if (operator === '>=')
|
|
2581
|
+
return leftNumber >= rightNumber;
|
|
2582
|
+
if (operator === '<')
|
|
2583
|
+
return leftNumber < rightNumber;
|
|
2584
|
+
if (operator === '<=')
|
|
2585
|
+
return leftNumber <= rightNumber;
|
|
2586
|
+
throw new Error(`Unsupported workflow expression operator "${operator}".`);
|
|
2587
|
+
}
|
|
2588
|
+
function splitWorkflowExpressionOperator(expression, operator) {
|
|
2589
|
+
const parts = [];
|
|
2590
|
+
let current = '';
|
|
2591
|
+
let quote;
|
|
2592
|
+
let depth = 0;
|
|
2593
|
+
for (let i = 0; i < expression.length; i++) {
|
|
2594
|
+
const char = expression[i] ?? '';
|
|
2595
|
+
if ((char === '"' || char === "'") && expression[i - 1] !== '\\') {
|
|
2596
|
+
quote = quote === char ? undefined : (quote ?? char);
|
|
2597
|
+
current += char;
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
if (!quote && char === '(') {
|
|
2601
|
+
depth += 1;
|
|
2602
|
+
current += char;
|
|
2603
|
+
continue;
|
|
2604
|
+
}
|
|
2605
|
+
if (!quote && char === ')' && depth > 0) {
|
|
2606
|
+
depth -= 1;
|
|
2607
|
+
current += char;
|
|
2608
|
+
continue;
|
|
2609
|
+
}
|
|
2610
|
+
if (!quote && depth === 0 && expression.slice(i, i + operator.length) === operator) {
|
|
2611
|
+
parts.push(current.trim());
|
|
2612
|
+
current = '';
|
|
2613
|
+
i += operator.length - 1;
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
current += char;
|
|
2617
|
+
}
|
|
2618
|
+
parts.push(current.trim());
|
|
2619
|
+
return parts;
|
|
2620
|
+
}
|
|
2621
|
+
function stripWorkflowExpressionParens(expression) {
|
|
2622
|
+
const trimmed = expression.trim();
|
|
2623
|
+
if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) {
|
|
2624
|
+
return trimmed;
|
|
2625
|
+
}
|
|
2626
|
+
let quote;
|
|
2627
|
+
let depth = 0;
|
|
2628
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
2629
|
+
const char = trimmed[i] ?? '';
|
|
2630
|
+
if ((char === '"' || char === "'") && trimmed[i - 1] !== '\\') {
|
|
2631
|
+
quote = quote === char ? undefined : (quote ?? char);
|
|
2632
|
+
continue;
|
|
2633
|
+
}
|
|
2634
|
+
if (quote)
|
|
2635
|
+
continue;
|
|
2636
|
+
if (char === '(')
|
|
2637
|
+
depth += 1;
|
|
2638
|
+
if (char === ')')
|
|
2639
|
+
depth -= 1;
|
|
2640
|
+
if (depth === 0 && i < trimmed.length - 1) {
|
|
2641
|
+
return trimmed;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return trimmed.slice(1, -1).trim();
|
|
2645
|
+
}
|
|
2646
|
+
function evaluateWorkflowExpressionText(rendered, context) {
|
|
2647
|
+
rendered = stripWorkflowExpressionParens(rendered);
|
|
2648
|
+
const orParts = splitWorkflowExpressionOperator(rendered, '||');
|
|
2649
|
+
if (orParts.length > 1) {
|
|
2650
|
+
return orParts.some((part) => evaluateWorkflowExpressionText(part, context));
|
|
2651
|
+
}
|
|
2652
|
+
const andParts = splitWorkflowExpressionOperator(rendered, '&&');
|
|
2653
|
+
if (andParts.length > 1) {
|
|
2654
|
+
return andParts.every((part) => evaluateWorkflowExpressionText(part, context));
|
|
2655
|
+
}
|
|
2656
|
+
const match = rendered.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
|
|
2657
|
+
if (!match) {
|
|
2658
|
+
return isWorkflowTruthy(parseWorkflowExpressionValue(rendered, context));
|
|
2659
|
+
}
|
|
2660
|
+
return compareWorkflowValues(parseWorkflowExpressionValue(match[1] ?? '', context), match[2] ?? '', parseWorkflowExpressionValue(match[3] ?? '', context));
|
|
2661
|
+
}
|
|
2662
|
+
function evaluateWorkflowExpression(expression, context) {
|
|
2663
|
+
return evaluateWorkflowExpressionText(expression.trim(), context);
|
|
2664
|
+
}
|
|
2665
|
+
function splitWorkflowSegments(workflowNodes, executionOrder) {
|
|
2666
|
+
const segments = [];
|
|
2667
|
+
let currentDag = [];
|
|
2668
|
+
for (const nodeId of executionOrder) {
|
|
2669
|
+
const node = workflowNodes[nodeId];
|
|
2670
|
+
if (!node) {
|
|
2671
|
+
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
2672
|
+
}
|
|
2673
|
+
if (node.control !== undefined) {
|
|
2674
|
+
if (currentDag.length > 0) {
|
|
2675
|
+
segments.push({ type: 'dag', nodeIds: currentDag });
|
|
2676
|
+
currentDag = [];
|
|
2677
|
+
}
|
|
2678
|
+
segments.push({ type: 'control', nodeId });
|
|
2679
|
+
}
|
|
2680
|
+
else {
|
|
2681
|
+
currentDag.push(nodeId);
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (currentDag.length > 0) {
|
|
2685
|
+
segments.push({ type: 'dag', nodeIds: currentDag });
|
|
2686
|
+
}
|
|
2687
|
+
return segments;
|
|
2688
|
+
}
|
|
2689
|
+
function findWorkflowSegmentIndex(segments, targetNodeId) {
|
|
2690
|
+
return segments.findIndex((segment) => segment.type === 'control'
|
|
2691
|
+
? segment.nodeId === targetNodeId
|
|
2692
|
+
: segment.nodeIds.includes(targetNodeId));
|
|
2693
|
+
}
|
|
2694
|
+
function computeActiveWorkflowNodes(workflowNodes, nodeIds, rootNodeId, includeRootDependencies = true) {
|
|
2695
|
+
const segmentNodeIds = new Set(nodeIds);
|
|
2696
|
+
const downstream = new Set([rootNodeId]);
|
|
2697
|
+
let changed = true;
|
|
2698
|
+
while (changed) {
|
|
2699
|
+
changed = false;
|
|
2700
|
+
for (const nodeId of nodeIds) {
|
|
2701
|
+
if (downstream.has(nodeId))
|
|
2702
|
+
continue;
|
|
2703
|
+
const needs = getNodeNeeds(workflowNodes[nodeId] ?? {});
|
|
2704
|
+
if (needs.some((dependency) => downstream.has(dependency))) {
|
|
2705
|
+
downstream.add(nodeId);
|
|
2706
|
+
changed = true;
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const active = new Set(downstream);
|
|
2711
|
+
const includeDependencies = (nodeId) => {
|
|
2712
|
+
const node = workflowNodes[nodeId];
|
|
2713
|
+
if (!node)
|
|
2714
|
+
return;
|
|
2715
|
+
for (const dependency of getNodeNeeds(node)) {
|
|
2716
|
+
if (!segmentNodeIds.has(dependency) || active.has(dependency))
|
|
2717
|
+
continue;
|
|
2718
|
+
active.add(dependency);
|
|
2719
|
+
includeDependencies(dependency);
|
|
2720
|
+
}
|
|
2721
|
+
};
|
|
2722
|
+
if (includeRootDependencies) {
|
|
2723
|
+
for (const nodeId of downstream) {
|
|
2724
|
+
includeDependencies(nodeId);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
return active;
|
|
2728
|
+
}
|
|
2729
|
+
function createSkippedWorkflowNodeResult(nodeId) {
|
|
2730
|
+
return {
|
|
2731
|
+
id: nodeId,
|
|
2732
|
+
type: 'skipped',
|
|
2733
|
+
status: 'skipped',
|
|
2734
|
+
response: '',
|
|
2735
|
+
output: undefined,
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
function getWorkflowNodeRunCondition(node) {
|
|
2739
|
+
return node.if;
|
|
2740
|
+
}
|
|
2741
|
+
function getSkippedWorkflowDependencies(node, nodes) {
|
|
2742
|
+
return getNodeNeeds(node).filter((dependency) => nodes[dependency]?.status === 'skipped');
|
|
2743
|
+
}
|
|
2744
|
+
function getWorkflowNodeSkipReason(node, context) {
|
|
2745
|
+
const skippedDependencies = getSkippedWorkflowDependencies(node, context.nodes);
|
|
2746
|
+
if (skippedDependencies.length > 0) {
|
|
2747
|
+
return `dependency skipped: ${skippedDependencies.join(', ')}`;
|
|
2748
|
+
}
|
|
2749
|
+
const ifExpression = getWorkflowNodeRunCondition(node);
|
|
2750
|
+
if (ifExpression !== undefined && !evaluateWorkflowExpression(ifExpression, context)) {
|
|
2751
|
+
return `if false: ${ifExpression}`;
|
|
2752
|
+
}
|
|
2753
|
+
return undefined;
|
|
2754
|
+
}
|
|
2755
|
+
function logWorkflowNodeRunning(nodeId, options) {
|
|
2756
|
+
if (!options.jsonOutput) {
|
|
2757
|
+
console.log(chalk.gray(`Running node ${nodeId}...`));
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
function logWorkflowNodeSkipped(nodeId, reason, options) {
|
|
2761
|
+
if (!options.jsonOutput) {
|
|
2762
|
+
console.log(chalk.gray(`Skipping node ${nodeId} (${reason})`));
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
function recordWorkflowNodeResult(nodeId, node, result, nodes, artifacts) {
|
|
2766
|
+
result.status ??= 'success';
|
|
2767
|
+
nodes[nodeId] = result;
|
|
2768
|
+
if (result.status !== 'skipped') {
|
|
2769
|
+
recordNodeArtifact(nodeId, node, result, artifacts);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
async function runWorkflowDagSegment(config, workflowNodes, nodeIds, activeNodeIds, options, workingDir, context, executionContext) {
|
|
2773
|
+
const completed = new Set();
|
|
2774
|
+
const segmentNodeIds = new Set(nodeIds);
|
|
2775
|
+
if (activeNodeIds) {
|
|
2776
|
+
for (const nodeId of nodeIds) {
|
|
2777
|
+
if (!activeNodeIds.has(nodeId)) {
|
|
2778
|
+
completed.add(nodeId);
|
|
2779
|
+
if (context.nodes[nodeId] === undefined) {
|
|
2780
|
+
logWorkflowNodeSkipped(nodeId, 'inactive branch', options);
|
|
2781
|
+
context.nodes[nodeId] = createSkippedWorkflowNodeResult(nodeId);
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
while (completed.size < nodeIds.length) {
|
|
2787
|
+
const runnable = nodeIds.filter((nodeId) => {
|
|
2788
|
+
if (completed.has(nodeId))
|
|
2789
|
+
return false;
|
|
2790
|
+
const node = workflowNodes[nodeId];
|
|
2791
|
+
if (!node)
|
|
2792
|
+
return false;
|
|
2793
|
+
return getNodeNeeds(node).every((dependency) => completed.has(dependency) || !segmentNodeIds.has(dependency));
|
|
2794
|
+
});
|
|
2795
|
+
if (runnable.length === 0) {
|
|
2796
|
+
throw new Error('Workflow control runner could not make progress in a DAG segment.');
|
|
2797
|
+
}
|
|
2798
|
+
const settled = await Promise.allSettled(runnable.map(async (nodeId) => {
|
|
2799
|
+
const node = workflowNodes[nodeId];
|
|
2800
|
+
if (!node) {
|
|
2801
|
+
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
2802
|
+
}
|
|
2803
|
+
const skipReason = getWorkflowNodeSkipReason(node, context);
|
|
2804
|
+
if (skipReason) {
|
|
2805
|
+
logWorkflowNodeSkipped(nodeId, skipReason, options);
|
|
2806
|
+
return { nodeId, node, result: createSkippedWorkflowNodeResult(nodeId) };
|
|
2807
|
+
}
|
|
2808
|
+
logWorkflowNodeRunning(nodeId, options);
|
|
2809
|
+
const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
2810
|
+
return { nodeId, node, result };
|
|
2811
|
+
}));
|
|
2812
|
+
const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
|
|
2813
|
+
if (firstRejection) {
|
|
2814
|
+
throw firstRejection.reason instanceof Error
|
|
2815
|
+
? firstRejection.reason
|
|
2816
|
+
: new Error(String(firstRejection.reason));
|
|
2817
|
+
}
|
|
2818
|
+
for (const outcome of settled) {
|
|
2819
|
+
const { nodeId, node, result } = outcome.value;
|
|
2820
|
+
completed.add(nodeId);
|
|
2821
|
+
recordWorkflowNodeResult(nodeId, node, result, context.nodes, context.artifacts);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
function runControlWorkflowNode(nodeId, node, context) {
|
|
2826
|
+
if (node.control === 'loop') {
|
|
2827
|
+
const expression = node.if;
|
|
2828
|
+
if (!expression) {
|
|
2829
|
+
throw new Error(`Workflow loop node "${nodeId}" must define if.`);
|
|
2830
|
+
}
|
|
2831
|
+
if (!node.target || !node.exit) {
|
|
2832
|
+
throw new Error(`Workflow loop node "${nodeId}" must define target and exit.`);
|
|
2833
|
+
}
|
|
2834
|
+
const rawMaxIterations = node.maxIterations;
|
|
2835
|
+
const renderedMaxIterations = typeof rawMaxIterations === 'string'
|
|
2836
|
+
? renderTemplate(rawMaxIterations, context).trim()
|
|
2837
|
+
: String(rawMaxIterations ?? '');
|
|
2838
|
+
const configuredMaxIterations = Number.parseInt(renderedMaxIterations, 10);
|
|
2839
|
+
if (renderedMaxIterations === '' ||
|
|
2840
|
+
!Number.isInteger(configuredMaxIterations) ||
|
|
2841
|
+
configuredMaxIterations <= 0) {
|
|
2842
|
+
throw new Error(`Workflow loop node "${nodeId}" must define a positive maxIterations.`);
|
|
2843
|
+
}
|
|
2844
|
+
const maxIterations = configuredMaxIterations;
|
|
2845
|
+
const shouldLoop = evaluateWorkflowExpression(expression, context);
|
|
2846
|
+
const current = context.loop[nodeId] ?? { iteration: 1, maxIterations };
|
|
2847
|
+
let nextNodeId = node.exit;
|
|
2848
|
+
let decision = 'exit';
|
|
2849
|
+
if (shouldLoop) {
|
|
2850
|
+
if (current.iteration >= maxIterations) {
|
|
2851
|
+
if (node.onMaxIterations === 'exit') {
|
|
2852
|
+
nextNodeId = node.exit;
|
|
2853
|
+
}
|
|
2854
|
+
else {
|
|
2855
|
+
throw new Error(`Workflow loop node "${nodeId}" reached maxIterations (${maxIterations}).`);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
else {
|
|
2859
|
+
decision = 'loop';
|
|
2860
|
+
nextNodeId = node.target;
|
|
2861
|
+
current.iteration += 1;
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
current.maxIterations = maxIterations;
|
|
2865
|
+
current.lastDecision = decision;
|
|
2866
|
+
context.loop[nodeId] = current;
|
|
2867
|
+
return {
|
|
2868
|
+
nextNodeId,
|
|
2869
|
+
result: {
|
|
2870
|
+
id: nodeId,
|
|
2871
|
+
type: 'control',
|
|
2872
|
+
status: 'success',
|
|
2873
|
+
control: node.control,
|
|
2874
|
+
decision,
|
|
2875
|
+
target: nextNodeId,
|
|
2876
|
+
response: decision,
|
|
2877
|
+
output: {
|
|
2878
|
+
matched: shouldLoop,
|
|
2879
|
+
target: nextNodeId,
|
|
2880
|
+
iteration: current.iteration,
|
|
2881
|
+
maxIterations,
|
|
2882
|
+
},
|
|
2883
|
+
},
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
if (node.control === 'switch') {
|
|
2887
|
+
if (!node.value || !node.cases) {
|
|
2888
|
+
throw new Error(`Workflow switch node "${nodeId}" must define value and cases.`);
|
|
2889
|
+
}
|
|
2890
|
+
const value = renderTemplate(node.value, context).trim();
|
|
2891
|
+
const nextNodeId = node.cases[value] ?? node.default;
|
|
2892
|
+
if (!nextNodeId) {
|
|
2893
|
+
throw new Error(`Workflow switch node "${nodeId}" has no case for "${value}" or default.`);
|
|
2894
|
+
}
|
|
2895
|
+
return {
|
|
2896
|
+
nextNodeId,
|
|
2897
|
+
result: {
|
|
2898
|
+
id: nodeId,
|
|
2899
|
+
type: 'control',
|
|
2900
|
+
status: 'success',
|
|
2901
|
+
control: node.control,
|
|
2902
|
+
decision: value,
|
|
2903
|
+
target: nextNodeId,
|
|
2904
|
+
response: value,
|
|
2905
|
+
output: { value, target: nextNodeId },
|
|
2906
|
+
},
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
if (node.control === 'end') {
|
|
2910
|
+
return {
|
|
2911
|
+
ended: true,
|
|
2912
|
+
result: {
|
|
2913
|
+
id: nodeId,
|
|
2914
|
+
type: 'control',
|
|
2915
|
+
status: 'success',
|
|
2916
|
+
control: node.control,
|
|
2917
|
+
decision: 'end',
|
|
2918
|
+
response: 'end',
|
|
2919
|
+
output: { ended: true },
|
|
2920
|
+
},
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
if (node.control === 'passThrough') {
|
|
2924
|
+
const result = {
|
|
2925
|
+
id: nodeId,
|
|
2926
|
+
type: 'control',
|
|
2927
|
+
control: 'passThrough',
|
|
2928
|
+
target: node.target,
|
|
2929
|
+
decision: 'pass',
|
|
2930
|
+
response: `passed through to ${node.target}`,
|
|
2931
|
+
};
|
|
2932
|
+
return { result, nextNodeId: node.target };
|
|
2933
|
+
}
|
|
2934
|
+
throw new Error(`Unsupported workflow control "${node.control}" in node "${nodeId}".`);
|
|
2935
|
+
}
|
|
2936
|
+
async function runControlWorkflow(config, workflowNodes, executionOrder, options, workingDir, context, executionContext) {
|
|
2937
|
+
const segments = splitWorkflowSegments(workflowNodes, executionOrder);
|
|
2938
|
+
let segmentIndex = 0;
|
|
2939
|
+
while (segmentIndex < segments.length) {
|
|
2940
|
+
const segment = segments[segmentIndex];
|
|
2941
|
+
if (segment.type === 'dag') {
|
|
2942
|
+
await runWorkflowDagSegment(config, workflowNodes, segment.nodeIds, segment.activeNodeIds, options, workingDir, context, executionContext);
|
|
2943
|
+
segmentIndex += 1;
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
const node = workflowNodes[segment.nodeId];
|
|
2947
|
+
if (!node) {
|
|
2948
|
+
throw new Error(`Workflow references unknown node "${segment.nodeId}".`);
|
|
2949
|
+
}
|
|
2950
|
+
const skipReason = getWorkflowNodeSkipReason(node, context);
|
|
2951
|
+
if (skipReason) {
|
|
2952
|
+
logWorkflowNodeSkipped(segment.nodeId, skipReason, options);
|
|
2953
|
+
}
|
|
2954
|
+
else {
|
|
2955
|
+
logWorkflowNodeRunning(segment.nodeId, options);
|
|
2956
|
+
}
|
|
2957
|
+
const { result, nextNodeId, ended } = skipReason
|
|
2958
|
+
? { result: createSkippedWorkflowNodeResult(segment.nodeId) }
|
|
2959
|
+
: runControlWorkflowNode(segment.nodeId, node, context);
|
|
2960
|
+
recordWorkflowNodeResult(segment.nodeId, node, result, context.nodes, context.artifacts);
|
|
2961
|
+
if (ended) {
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
if (!nextNodeId) {
|
|
2965
|
+
segmentIndex += 1;
|
|
2966
|
+
continue;
|
|
2967
|
+
}
|
|
2968
|
+
const targetIndex = findWorkflowSegmentIndex(segments, nextNodeId);
|
|
2969
|
+
if (targetIndex < 0) {
|
|
2970
|
+
throw new Error(`Workflow control node "${segment.nodeId}" targets unknown node "${nextNodeId}".`);
|
|
2971
|
+
}
|
|
2972
|
+
if (node.control !== 'loop' && targetIndex <= segmentIndex) {
|
|
2973
|
+
throw new Error(`Workflow control node "${segment.nodeId}" cannot jump backward to "${nextNodeId}". Use control: loop with maxIterations for repeated execution.`);
|
|
2974
|
+
}
|
|
2975
|
+
const targetSegment = segments[targetIndex];
|
|
2976
|
+
if (targetSegment.type === 'dag') {
|
|
2977
|
+
targetSegment.activeNodeIds = computeActiveWorkflowNodes(workflowNodes, targetSegment.nodeIds, nextNodeId, !(node.control === 'loop' && nextNodeId === node.target));
|
|
2978
|
+
}
|
|
2979
|
+
segmentIndex = targetIndex;
|
|
2980
|
+
}
|
|
1104
2981
|
}
|
|
1105
2982
|
function formatWorkflowJson(result) {
|
|
1106
2983
|
return JSON.stringify(result, null, 2);
|
|
1107
2984
|
}
|
|
1108
2985
|
async function runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
1109
2986
|
const kind = getNodeKind(node);
|
|
2987
|
+
if (getWorkflowNodeSkipReason(node, context)) {
|
|
2988
|
+
return createSkippedWorkflowNodeResult(nodeId);
|
|
2989
|
+
}
|
|
1110
2990
|
if (kind === 'agent') {
|
|
1111
|
-
return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context);
|
|
2991
|
+
return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
1112
2992
|
}
|
|
1113
2993
|
if (kind === 'agents') {
|
|
1114
2994
|
return runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context);
|
|
1115
2995
|
}
|
|
2996
|
+
if (kind === 'control') {
|
|
2997
|
+
throw new Error(`Workflow control node "${nodeId}" cannot run in the static DAG executor.`);
|
|
2998
|
+
}
|
|
1116
2999
|
return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
1117
3000
|
}
|
|
1118
3001
|
export async function runWorkflow(config, workflowName, options = {}) {
|
|
@@ -1125,10 +3008,12 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
1125
3008
|
const inputs = await resolveWorkflowInputs(workflow, options, workingDir);
|
|
1126
3009
|
const nodes = {};
|
|
1127
3010
|
const artifacts = {};
|
|
1128
|
-
const
|
|
3011
|
+
const loop = {};
|
|
3012
|
+
const context = { inputs, nodes, artifacts, loop };
|
|
1129
3013
|
const executionContext = {
|
|
1130
3014
|
gitClients: new Map(),
|
|
1131
3015
|
platformClients: {},
|
|
3016
|
+
traceCollector: options.trace ? new TraceCollector() : undefined,
|
|
1132
3017
|
locks: {
|
|
1133
3018
|
exit: createWorkflowLock(),
|
|
1134
3019
|
console: createWorkflowLock(),
|
|
@@ -1139,32 +3024,60 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
1139
3024
|
if (!options.jsonOutput) {
|
|
1140
3025
|
console.log(chalk.gray(`Running workflow ${workflowName}...\n`));
|
|
1141
3026
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
3027
|
+
try {
|
|
3028
|
+
if (hasWorkflowControlNodes(workflowNodes)) {
|
|
3029
|
+
await runControlWorkflow(config, workflowNodes, executionOrder, options, workingDir, context, executionContext);
|
|
3030
|
+
}
|
|
3031
|
+
else {
|
|
3032
|
+
for (const wave of executionWaves) {
|
|
3033
|
+
const settled = await Promise.allSettled(wave.map(async (nodeId) => {
|
|
3034
|
+
const node = workflowNodes[nodeId];
|
|
3035
|
+
if (!node) {
|
|
3036
|
+
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
3037
|
+
}
|
|
3038
|
+
const skipReason = getWorkflowNodeSkipReason(node, context);
|
|
3039
|
+
if (skipReason) {
|
|
3040
|
+
logWorkflowNodeSkipped(nodeId, skipReason, options);
|
|
3041
|
+
return { nodeId, node, result: createSkippedWorkflowNodeResult(nodeId) };
|
|
3042
|
+
}
|
|
3043
|
+
logWorkflowNodeRunning(nodeId, options);
|
|
3044
|
+
const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
3045
|
+
return { nodeId, node, result };
|
|
3046
|
+
}));
|
|
3047
|
+
const firstRejection = settled.find((outcome) => outcome.status === 'rejected');
|
|
3048
|
+
if (firstRejection) {
|
|
3049
|
+
throw firstRejection.reason instanceof Error
|
|
3050
|
+
? firstRejection.reason
|
|
3051
|
+
: new Error(String(firstRejection.reason));
|
|
3052
|
+
}
|
|
3053
|
+
for (const outcome of settled) {
|
|
3054
|
+
const { nodeId, node, result } = outcome.value;
|
|
3055
|
+
recordWorkflowNodeResult(nodeId, node, result, nodes, artifacts);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
catch (error) {
|
|
3061
|
+
if (executionContext.traceCollector && executionContext.traceCollector.getTraces().length > 0) {
|
|
3062
|
+
try {
|
|
3063
|
+
await flushWorkflowTrace(executionContext.traceCollector, workflowName, inputs, new Date().toISOString(), workingDir, options);
|
|
1147
3064
|
}
|
|
1148
|
-
|
|
1149
|
-
console.
|
|
3065
|
+
catch (flushError) {
|
|
3066
|
+
console.error(chalk.yellow('Warning:'), 'Failed to persist workflow trace on failure:', flushError instanceof Error ? flushError.message : String(flushError));
|
|
1150
3067
|
}
|
|
1151
|
-
const result = await runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
1152
|
-
return { nodeId, node, result };
|
|
1153
|
-
}));
|
|
1154
|
-
for (const { nodeId, node, result } of results) {
|
|
1155
|
-
nodes[nodeId] = result;
|
|
1156
|
-
recordNodeArtifact(nodeId, node, result, artifacts);
|
|
1157
3068
|
}
|
|
3069
|
+
throw error;
|
|
1158
3070
|
}
|
|
1159
3071
|
const lastNodeId = executionOrder[executionOrder.length - 1];
|
|
1160
3072
|
const lastNode = workflowNodes[lastNodeId];
|
|
1161
|
-
const outputKey = lastNode.output ?? lastNodeId;
|
|
3073
|
+
const outputKey = workflow.output ?? lastNode.output ?? lastNodeId;
|
|
1162
3074
|
const result = {
|
|
1163
3075
|
timestamp: new Date().toISOString(),
|
|
1164
3076
|
workflow: workflowName,
|
|
1165
3077
|
inputs,
|
|
1166
3078
|
nodes,
|
|
1167
3079
|
artifacts,
|
|
3080
|
+
loop,
|
|
1168
3081
|
output: artifacts[outputKey],
|
|
1169
3082
|
};
|
|
1170
3083
|
if (options.outputPath) {
|
|
@@ -1173,6 +3086,9 @@ export async function runWorkflow(config, workflowName, options = {}) {
|
|
|
1173
3086
|
console.log(chalk.green(`\n✓ Workflow output saved to ${options.outputPath}`));
|
|
1174
3087
|
}
|
|
1175
3088
|
}
|
|
3089
|
+
if (executionContext.traceCollector && executionContext.traceCollector.getTraces().length > 0) {
|
|
3090
|
+
await flushWorkflowTrace(executionContext.traceCollector, workflowName, inputs, result.timestamp, workingDir, options);
|
|
3091
|
+
}
|
|
1176
3092
|
if (options.jsonOutput) {
|
|
1177
3093
|
console.log(formatWorkflowJson(result));
|
|
1178
3094
|
}
|
|
@@ -1226,4 +3142,168 @@ export function listWorkflows(config, options = {}) {
|
|
|
1226
3142
|
}
|
|
1227
3143
|
return entries;
|
|
1228
3144
|
}
|
|
3145
|
+
function getWorkflowNodeRoutes(node) {
|
|
3146
|
+
if (node.control === 'loop') {
|
|
3147
|
+
return { target: node.target, exit: node.exit };
|
|
3148
|
+
}
|
|
3149
|
+
if (node.control === 'switch') {
|
|
3150
|
+
return { cases: node.cases, default: node.default };
|
|
3151
|
+
}
|
|
3152
|
+
if (node.control === 'passThrough') {
|
|
3153
|
+
return { target: node.target };
|
|
3154
|
+
}
|
|
3155
|
+
return undefined;
|
|
3156
|
+
}
|
|
3157
|
+
function formatWorkflowInput(input) {
|
|
3158
|
+
if (typeof input === 'string') {
|
|
3159
|
+
return JSON.stringify(input);
|
|
3160
|
+
}
|
|
3161
|
+
if (input.file !== undefined) {
|
|
3162
|
+
return `file:${input.file}`;
|
|
3163
|
+
}
|
|
3164
|
+
return JSON.stringify(input.value ?? '');
|
|
3165
|
+
}
|
|
3166
|
+
function buildWorkflowDetail(name, workflow, workingDir) {
|
|
3167
|
+
const sourceInfo = loadWorkflowSourceInfo(workingDir);
|
|
3168
|
+
const info = sourceInfo[name] ?? {
|
|
3169
|
+
source: 'packaged',
|
|
3170
|
+
overridesPackaged: false,
|
|
3171
|
+
};
|
|
3172
|
+
const workflowNodes = getWorkflowNodes(name, workflow);
|
|
3173
|
+
getWorkflowExecutionOrder(workflowNodes);
|
|
3174
|
+
return {
|
|
3175
|
+
name,
|
|
3176
|
+
source: info.source,
|
|
3177
|
+
overridden: info.overridesPackaged,
|
|
3178
|
+
description: workflow.description,
|
|
3179
|
+
inputs: workflow.inputs ?? {},
|
|
3180
|
+
output: workflow.output,
|
|
3181
|
+
nodes: Object.entries(workflowNodes).map(([nodeId, node]) => ({
|
|
3182
|
+
id: nodeId,
|
|
3183
|
+
kind: getNodeKind(node),
|
|
3184
|
+
needs: getNodeNeeds(node),
|
|
3185
|
+
agent: node.agent,
|
|
3186
|
+
agentsFrom: node.agentsFrom,
|
|
3187
|
+
action: node.action,
|
|
3188
|
+
control: node.control,
|
|
3189
|
+
if: node.if,
|
|
3190
|
+
input: node.input,
|
|
3191
|
+
with: node.with,
|
|
3192
|
+
output: node.output,
|
|
3193
|
+
writes: node.writes,
|
|
3194
|
+
json: node.json,
|
|
3195
|
+
routes: getWorkflowNodeRoutes(node),
|
|
3196
|
+
})),
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
export function showWorkflow(config, workflowName, options = {}) {
|
|
3200
|
+
const workflow = config.workflows?.[workflowName];
|
|
3201
|
+
if (!workflow) {
|
|
3202
|
+
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
3203
|
+
}
|
|
3204
|
+
const detail = buildWorkflowDetail(workflowName, workflow, options.workingDir ?? process.cwd());
|
|
3205
|
+
if (options.json) {
|
|
3206
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
3207
|
+
return detail;
|
|
3208
|
+
}
|
|
3209
|
+
const sourceLabel = detail.source === 'packaged' ? chalk.gray('packaged') : chalk.cyan('project');
|
|
3210
|
+
const overridden = detail.overridden ? ` ${chalk.yellow('(overrides packaged)')}` : '';
|
|
3211
|
+
console.log(chalk.bold(`\nWorkflow: ${detail.name}\n`));
|
|
3212
|
+
console.log(` Source: ${sourceLabel}${overridden}`);
|
|
3213
|
+
if (detail.description) {
|
|
3214
|
+
console.log(` Description: ${detail.description}`);
|
|
3215
|
+
}
|
|
3216
|
+
if (detail.output) {
|
|
3217
|
+
console.log(` Output: ${detail.output}`);
|
|
3218
|
+
}
|
|
3219
|
+
console.log(chalk.bold('\nInputs:'));
|
|
3220
|
+
const inputEntries = Object.entries(detail.inputs);
|
|
3221
|
+
if (inputEntries.length === 0) {
|
|
3222
|
+
console.log(chalk.gray(' (none)'));
|
|
3223
|
+
}
|
|
3224
|
+
else {
|
|
3225
|
+
for (const [key, input] of inputEntries) {
|
|
3226
|
+
console.log(` ${key}: ${formatWorkflowInput(input)}`);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
console.log(chalk.bold('\nNodes:'));
|
|
3230
|
+
for (const node of detail.nodes) {
|
|
3231
|
+
console.log(` ${node.id} (${node.kind})`);
|
|
3232
|
+
if (node.needs.length > 0) {
|
|
3233
|
+
console.log(` needs: ${node.needs.join(', ')}`);
|
|
3234
|
+
}
|
|
3235
|
+
if (node.agent)
|
|
3236
|
+
console.log(` agent: ${node.agent}`);
|
|
3237
|
+
if (node.agentsFrom)
|
|
3238
|
+
console.log(` agentsFrom: ${node.agentsFrom}`);
|
|
3239
|
+
if (node.action)
|
|
3240
|
+
console.log(` action: ${node.action}`);
|
|
3241
|
+
if (node.control)
|
|
3242
|
+
console.log(` control: ${node.control}`);
|
|
3243
|
+
if (node.if)
|
|
3244
|
+
console.log(` if: ${node.if}`);
|
|
3245
|
+
if (node.output)
|
|
3246
|
+
console.log(` output: ${node.output}`);
|
|
3247
|
+
if (node.writes)
|
|
3248
|
+
console.log(` writes: ${node.writes}`);
|
|
3249
|
+
if (node.input)
|
|
3250
|
+
console.log(` input: ${node.input.split('\n')[0]}`);
|
|
3251
|
+
if (node.with && Object.keys(node.with).length > 0) {
|
|
3252
|
+
console.log(` with: ${JSON.stringify(node.with)}`);
|
|
3253
|
+
}
|
|
3254
|
+
if (node.routes && Object.keys(node.routes).length > 0) {
|
|
3255
|
+
console.log(` routes: ${JSON.stringify(node.routes)}`);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
console.log('');
|
|
3259
|
+
return detail;
|
|
3260
|
+
}
|
|
3261
|
+
function validateSingleWorkflow(config, workflowName, workingDir) {
|
|
3262
|
+
const workflow = config.workflows?.[workflowName];
|
|
3263
|
+
if (!workflow) {
|
|
3264
|
+
return { name: workflowName, valid: false, error: `Unknown workflow "${workflowName}".` };
|
|
3265
|
+
}
|
|
3266
|
+
try {
|
|
3267
|
+
const sourceInfo = loadWorkflowSourceInfo(workingDir);
|
|
3268
|
+
const workflowNodes = getWorkflowNodes(workflowName, workflow);
|
|
3269
|
+
const executionOrder = getWorkflowExecutionOrder(workflowNodes);
|
|
3270
|
+
return {
|
|
3271
|
+
name: workflowName,
|
|
3272
|
+
valid: true,
|
|
3273
|
+
source: sourceInfo[workflowName]?.source ?? 'packaged',
|
|
3274
|
+
waves: getWorkflowExecutionWaves(workflowNodes, executionOrder),
|
|
3275
|
+
};
|
|
3276
|
+
}
|
|
3277
|
+
catch (error) {
|
|
3278
|
+
return {
|
|
3279
|
+
name: workflowName,
|
|
3280
|
+
valid: false,
|
|
3281
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
export function validateWorkflows(config, workflowName, options = {}) {
|
|
3286
|
+
const workingDir = options.workingDir ?? process.cwd();
|
|
3287
|
+
const names = workflowName ? [workflowName] : Object.keys(config.workflows ?? {}).sort();
|
|
3288
|
+
const results = names.map((name) => validateSingleWorkflow(config, name, workingDir));
|
|
3289
|
+
if (options.json) {
|
|
3290
|
+
console.log(JSON.stringify(results, null, 2));
|
|
3291
|
+
return results;
|
|
3292
|
+
}
|
|
3293
|
+
console.log(chalk.bold('\nWorkflow Validation:\n'));
|
|
3294
|
+
for (const result of results) {
|
|
3295
|
+
if (result.valid) {
|
|
3296
|
+
console.log(` ${chalk.green('✓')} ${result.name}`);
|
|
3297
|
+
if (result.waves) {
|
|
3298
|
+
console.log(` waves: ${result.waves.map((wave) => `[${wave.join(', ')}]`).join(' -> ')}`);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
else {
|
|
3302
|
+
console.log(` ${chalk.red('✗')} ${result.name}`);
|
|
3303
|
+
console.log(` ${chalk.red(result.error ?? 'invalid workflow')}`);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
console.log('');
|
|
3307
|
+
return results;
|
|
3308
|
+
}
|
|
1229
3309
|
//# sourceMappingURL=workflow.js.map
|