@diff-review-system/drs 3.3.1 → 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 +26 -0
- package/.pi/agents/task/changelog-updater.md +29 -0
- package/.pi/agents/task/review-issue-fixer.md +42 -0
- package/.pi/agents/visual/pr-explainer.md +205 -0
- package/.pi/workflows/github-pr-describe.yaml +26 -0
- package/.pi/workflows/github-pr-fix-review-issues-stacked.yaml +148 -0
- package/.pi/workflows/github-pr-post-comment.yaml +19 -0
- package/.pi/workflows/github-pr-review-post.yaml +43 -0
- package/.pi/workflows/github-pr-review.yaml +364 -0
- package/.pi/workflows/github-pr-show-changes.yaml +25 -0
- 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 +24 -0
- package/.pi/workflows/gitlab-mr-fix-review-issues-stacked.yaml +144 -0
- package/.pi/workflows/gitlab-mr-post-comment.yaml +17 -0
- package/.pi/workflows/gitlab-mr-review.yaml +364 -0
- package/.pi/workflows/gitlab-mr-show-changes.yaml +23 -0
- 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-changelog-update.yaml +23 -0
- package/.pi/workflows/local-fix-review-issues.yaml +111 -0
- package/.pi/workflows/local-review.yaml +24 -0
- package/.pi/workflows/local-update-agents-md.yaml +24 -0
- 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 +26 -0
- package/README.md +281 -104
- package/dist/ci/runner.d.ts.map +1 -1
- package/dist/ci/runner.js +9 -8
- package/dist/ci/runner.js.map +1 -1
- package/dist/cli/index.js +95 -325
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +25 -23
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/run-agent.d.ts +26 -0
- package/dist/cli/run-agent.d.ts.map +1 -0
- package/dist/cli/run-agent.js +143 -0
- package/dist/cli/run-agent.js.map +1 -0
- package/dist/cli/workflow.d.ts +105 -0
- package/dist/cli/workflow.d.ts.map +1 -0
- package/dist/cli/workflow.js +3309 -0
- package/dist/cli/workflow.js.map +1 -0
- 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/agent-id.d.ts +9 -0
- package/dist/lib/agent-id.d.ts.map +1 -0
- package/dist/lib/agent-id.js +32 -0
- package/dist/lib/agent-id.js.map +1 -0
- package/dist/lib/comment-formatter.d.ts +15 -1
- package/dist/lib/comment-formatter.d.ts.map +1 -1
- package/dist/lib/comment-formatter.js +53 -4
- package/dist/lib/comment-formatter.js.map +1 -1
- package/dist/lib/comment-manager.d.ts +4 -0
- package/dist/lib/comment-manager.d.ts.map +1 -1
- package/dist/lib/comment-manager.js +7 -1
- package/dist/lib/comment-manager.js.map +1 -1
- package/dist/lib/comment-poster.d.ts +2 -2
- package/dist/lib/comment-poster.d.ts.map +1 -1
- package/dist/lib/comment-poster.js +31 -4
- package/dist/lib/comment-poster.js.map +1 -1
- package/dist/lib/config.d.ts +160 -44
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +475 -101
- 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 +5 -4
- package/dist/lib/context-loader.d.ts.map +1 -1
- package/dist/lib/context-loader.js +79 -7
- 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/description-executor.js +1 -1
- package/dist/lib/description-executor.js.map +1 -1
- package/dist/lib/diff-lines.d.ts +18 -0
- package/dist/lib/diff-lines.d.ts.map +1 -0
- package/dist/lib/diff-lines.js +40 -0
- package/dist/lib/diff-lines.js.map +1 -0
- 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/logger.d.ts +1 -1
- package/dist/lib/logger.d.ts.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 +88 -173
- 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 +31 -21
- 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 +605 -16
- package/dist/pi/sdk.js.map +1 -1
- package/dist/runtime/agent-loader.d.ts +10 -6
- package/dist/runtime/agent-loader.d.ts.map +1 -1
- package/dist/runtime/agent-loader.js +55 -29
- package/dist/runtime/agent-loader.js.map +1 -1
- package/dist/runtime/built-in-paths.d.ts +1 -0
- package/dist/runtime/built-in-paths.d.ts.map +1 -1
- package/dist/runtime/built-in-paths.js +7 -0
- package/dist/runtime/built-in-paths.js.map +1 -1
- package/dist/runtime/client.d.ts +14 -0
- package/dist/runtime/client.d.ts.map +1 -1
- package/dist/runtime/client.js +87 -56
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/path-config.d.ts +2 -2
- package/dist/runtime/path-config.d.ts.map +1 -1
- package/dist/runtime/path-config.js +8 -8
- package/dist/runtime/path-config.js.map +1 -1
- package/package.json +22 -16
- package/.pi/agents/review/documentation.md +0 -56
- package/.pi/agents/review/performance.md +0 -53
- package/.pi/agents/review/quality.md +0 -59
- package/.pi/agents/review/security.md +0 -53
- package/.pi/agents/review/style.md +0 -132
- package/dist/cli/describe-mr.d.ts +0 -11
- package/dist/cli/describe-mr.d.ts.map +0 -1
- package/dist/cli/describe-mr.js +0 -134
- package/dist/cli/describe-mr.js.map +0 -1
- package/dist/cli/describe-pr.d.ts +0 -12
- package/dist/cli/describe-pr.d.ts.map +0 -1
- package/dist/cli/describe-pr.js +0 -135
- package/dist/cli/describe-pr.js.map +0 -1
- package/dist/cli/post-comments.d.ts +0 -20
- package/dist/cli/post-comments.d.ts.map +0 -1
- package/dist/cli/post-comments.js +0 -225
- package/dist/cli/post-comments.js.map +0 -1
- package/dist/cli/review-local.d.ts +0 -13
- package/dist/cli/review-local.d.ts.map +0 -1
- package/dist/cli/review-local.integration.test.d.ts +0 -2
- package/dist/cli/review-local.integration.test.d.ts.map +0 -1
- package/dist/cli/review-local.integration.test.js +0 -343
- package/dist/cli/review-local.integration.test.js.map +0 -1
- package/dist/cli/review-local.js +0 -90
- package/dist/cli/review-local.js.map +0 -1
- package/dist/cli/review-local.live.e2e.test.d.ts +0 -2
- package/dist/cli/review-local.live.e2e.test.d.ts.map +0 -1
- package/dist/cli/review-local.live.e2e.test.js +0 -153
- package/dist/cli/review-local.live.e2e.test.js.map +0 -1
- package/dist/cli/review-local.test.d.ts +0 -2
- package/dist/cli/review-local.test.d.ts.map +0 -1
- package/dist/cli/review-local.test.js +0 -164
- package/dist/cli/review-local.test.js.map +0 -1
- package/dist/cli/review-mr.d.ts +0 -22
- package/dist/cli/review-mr.d.ts.map +0 -1
- package/dist/cli/review-mr.js +0 -181
- package/dist/cli/review-mr.js.map +0 -1
- package/dist/cli/review-mr.test.d.ts +0 -2
- package/dist/cli/review-mr.test.d.ts.map +0 -1
- package/dist/cli/review-mr.test.js +0 -142
- package/dist/cli/review-mr.test.js.map +0 -1
- package/dist/cli/review-pr.d.ts +0 -22
- package/dist/cli/review-pr.d.ts.map +0 -1
- package/dist/cli/review-pr.js +0 -181
- package/dist/cli/review-pr.js.map +0 -1
- package/dist/cli/review-pr.test.d.ts +0 -2
- package/dist/cli/review-pr.test.d.ts.map +0 -1
- package/dist/cli/review-pr.test.js +0 -137
- package/dist/cli/review-pr.test.js.map +0 -1
- package/dist/cli/review-url.d.ts +0 -35
- package/dist/cli/review-url.d.ts.map +0 -1
- package/dist/cli/review-url.js +0 -110
- package/dist/cli/review-url.js.map +0 -1
- package/dist/cli/review-url.test.d.ts +0 -2
- package/dist/cli/review-url.test.d.ts.map +0 -1
- package/dist/cli/review-url.test.js +0 -132
- package/dist/cli/review-url.test.js.map +0 -1
- package/dist/cli/show-changes.d.ts +0 -15
- package/dist/cli/show-changes.d.ts.map +0 -1
- package/dist/cli/show-changes.js +0 -184
- package/dist/cli/show-changes.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 -694
- 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 -245
- package/dist/lib/comment-poster.test.js.map +0 -1
- package/dist/lib/config-model-overrides.test.d.ts +0 -12
- package/dist/lib/config-model-overrides.test.d.ts.map +0 -1
- package/dist/lib/config-model-overrides.test.js +0 -254
- 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 -73
- 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 -207
- 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 -128
- 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-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 -600
- 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 -531
- 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 -60
- package/dist/lib/unified-review-executor.d.ts.map +0 -1
- package/dist/lib/unified-review-executor.js +0 -207
- 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 -449
- 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 -280
- 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 -523
- 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
|
@@ -0,0 +1,3309 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import simpleGit from 'simple-git';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { getDescriberModelOverride, getReviewAgentIds, loadWorkflowSourceInfo, normalizeAgentConfig, resolveAgentRunConfig, } from '../lib/config.js';
|
|
6
|
+
import { resolveWithinWorkingDir } from '../lib/path-utils.js';
|
|
7
|
+
import { parseDiff, getChangedFiles, getFilesWithDiffs } from '../lib/diff-parser.js';
|
|
8
|
+
import { parseDiffLineInfo } from '../lib/diff-lines.js';
|
|
9
|
+
import { connectToRuntime, executeReview, filterIgnoredFiles, } from '../lib/review-orchestrator.js';
|
|
10
|
+
import { postReviewComments } from '../lib/comment-poster.js';
|
|
11
|
+
import { findExistingCommentById, createIssueFingerprint } from '../lib/comment-manager.js';
|
|
12
|
+
import { removeErrorComment } from '../lib/error-comment-poster.js';
|
|
13
|
+
import { runDescribeIfEnabled } from '../lib/description-executor.js';
|
|
14
|
+
import { buildBaseInstructions } from '../lib/review-core.js';
|
|
15
|
+
import { resolveCursorFixLinkOptions } from '../lib/cursor-fix-link.js';
|
|
16
|
+
import { extractHtmlDocument, parseArtifactOutputPointer, readArtifactOutputPointer, validateHtmlArtifact, } from '../lib/html-artifact.js';
|
|
17
|
+
import { getCanonicalDiffCommand, resolveBaseBranch } from '../lib/repository-validator.js';
|
|
18
|
+
import { formatCodeQualityReport, generateCodeQualityReport } from '../lib/code-quality-report.js';
|
|
19
|
+
import { createGitHubClient } from '../github/client.js';
|
|
20
|
+
import { GitHubPlatformAdapter } from '../github/platform-adapter.js';
|
|
21
|
+
import { createGitLabClient } from '../gitlab/client.js';
|
|
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';
|
|
25
|
+
import { runAgent } from './run-agent.js';
|
|
26
|
+
import { TraceCollector } from '../lib/trace-collector.js';
|
|
27
|
+
import { renderTraceHtml } from '../lib/trace-html.js';
|
|
28
|
+
function createWorkflowLock() {
|
|
29
|
+
return { current: Promise.resolve() };
|
|
30
|
+
}
|
|
31
|
+
async function withWorkflowLock(lock, run) {
|
|
32
|
+
const previousLock = lock.current;
|
|
33
|
+
let releaseLock;
|
|
34
|
+
lock.current = new Promise((resolve) => {
|
|
35
|
+
releaseLock = resolve;
|
|
36
|
+
});
|
|
37
|
+
await previousLock;
|
|
38
|
+
try {
|
|
39
|
+
return await run();
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
releaseLock();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function withWorkflowConsoleSuppressed(executionContext, suppress, run) {
|
|
46
|
+
if (!suppress) {
|
|
47
|
+
return run();
|
|
48
|
+
}
|
|
49
|
+
return withWorkflowLock(executionContext.locks.console, async () => {
|
|
50
|
+
const originalLog = console.log;
|
|
51
|
+
const originalWarn = console.warn;
|
|
52
|
+
console.log = () => undefined;
|
|
53
|
+
console.warn = () => undefined;
|
|
54
|
+
try {
|
|
55
|
+
return await run();
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
console.log = originalLog;
|
|
59
|
+
console.warn = originalWarn;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function getWorkflowGitClient(executionContext, workingDir) {
|
|
64
|
+
const existing = executionContext.gitClients.get(workingDir);
|
|
65
|
+
if (existing) {
|
|
66
|
+
return existing;
|
|
67
|
+
}
|
|
68
|
+
const git = simpleGit({ baseDir: workingDir });
|
|
69
|
+
executionContext.gitClients.set(workingDir, git);
|
|
70
|
+
return git;
|
|
71
|
+
}
|
|
72
|
+
function getWorkflowPlatformClient(executionContext, platform) {
|
|
73
|
+
const existing = executionContext.platformClients[platform];
|
|
74
|
+
if (existing) {
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
const client = platform === 'github'
|
|
78
|
+
? new GitHubPlatformAdapter(createGitHubClient())
|
|
79
|
+
: new GitLabPlatformAdapter(createGitLabClient());
|
|
80
|
+
executionContext.platformClients[platform] = client;
|
|
81
|
+
return client;
|
|
82
|
+
}
|
|
83
|
+
function getNodeNeeds(node) {
|
|
84
|
+
if (node.needs === undefined) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(node.needs)) {
|
|
88
|
+
throw new Error('Workflow node "needs" must be an array of node ids.');
|
|
89
|
+
}
|
|
90
|
+
return node.needs;
|
|
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
|
+
}
|
|
407
|
+
function getWorkflowExecutionOrder(nodes) {
|
|
408
|
+
const nodeIds = Object.keys(nodes);
|
|
409
|
+
const visiting = new Set();
|
|
410
|
+
const visited = new Set();
|
|
411
|
+
const order = [];
|
|
412
|
+
function visit(nodeId) {
|
|
413
|
+
if (visited.has(nodeId)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (visiting.has(nodeId)) {
|
|
417
|
+
throw new Error(`Workflow contains a dependency cycle at node "${nodeId}".`);
|
|
418
|
+
}
|
|
419
|
+
const node = nodes[nodeId];
|
|
420
|
+
if (!node) {
|
|
421
|
+
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
422
|
+
}
|
|
423
|
+
visiting.add(nodeId);
|
|
424
|
+
for (const dependency of getNodeNeeds(node)) {
|
|
425
|
+
if (!nodes[dependency]) {
|
|
426
|
+
throw new Error(`Workflow node "${nodeId}" needs unknown node "${dependency}".`);
|
|
427
|
+
}
|
|
428
|
+
visit(dependency);
|
|
429
|
+
}
|
|
430
|
+
visiting.delete(nodeId);
|
|
431
|
+
visited.add(nodeId);
|
|
432
|
+
order.push(nodeId);
|
|
433
|
+
}
|
|
434
|
+
for (const nodeId of nodeIds) {
|
|
435
|
+
visit(nodeId);
|
|
436
|
+
}
|
|
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;
|
|
444
|
+
}
|
|
445
|
+
function getWorkflowNodes(workflowName, workflow) {
|
|
446
|
+
const nodes = workflow.nodes;
|
|
447
|
+
if (typeof nodes !== 'object' ||
|
|
448
|
+
nodes === null ||
|
|
449
|
+
Array.isArray(nodes) ||
|
|
450
|
+
Object.keys(nodes).length === 0) {
|
|
451
|
+
throw new Error(`Workflow "${workflowName}" must define at least one node.`);
|
|
452
|
+
}
|
|
453
|
+
return nodes;
|
|
454
|
+
}
|
|
455
|
+
function getWorkflowExecutionWaves(nodes, executionOrder) {
|
|
456
|
+
const depthByNode = new Map();
|
|
457
|
+
const waves = [];
|
|
458
|
+
for (const nodeId of executionOrder) {
|
|
459
|
+
const node = nodes[nodeId];
|
|
460
|
+
if (!node) {
|
|
461
|
+
throw new Error(`Workflow references unknown node "${nodeId}".`);
|
|
462
|
+
}
|
|
463
|
+
const depth = getNodeNeeds(node).reduce((maxDepth, dependency) => {
|
|
464
|
+
return Math.max(maxDepth, (depthByNode.get(dependency) ?? 0) + 1);
|
|
465
|
+
}, 0);
|
|
466
|
+
depthByNode.set(nodeId, depth);
|
|
467
|
+
waves[depth] = waves[depth] ?? [];
|
|
468
|
+
waves[depth].push(nodeId);
|
|
469
|
+
}
|
|
470
|
+
return waves;
|
|
471
|
+
}
|
|
472
|
+
function getPathValue(root, path) {
|
|
473
|
+
return path.split('.').reduce((current, part) => {
|
|
474
|
+
if (current === undefined || current === null) {
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
if (typeof current !== 'object') {
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
return current[part];
|
|
481
|
+
}, root);
|
|
482
|
+
}
|
|
483
|
+
function stringifyTemplateValue(value) {
|
|
484
|
+
if (value === undefined) {
|
|
485
|
+
return '';
|
|
486
|
+
}
|
|
487
|
+
if (typeof value === 'string') {
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
return JSON.stringify(value, null, 2);
|
|
491
|
+
}
|
|
492
|
+
function renderTemplate(template, context) {
|
|
493
|
+
return template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, rawPath) => {
|
|
494
|
+
const path = rawPath.trim();
|
|
495
|
+
const value = getPathValue(context, path);
|
|
496
|
+
if (value === undefined) {
|
|
497
|
+
throw new Error(`Unknown workflow template value "{{${path}}}".`);
|
|
498
|
+
}
|
|
499
|
+
return stringifyTemplateValue(value);
|
|
500
|
+
});
|
|
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
|
+
}
|
|
522
|
+
async function resolveWorkflowInput(key, input, workingDir) {
|
|
523
|
+
if (typeof input === 'string') {
|
|
524
|
+
return input;
|
|
525
|
+
}
|
|
526
|
+
const hasValue = input.value !== undefined || input.default !== undefined;
|
|
527
|
+
const hasFile = input.file !== undefined;
|
|
528
|
+
if (hasValue && hasFile) {
|
|
529
|
+
throw new Error(`Workflow input "${key}" cannot define both value/default and file.`);
|
|
530
|
+
}
|
|
531
|
+
if (hasValue) {
|
|
532
|
+
return String(input.value ?? input.default ?? '');
|
|
533
|
+
}
|
|
534
|
+
if (hasFile) {
|
|
535
|
+
const inputPath = resolveWithinWorkingDir(workingDir, input.file ?? '', 'read');
|
|
536
|
+
return readFile(inputPath, 'utf-8');
|
|
537
|
+
}
|
|
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
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function resolveWorkflowInputs(workflow, options, workingDir) {
|
|
581
|
+
const values = {};
|
|
582
|
+
for (const [key, input] of Object.entries(workflow.inputs ?? {})) {
|
|
583
|
+
values[key] = await resolveWorkflowInput(key, input, workingDir);
|
|
584
|
+
}
|
|
585
|
+
for (const [key, value] of Object.entries(options.inputs ?? {})) {
|
|
586
|
+
values[key] = value;
|
|
587
|
+
}
|
|
588
|
+
for (const [key, filePath] of Object.entries(options.inputFiles ?? {})) {
|
|
589
|
+
const resolvedPath = resolveWithinWorkingDir(workingDir, filePath, 'read');
|
|
590
|
+
values[key] = await readFile(resolvedPath, 'utf-8');
|
|
591
|
+
}
|
|
592
|
+
for (const [key, input] of Object.entries(workflow.inputs ?? {})) {
|
|
593
|
+
validateResolvedWorkflowInput(key, input, values[key] ?? '');
|
|
594
|
+
}
|
|
595
|
+
return values;
|
|
596
|
+
}
|
|
597
|
+
function resolveAgentsFrom(config, agentsFrom) {
|
|
598
|
+
if (agentsFrom === 'review.agents') {
|
|
599
|
+
return normalizeAgentConfig(config.review.agents).map((agent) => agent.name);
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`Unsupported workflow agentsFrom "${agentsFrom}". ` + 'Currently supported: review.agents.');
|
|
602
|
+
}
|
|
603
|
+
function getNodeKind(node) {
|
|
604
|
+
const configuredKinds = [node.agent, node.agentsFrom, node.action, node.control].filter((value) => value !== undefined).length;
|
|
605
|
+
if (configuredKinds !== 1) {
|
|
606
|
+
throw new Error('Workflow node must define exactly one of agent, agentsFrom, action, or control.');
|
|
607
|
+
}
|
|
608
|
+
if (node.agent !== undefined)
|
|
609
|
+
return 'agent';
|
|
610
|
+
if (node.agentsFrom !== undefined)
|
|
611
|
+
return 'agents';
|
|
612
|
+
if (node.control !== undefined)
|
|
613
|
+
return 'control';
|
|
614
|
+
return 'action';
|
|
615
|
+
}
|
|
616
|
+
function hasConfiguredAgentPrompt(config, agentId) {
|
|
617
|
+
const runConfig = resolveAgentRunConfig(config, agentId);
|
|
618
|
+
return runConfig.prompt !== undefined || runConfig.promptFile !== undefined;
|
|
619
|
+
}
|
|
620
|
+
function createAgentOptions(prompt, options, workingDir) {
|
|
621
|
+
return {
|
|
622
|
+
prompt,
|
|
623
|
+
jsonOutput: false,
|
|
624
|
+
debug: options.debug,
|
|
625
|
+
thinkingLevel: options.thinkingLevel,
|
|
626
|
+
workingDir,
|
|
627
|
+
quiet: true,
|
|
628
|
+
allowImplicitStdin: false,
|
|
629
|
+
ignoreConfiguredOutput: true,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async function writeWorkflowFile(workingDir, relativeOutputPath, content) {
|
|
633
|
+
if (!relativeOutputPath.trim()) {
|
|
634
|
+
throw new Error('Workflow output path cannot be empty.');
|
|
635
|
+
}
|
|
636
|
+
const outputPath = resolveWithinWorkingDir(workingDir, relativeOutputPath, 'write');
|
|
637
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
638
|
+
await writeFile(outputPath, content, 'utf-8');
|
|
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
|
+
}
|
|
669
|
+
function renderNodeWritesPath(nodeId, node, context) {
|
|
670
|
+
if (!node.writes) {
|
|
671
|
+
return undefined;
|
|
672
|
+
}
|
|
673
|
+
const writes = renderTemplate(node.writes, context);
|
|
674
|
+
if (!writes.trim()) {
|
|
675
|
+
throw new Error(`Workflow node "${nodeId}" writes resolved to an empty path.`);
|
|
676
|
+
}
|
|
677
|
+
return writes;
|
|
678
|
+
}
|
|
679
|
+
async function runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
680
|
+
const agentId = node.agent;
|
|
681
|
+
if (!agentId) {
|
|
682
|
+
throw new Error(`Workflow node "${nodeId}" is missing agent.`);
|
|
683
|
+
}
|
|
684
|
+
const prompt = node.input === undefined ? undefined : renderTemplate(node.input, context);
|
|
685
|
+
if (prompt === undefined && !hasConfiguredAgentPrompt(config, agentId)) {
|
|
686
|
+
throw new Error(`Workflow agent node "${nodeId}" must define input or configure ` +
|
|
687
|
+
`agents.overrides.${agentId}.run.prompt/promptFile.`);
|
|
688
|
+
}
|
|
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);
|
|
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;
|
|
699
|
+
if (writes) {
|
|
700
|
+
await writeWorkflowFile(workingDir, writes, output);
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
id: nodeId,
|
|
704
|
+
type: 'agent',
|
|
705
|
+
agent: agentId,
|
|
706
|
+
response: result.response,
|
|
707
|
+
output,
|
|
708
|
+
writes,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
async function runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context) {
|
|
712
|
+
const agentsFrom = node.agentsFrom;
|
|
713
|
+
if (!agentsFrom) {
|
|
714
|
+
throw new Error(`Workflow node "${nodeId}" is missing agentsFrom.`);
|
|
715
|
+
}
|
|
716
|
+
const agentIds = resolveAgentsFrom(config, agentsFrom);
|
|
717
|
+
const prompt = node.input === undefined ? undefined : renderTemplate(node.input, context);
|
|
718
|
+
if (prompt === undefined) {
|
|
719
|
+
const missingPromptAgent = agentIds.find((agentId) => !hasConfiguredAgentPrompt(config, agentId));
|
|
720
|
+
if (missingPromptAgent) {
|
|
721
|
+
throw new Error(`Workflow agentsFrom node "${nodeId}" must define input or configure ` +
|
|
722
|
+
`agents.overrides.${missingPromptAgent}.run.prompt/promptFile.`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const responses = await Promise.all(agentIds.map((agentId) => runAgent(config, agentId, createAgentOptions(prompt, options, workingDir))));
|
|
726
|
+
const response = responses
|
|
727
|
+
.map((result) => `## ${result.agent}\n\n${result.response.trim()}`.trim())
|
|
728
|
+
.join('\n\n');
|
|
729
|
+
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
730
|
+
if (writes) {
|
|
731
|
+
await writeWorkflowFile(workingDir, writes, node.json === true ? JSON.stringify(responses, null, 2) : response);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
id: nodeId,
|
|
735
|
+
type: 'agents',
|
|
736
|
+
agents: agentIds,
|
|
737
|
+
response,
|
|
738
|
+
responses,
|
|
739
|
+
output: response,
|
|
740
|
+
writes,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
744
|
+
if (node.action === 'write') {
|
|
745
|
+
return runWriteWorkflowNode(nodeId, node, workingDir, context);
|
|
746
|
+
}
|
|
747
|
+
if (node.action === 'git-diff') {
|
|
748
|
+
return runGitDiffWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
749
|
+
}
|
|
750
|
+
if (node.action === 'git-add') {
|
|
751
|
+
return runGitAddWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
752
|
+
}
|
|
753
|
+
if (node.action === 'git-branch') {
|
|
754
|
+
return runGitBranchWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
755
|
+
}
|
|
756
|
+
if (node.action === 'git-commit') {
|
|
757
|
+
return runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
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
|
+
}
|
|
811
|
+
if (node.action === 'change-source') {
|
|
812
|
+
return runChangeSourceWorkflowNode(nodeId, node, workingDir, context, executionContext);
|
|
813
|
+
}
|
|
814
|
+
if (node.action === 'review') {
|
|
815
|
+
return runReviewWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
816
|
+
}
|
|
817
|
+
if (node.action === 'review-context') {
|
|
818
|
+
return runReviewContextWorkflowNode(config, nodeId, node, workingDir, context);
|
|
819
|
+
}
|
|
820
|
+
if (node.action === 'describe') {
|
|
821
|
+
return runDescribeWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
822
|
+
}
|
|
823
|
+
if (node.action === 'code-quality-report') {
|
|
824
|
+
return runCodeQualityReportWorkflowNode(nodeId, node, workingDir, context);
|
|
825
|
+
}
|
|
826
|
+
if (node.action === 'post-comment') {
|
|
827
|
+
return runPostCommentWorkflowNode(nodeId, node, options, workingDir, context, executionContext);
|
|
828
|
+
}
|
|
829
|
+
if (node.action === 'post-review-comments') {
|
|
830
|
+
return runPostReviewCommentsWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
831
|
+
}
|
|
832
|
+
if (node.action === 'post-fix-status') {
|
|
833
|
+
return runPostFixStatusWorkflowNode(nodeId, node, options, workingDir, context, executionContext);
|
|
834
|
+
}
|
|
835
|
+
throw new Error(`Unsupported workflow action "${node.action}" in node "${nodeId}".`);
|
|
836
|
+
}
|
|
837
|
+
async function runWriteWorkflowNode(nodeId, node, workingDir, context) {
|
|
838
|
+
if (!node.writes) {
|
|
839
|
+
throw new Error(`Workflow write node "${nodeId}" must define writes.`);
|
|
840
|
+
}
|
|
841
|
+
if (node.input === undefined) {
|
|
842
|
+
throw new Error(`Workflow write node "${nodeId}" must define input.`);
|
|
843
|
+
}
|
|
844
|
+
const content = renderTemplate(node.input, context);
|
|
845
|
+
const relativeOutputPath = renderNodeWritesPath(nodeId, node, context);
|
|
846
|
+
if (!relativeOutputPath) {
|
|
847
|
+
throw new Error(`Workflow write node "${nodeId}" must define writes.`);
|
|
848
|
+
}
|
|
849
|
+
await writeWorkflowFile(workingDir, relativeOutputPath, content);
|
|
850
|
+
return {
|
|
851
|
+
id: nodeId,
|
|
852
|
+
type: 'action',
|
|
853
|
+
action: node.action,
|
|
854
|
+
response: content,
|
|
855
|
+
output: content,
|
|
856
|
+
writes: relativeOutputPath,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function getBooleanActionOption(node, key, context) {
|
|
860
|
+
const value = node.with?.[key];
|
|
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;
|
|
866
|
+
}
|
|
867
|
+
function getStringActionOption(node, key, context) {
|
|
868
|
+
const value = node.with?.[key];
|
|
869
|
+
if (value === undefined) {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
if (typeof value === 'string') {
|
|
873
|
+
return renderTemplate(value, context);
|
|
874
|
+
}
|
|
875
|
+
return String(value);
|
|
876
|
+
}
|
|
877
|
+
function hasActionOption(node, key) {
|
|
878
|
+
return Object.prototype.hasOwnProperty.call(node.with ?? {}, key);
|
|
879
|
+
}
|
|
880
|
+
function requireStringActionOption(nodeId, node, key, context) {
|
|
881
|
+
const value = getStringActionOption(node, key, context)?.trim();
|
|
882
|
+
if (!value) {
|
|
883
|
+
throw new Error(`Workflow node "${nodeId}" must define with.${key}.`);
|
|
884
|
+
}
|
|
885
|
+
return value;
|
|
886
|
+
}
|
|
887
|
+
function requireNumberActionOption(nodeId, node, key, context) {
|
|
888
|
+
const value = requireStringActionOption(nodeId, node, key, context);
|
|
889
|
+
const parsed = Number.parseInt(value, 10);
|
|
890
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
891
|
+
throw new Error(`Workflow node "${nodeId}" with.${key} must be a positive number.`);
|
|
892
|
+
}
|
|
893
|
+
return parsed;
|
|
894
|
+
}
|
|
895
|
+
function getPathActionOption(nodeId, node, context, workingDir) {
|
|
896
|
+
const rawPaths = hasActionOption(node, 'paths')
|
|
897
|
+
? requireStringActionOption(nodeId, node, 'paths', context)
|
|
898
|
+
: requireStringActionOption(nodeId, node, 'path', context);
|
|
899
|
+
const paths = rawPaths
|
|
900
|
+
.split(/[\n,]/)
|
|
901
|
+
.map((path) => path.trim())
|
|
902
|
+
.filter(Boolean);
|
|
903
|
+
if (paths.length === 0) {
|
|
904
|
+
throw new Error(`Workflow node "${nodeId}" must define at least one path.`);
|
|
905
|
+
}
|
|
906
|
+
for (const path of paths) {
|
|
907
|
+
resolveWithinWorkingDir(workingDir, path, 'access');
|
|
908
|
+
}
|
|
909
|
+
return paths;
|
|
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
|
+
}
|
|
917
|
+
async function requireWorkflowGitRepo(nodeId, workingDir, executionContext) {
|
|
918
|
+
const git = getWorkflowGitClient(executionContext, workingDir);
|
|
919
|
+
const isRepo = await git.checkIsRepo();
|
|
920
|
+
if (!isRepo) {
|
|
921
|
+
throw new Error(`Workflow git node "${nodeId}" must run from a git repository.`);
|
|
922
|
+
}
|
|
923
|
+
return git;
|
|
924
|
+
}
|
|
925
|
+
async function runGitDiffWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
926
|
+
const git = getWorkflowGitClient(executionContext, workingDir);
|
|
927
|
+
const isRepo = await git.checkIsRepo();
|
|
928
|
+
if (!isRepo) {
|
|
929
|
+
throw new Error(`Workflow git-diff node "${nodeId}" must run from a git repository.`);
|
|
930
|
+
}
|
|
931
|
+
const staged = getBooleanActionOption(node, 'staged', context);
|
|
932
|
+
const diff = staged ? await git.diff(['--cached']) : await git.diff();
|
|
933
|
+
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
934
|
+
if (writes) {
|
|
935
|
+
await writeWorkflowFile(workingDir, writes, diff);
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
id: nodeId,
|
|
939
|
+
type: 'action',
|
|
940
|
+
action: node.action,
|
|
941
|
+
response: diff,
|
|
942
|
+
output: diff,
|
|
943
|
+
writes,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
async function runGitAddWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
947
|
+
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
948
|
+
const paths = getPathActionOption(nodeId, node, context, workingDir);
|
|
949
|
+
await git.add(paths);
|
|
950
|
+
return {
|
|
951
|
+
id: nodeId,
|
|
952
|
+
type: 'action',
|
|
953
|
+
action: node.action,
|
|
954
|
+
response: paths.join('\n'),
|
|
955
|
+
output: paths,
|
|
956
|
+
};
|
|
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
|
+
}
|
|
976
|
+
async function runGitCommitWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
977
|
+
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
978
|
+
const message = requireStringActionOption(nodeId, node, 'message', context);
|
|
979
|
+
const paths = hasActionOption(node, 'paths') || hasActionOption(node, 'path')
|
|
980
|
+
? getPathActionOption(nodeId, node, context, workingDir)
|
|
981
|
+
: undefined;
|
|
982
|
+
if (paths) {
|
|
983
|
+
await git.add(paths);
|
|
984
|
+
}
|
|
985
|
+
const commit = paths ? await git.commit(message, paths) : await git.commit(message);
|
|
986
|
+
const output = {
|
|
987
|
+
commit: commit.commit,
|
|
988
|
+
message,
|
|
989
|
+
paths,
|
|
990
|
+
summary: commit.summary,
|
|
991
|
+
};
|
|
992
|
+
return {
|
|
993
|
+
id: nodeId,
|
|
994
|
+
type: 'action',
|
|
995
|
+
action: node.action,
|
|
996
|
+
response: commit.commit ? `Created commit ${commit.commit}` : 'Created git commit',
|
|
997
|
+
output,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
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) {
|
|
1452
|
+
const git = simpleGit({ baseDir: workingDir });
|
|
1453
|
+
const isRepo = await git.checkIsRepo();
|
|
1454
|
+
if (!isRepo) {
|
|
1455
|
+
throw new Error(`Workflow change-source node "${nodeId}" must run from a git repository.`);
|
|
1456
|
+
}
|
|
1457
|
+
const staged = getBooleanActionOption(node, 'staged', context);
|
|
1458
|
+
const diffText = staged ? await git.diff(['--cached']) : await git.diff();
|
|
1459
|
+
const diffs = parseDiff(diffText);
|
|
1460
|
+
const changedFiles = getChangedFiles(diffs);
|
|
1461
|
+
return {
|
|
1462
|
+
name: `Local ${staged ? 'staged' : 'unstaged'} diff`,
|
|
1463
|
+
files: changedFiles,
|
|
1464
|
+
filesWithDiffs: getFilesWithDiffs(diffs),
|
|
1465
|
+
context: {},
|
|
1466
|
+
workingDir,
|
|
1467
|
+
staged,
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function parseGitRangeCommits(logOutput) {
|
|
1471
|
+
return logOutput
|
|
1472
|
+
.split('\n')
|
|
1473
|
+
.map((line) => line.trim())
|
|
1474
|
+
.filter(Boolean)
|
|
1475
|
+
.map((line) => {
|
|
1476
|
+
const [sha = '', author = '', date = '', subject = ''] = line.split('\x1f');
|
|
1477
|
+
return { sha, author, date, subject };
|
|
1478
|
+
})
|
|
1479
|
+
.filter((commit) => commit.sha.length > 0);
|
|
1480
|
+
}
|
|
1481
|
+
async function resolveGitRangeToRef(git) {
|
|
1482
|
+
if (process.env.GITHUB_REF_TYPE === 'tag' && process.env.GITHUB_REF_NAME) {
|
|
1483
|
+
return process.env.GITHUB_REF_NAME;
|
|
1484
|
+
}
|
|
1485
|
+
const tag = (await git.raw(['describe', '--tags', '--exact-match', 'HEAD'])).trim();
|
|
1486
|
+
if (!tag) {
|
|
1487
|
+
throw new Error('Workflow git-range change-source could not infer the current tag. ' +
|
|
1488
|
+
'Run from a tag checkout or provide with.to.');
|
|
1489
|
+
}
|
|
1490
|
+
return tag;
|
|
1491
|
+
}
|
|
1492
|
+
function isStableSemverTag(tag) {
|
|
1493
|
+
return /^v?\d+\.\d+\.\d+$/.test(tag);
|
|
1494
|
+
}
|
|
1495
|
+
async function resolvePreviousGitRangeTag(nodeId, node, git, toRef, context) {
|
|
1496
|
+
const includePrerelease = getBooleanActionOption(node, 'includePrereleaseFrom', context);
|
|
1497
|
+
const tagOutput = await git.raw(['tag', '--merged', toRef, '--sort=-v:refname']);
|
|
1498
|
+
const tags = tagOutput
|
|
1499
|
+
.split('\n')
|
|
1500
|
+
.map((tag) => tag.trim())
|
|
1501
|
+
.filter((tag) => tag.length > 0 && tag !== toRef);
|
|
1502
|
+
const previousTag = tags.find((tag) => includePrerelease || isStableSemverTag(tag)) ?? tags[0];
|
|
1503
|
+
if (!previousTag) {
|
|
1504
|
+
throw new Error(`Workflow node "${nodeId}" could not infer the previous tag for ${toRef}. ` +
|
|
1505
|
+
'Provide with.from explicitly.');
|
|
1506
|
+
}
|
|
1507
|
+
return previousTag;
|
|
1508
|
+
}
|
|
1509
|
+
async function resolveGitRangeRefs(nodeId, node, context, git) {
|
|
1510
|
+
const configuredToRef = getStringActionOption(node, 'to', context)?.trim();
|
|
1511
|
+
const toRef = configuredToRef ?? (await resolveGitRangeToRef(git));
|
|
1512
|
+
const configuredFromRef = getStringActionOption(node, 'from', context)?.trim();
|
|
1513
|
+
const fromRef = configuredFromRef ?? (await resolvePreviousGitRangeTag(nodeId, node, git, toRef, context));
|
|
1514
|
+
if (!fromRef) {
|
|
1515
|
+
throw new Error(`Workflow node "${nodeId}" could not infer the previous tag for ${toRef}. ` +
|
|
1516
|
+
'Provide with.from explicitly.');
|
|
1517
|
+
}
|
|
1518
|
+
return { fromRef, toRef };
|
|
1519
|
+
}
|
|
1520
|
+
async function loadGitRangeChangeSource(nodeId, node, workingDir, context, executionContext) {
|
|
1521
|
+
const git = await requireWorkflowGitRepo(nodeId, workingDir, executionContext);
|
|
1522
|
+
const { fromRef, toRef } = await resolveGitRangeRefs(nodeId, node, context, git);
|
|
1523
|
+
const range = `${fromRef}..${toRef}`;
|
|
1524
|
+
const diffText = await git.diff([range]);
|
|
1525
|
+
const logOutput = await git.raw(['log', '--format=%H%x1f%an%x1f%aI%x1f%s', '--no-merges', range]);
|
|
1526
|
+
const diffs = parseDiff(diffText);
|
|
1527
|
+
const changedFiles = getChangedFiles(diffs);
|
|
1528
|
+
return {
|
|
1529
|
+
name: `Git range ${range}`,
|
|
1530
|
+
files: changedFiles,
|
|
1531
|
+
filesWithDiffs: getFilesWithDiffs(diffs),
|
|
1532
|
+
context: {
|
|
1533
|
+
sourceType: 'git-range',
|
|
1534
|
+
fromRef,
|
|
1535
|
+
toRef,
|
|
1536
|
+
range,
|
|
1537
|
+
commits: parseGitRangeCommits(logOutput),
|
|
1538
|
+
},
|
|
1539
|
+
workingDir,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
function createPlatformChangeSource(platform, name, projectId, pullRequest, changedFiles, workingDir) {
|
|
1543
|
+
return {
|
|
1544
|
+
name,
|
|
1545
|
+
files: changedFiles.map((file) => file.filename),
|
|
1546
|
+
filesWithDiffs: changedFiles
|
|
1547
|
+
.filter((file) => file.patch && file.patch.length > 0)
|
|
1548
|
+
.map((file) => ({ filename: file.filename, patch: file.patch ?? '' })),
|
|
1549
|
+
context: {
|
|
1550
|
+
platform,
|
|
1551
|
+
projectId,
|
|
1552
|
+
pullRequest,
|
|
1553
|
+
changedFiles,
|
|
1554
|
+
},
|
|
1555
|
+
workingDir,
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
async function loadGitHubChangeSource(nodeId, node, workingDir, context, executionContext) {
|
|
1559
|
+
const owner = requireStringActionOption(nodeId, node, 'owner', context);
|
|
1560
|
+
const repo = requireStringActionOption(nodeId, node, 'repo', context);
|
|
1561
|
+
const prNumber = requireNumberActionOption(nodeId, node, 'pr', context);
|
|
1562
|
+
const projectId = `${owner}/${repo}`;
|
|
1563
|
+
const platformClient = getWorkflowPlatformClient(executionContext, 'github');
|
|
1564
|
+
const [pullRequest, changedFiles] = await Promise.all([
|
|
1565
|
+
platformClient.getPullRequest(projectId, prNumber),
|
|
1566
|
+
platformClient.getChangedFiles(projectId, prNumber),
|
|
1567
|
+
]);
|
|
1568
|
+
return createPlatformChangeSource('github', `GitHub PR ${projectId}#${prNumber}`, projectId, pullRequest, changedFiles, workingDir);
|
|
1569
|
+
}
|
|
1570
|
+
async function loadGitLabChangeSource(nodeId, node, workingDir, context, executionContext) {
|
|
1571
|
+
const projectId = hasActionOption(node, 'project')
|
|
1572
|
+
? requireStringActionOption(nodeId, node, 'project', context)
|
|
1573
|
+
: requireStringActionOption(nodeId, node, 'projectId', context);
|
|
1574
|
+
const mrIid = hasActionOption(node, 'mr')
|
|
1575
|
+
? requireNumberActionOption(nodeId, node, 'mr', context)
|
|
1576
|
+
: requireNumberActionOption(nodeId, node, 'mrIid', context);
|
|
1577
|
+
const platformClient = getWorkflowPlatformClient(executionContext, 'gitlab');
|
|
1578
|
+
const [pullRequest, changedFiles] = await Promise.all([
|
|
1579
|
+
platformClient.getPullRequest(projectId, mrIid),
|
|
1580
|
+
platformClient.getChangedFiles(projectId, mrIid),
|
|
1581
|
+
]);
|
|
1582
|
+
return createPlatformChangeSource('gitlab', `GitLab MR ${projectId}!${mrIid}`, projectId, pullRequest, changedFiles, workingDir);
|
|
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
|
+
}
|
|
1641
|
+
async function runChangeSourceWorkflowNode(nodeId, node, workingDir, context, executionContext) {
|
|
1642
|
+
const type = getStringActionOption(node, 'type', context) ?? 'local';
|
|
1643
|
+
let source;
|
|
1644
|
+
if (type === 'local') {
|
|
1645
|
+
source = await loadLocalChangeSource(nodeId, node, workingDir, context);
|
|
1646
|
+
}
|
|
1647
|
+
else if (type === 'git-range') {
|
|
1648
|
+
source = await loadGitRangeChangeSource(nodeId, node, workingDir, context, executionContext);
|
|
1649
|
+
}
|
|
1650
|
+
else if (type === 'github-pr') {
|
|
1651
|
+
source = await loadGitHubChangeSource(nodeId, node, workingDir, context, executionContext);
|
|
1652
|
+
}
|
|
1653
|
+
else if (type === 'gitlab-mr') {
|
|
1654
|
+
source = await loadGitLabChangeSource(nodeId, node, workingDir, context, executionContext);
|
|
1655
|
+
}
|
|
1656
|
+
else if (type === 'fix-verification') {
|
|
1657
|
+
source = await loadFixVerificationChangeSource(nodeId, node, workingDir, context);
|
|
1658
|
+
}
|
|
1659
|
+
else {
|
|
1660
|
+
throw new Error(`Unsupported workflow change-source type "${type}" in node "${nodeId}". ` +
|
|
1661
|
+
'Currently supported: local, git-range, github-pr, gitlab-mr, fix-verification.');
|
|
1662
|
+
}
|
|
1663
|
+
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
1664
|
+
if (writes) {
|
|
1665
|
+
await writeWorkflowFile(workingDir, writes, JSON.stringify(source, null, 2));
|
|
1666
|
+
}
|
|
1667
|
+
return {
|
|
1668
|
+
id: nodeId,
|
|
1669
|
+
type: 'action',
|
|
1670
|
+
action: node.action,
|
|
1671
|
+
response: source.name,
|
|
1672
|
+
output: source,
|
|
1673
|
+
writes,
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
function isWorkflowPlatform(value) {
|
|
1677
|
+
return value === 'github' || value === 'gitlab';
|
|
1678
|
+
}
|
|
1679
|
+
function readSourcePostTarget(source) {
|
|
1680
|
+
if (!source) {
|
|
1681
|
+
return {};
|
|
1682
|
+
}
|
|
1683
|
+
const platform = typeof source.context.platform === 'string' ? source.context.platform : undefined;
|
|
1684
|
+
const projectId = typeof source.context.projectId === 'string' ? source.context.projectId : undefined;
|
|
1685
|
+
const pullRequest = isPullRequest(source.context.pullRequest)
|
|
1686
|
+
? source.context.pullRequest
|
|
1687
|
+
: undefined;
|
|
1688
|
+
const changedFiles = Array.isArray(source.context.changedFiles)
|
|
1689
|
+
? source.context.changedFiles.filter(isFileChange)
|
|
1690
|
+
: undefined;
|
|
1691
|
+
return {
|
|
1692
|
+
platform: isWorkflowPlatform(platform) ? platform : undefined,
|
|
1693
|
+
projectId,
|
|
1694
|
+
prNumber: pullRequest?.number,
|
|
1695
|
+
pullRequest,
|
|
1696
|
+
changedFiles,
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function resolvePostProjectId(nodeId, node, context, sourceTarget) {
|
|
1700
|
+
if (hasActionOption(node, 'owner') || hasActionOption(node, 'repo')) {
|
|
1701
|
+
const owner = requireStringActionOption(nodeId, node, 'owner', context);
|
|
1702
|
+
const repo = requireStringActionOption(nodeId, node, 'repo', context);
|
|
1703
|
+
return `${owner}/${repo}`;
|
|
1704
|
+
}
|
|
1705
|
+
const projectId = getStringActionOption(node, 'project', context) ??
|
|
1706
|
+
getStringActionOption(node, 'projectId', context) ??
|
|
1707
|
+
sourceTarget.projectId;
|
|
1708
|
+
if (!projectId) {
|
|
1709
|
+
throw new Error(`Workflow post node "${nodeId}" must define a project target.`);
|
|
1710
|
+
}
|
|
1711
|
+
return projectId;
|
|
1712
|
+
}
|
|
1713
|
+
function resolvePostPrNumber(nodeId, node, context, sourceTarget) {
|
|
1714
|
+
if (hasActionOption(node, 'pr')) {
|
|
1715
|
+
return requireNumberActionOption(nodeId, node, 'pr', context);
|
|
1716
|
+
}
|
|
1717
|
+
if (hasActionOption(node, 'mr')) {
|
|
1718
|
+
return requireNumberActionOption(nodeId, node, 'mr', context);
|
|
1719
|
+
}
|
|
1720
|
+
if (hasActionOption(node, 'prNumber')) {
|
|
1721
|
+
return requireNumberActionOption(nodeId, node, 'prNumber', context);
|
|
1722
|
+
}
|
|
1723
|
+
if (hasActionOption(node, 'mrIid')) {
|
|
1724
|
+
return requireNumberActionOption(nodeId, node, 'mrIid', context);
|
|
1725
|
+
}
|
|
1726
|
+
if (sourceTarget.prNumber) {
|
|
1727
|
+
return sourceTarget.prNumber;
|
|
1728
|
+
}
|
|
1729
|
+
throw new Error(`Workflow post node "${nodeId}" must define a PR/MR number.`);
|
|
1730
|
+
}
|
|
1731
|
+
function resolvePostTarget(nodeId, node, context, executionContext, source) {
|
|
1732
|
+
const sourceTarget = readSourcePostTarget(source);
|
|
1733
|
+
const explicitPlatform = getStringActionOption(node, 'platform', context);
|
|
1734
|
+
const platform = explicitPlatform ?? sourceTarget.platform;
|
|
1735
|
+
if (!isWorkflowPlatform(platform)) {
|
|
1736
|
+
throw new Error(`Workflow post node "${nodeId}" must resolve with.platform to github or gitlab.`);
|
|
1737
|
+
}
|
|
1738
|
+
const projectId = resolvePostProjectId(nodeId, node, context, sourceTarget);
|
|
1739
|
+
const prNumber = resolvePostPrNumber(nodeId, node, context, sourceTarget);
|
|
1740
|
+
return {
|
|
1741
|
+
platform,
|
|
1742
|
+
platformClient: getWorkflowPlatformClient(executionContext, platform),
|
|
1743
|
+
projectId,
|
|
1744
|
+
prNumber,
|
|
1745
|
+
pullRequest: sourceTarget.pullRequest,
|
|
1746
|
+
changedFiles: sourceTarget.changedFiles,
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
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;
|
|
1827
|
+
}
|
|
1828
|
+
const candidate = value;
|
|
1829
|
+
return (typeof candidate.number === 'number' &&
|
|
1830
|
+
typeof candidate.title === 'string' &&
|
|
1831
|
+
typeof candidate.author === 'string' &&
|
|
1832
|
+
typeof candidate.sourceBranch === 'string' &&
|
|
1833
|
+
typeof candidate.targetBranch === 'string' &&
|
|
1834
|
+
typeof candidate.headSha === 'string');
|
|
1835
|
+
}
|
|
1836
|
+
function isFileChange(value) {
|
|
1837
|
+
if (!value || typeof value !== 'object') {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
const candidate = value;
|
|
1841
|
+
return typeof candidate.filename === 'string' && typeof candidate.status === 'string';
|
|
1842
|
+
}
|
|
1843
|
+
function createWorkflowLineValidator(platform, source) {
|
|
1844
|
+
const pullRequest = isPullRequest(source.context.pullRequest) ? source.context.pullRequest : null;
|
|
1845
|
+
const platformData = pullRequest?.platformData;
|
|
1846
|
+
const diffRefs = platformData?.diff_refs;
|
|
1847
|
+
if (platform === 'gitlab' && (!diffRefs?.base_sha || !diffRefs.head_sha || !diffRefs.start_sha)) {
|
|
1848
|
+
return undefined;
|
|
1849
|
+
}
|
|
1850
|
+
const fileChanges = Array.isArray(source.context.changedFiles)
|
|
1851
|
+
? source.context.changedFiles.filter(isFileChange)
|
|
1852
|
+
: [];
|
|
1853
|
+
const patchSources = fileChanges.length > 0 ? fileChanges : (source.filesWithDiffs ?? []);
|
|
1854
|
+
const validLinesMap = new Map();
|
|
1855
|
+
const changedLinesMap = new Map();
|
|
1856
|
+
for (const file of patchSources) {
|
|
1857
|
+
if ('status' in file && file.status === 'removed') {
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
const patch = file.patch;
|
|
1861
|
+
if (patch) {
|
|
1862
|
+
const lineInfo = parseDiffLineInfo(patch);
|
|
1863
|
+
validLinesMap.set(file.filename, lineInfo.commentableLines);
|
|
1864
|
+
changedLinesMap.set(file.filename, lineInfo.addedLines);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return {
|
|
1868
|
+
isValidLine(file, line) {
|
|
1869
|
+
return validLinesMap.get(file)?.has(line) ?? false;
|
|
1870
|
+
},
|
|
1871
|
+
isChangedLine(file, line) {
|
|
1872
|
+
return changedLinesMap.get(file)?.has(line) ?? false;
|
|
1873
|
+
},
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
function createWorkflowInlinePosition(platform, source) {
|
|
1877
|
+
const pullRequest = isPullRequest(source.context.pullRequest) ? source.context.pullRequest : null;
|
|
1878
|
+
if (!pullRequest) {
|
|
1879
|
+
return undefined;
|
|
1880
|
+
}
|
|
1881
|
+
if (platform === 'github') {
|
|
1882
|
+
return (issue) => ({
|
|
1883
|
+
path: issue.file,
|
|
1884
|
+
line: issue.line,
|
|
1885
|
+
commitSha: pullRequest.headSha,
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
return (issue, platformData) => {
|
|
1889
|
+
const data = platformData;
|
|
1890
|
+
const refs = data?.diff_refs;
|
|
1891
|
+
return {
|
|
1892
|
+
path: issue.file,
|
|
1893
|
+
line: issue.line,
|
|
1894
|
+
baseSha: refs?.base_sha,
|
|
1895
|
+
headSha: refs?.head_sha,
|
|
1896
|
+
startSha: refs?.start_sha,
|
|
1897
|
+
};
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function isReviewResult(value) {
|
|
1901
|
+
if (!value || typeof value !== 'object') {
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
const candidate = value;
|
|
1905
|
+
return Array.isArray(candidate.issues) && typeof candidate.summary === 'object';
|
|
1906
|
+
}
|
|
1907
|
+
function getReviewSourceDiffCommand(source, baseBranch) {
|
|
1908
|
+
const pullRequest = isPullRequest(source.context.pullRequest) ? source.context.pullRequest : null;
|
|
1909
|
+
if (pullRequest) {
|
|
1910
|
+
return getCanonicalDiffCommand(pullRequest, resolveBaseBranch(baseBranch, pullRequest.targetBranch));
|
|
1911
|
+
}
|
|
1912
|
+
return source.staged ? 'git diff --cached -- <file>' : 'git diff -- <file>';
|
|
1913
|
+
}
|
|
1914
|
+
function getReviewContextFiles(config, source, fileFilter) {
|
|
1915
|
+
const filteredFiles = filterIgnoredFiles(source.files, config);
|
|
1916
|
+
const patchByFile = new Map((source.filesWithDiffs ?? []).map((file) => [file.filename, file.patch]));
|
|
1917
|
+
const files = filteredFiles.map((filename) => ({
|
|
1918
|
+
filename,
|
|
1919
|
+
patch: patchByFile.get(filename),
|
|
1920
|
+
}));
|
|
1921
|
+
if (!fileFilter) {
|
|
1922
|
+
return files;
|
|
1923
|
+
}
|
|
1924
|
+
const matches = files.filter((file) => file.filename === fileFilter);
|
|
1925
|
+
if (matches.length === 0) {
|
|
1926
|
+
throw new Error(`No matching file "${fileFilter}" found in review source.`);
|
|
1927
|
+
}
|
|
1928
|
+
return matches;
|
|
1929
|
+
}
|
|
1930
|
+
async function runReviewContextWorkflowNode(config, nodeId, node, workingDir, context) {
|
|
1931
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
1932
|
+
const source = context.artifacts[sourceArtifact];
|
|
1933
|
+
if (!isReviewSource(source)) {
|
|
1934
|
+
throw new Error(`Workflow review-context node "${nodeId}" needs a ReviewSource artifact. ` +
|
|
1935
|
+
'Set with.source to a change-source output.');
|
|
1936
|
+
}
|
|
1937
|
+
const rawFileFilter = getStringActionOption(node, 'file', context)?.trim();
|
|
1938
|
+
const rawBaseBranch = getStringActionOption(node, 'baseBranch', context)?.trim();
|
|
1939
|
+
const fileFilter = rawFileFilter === '' ? undefined : rawFileFilter;
|
|
1940
|
+
const baseBranch = rawBaseBranch === '' ? undefined : rawBaseBranch;
|
|
1941
|
+
const files = getReviewContextFiles(config, source, fileFilter);
|
|
1942
|
+
const instructions = buildBaseInstructions(source.name, files, getReviewSourceDiffCommand(source, baseBranch));
|
|
1943
|
+
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
1944
|
+
if (writes) {
|
|
1945
|
+
await writeWorkflowFile(workingDir, writes, instructions);
|
|
1946
|
+
}
|
|
1947
|
+
return {
|
|
1948
|
+
id: nodeId,
|
|
1949
|
+
type: 'action',
|
|
1950
|
+
action: node.action,
|
|
1951
|
+
response: instructions,
|
|
1952
|
+
output: instructions,
|
|
1953
|
+
writes,
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
function getDescribeFiles(source, target) {
|
|
1957
|
+
if (source.filesWithDiffs && source.filesWithDiffs.length > 0) {
|
|
1958
|
+
return source.filesWithDiffs;
|
|
1959
|
+
}
|
|
1960
|
+
return (target.changedFiles ?? [])
|
|
1961
|
+
.filter((file) => file.status !== 'removed')
|
|
1962
|
+
.map((file) => ({ filename: file.filename, patch: file.patch }));
|
|
1963
|
+
}
|
|
1964
|
+
async function runCodeQualityReportWorkflowNode(nodeId, node, workingDir, context) {
|
|
1965
|
+
const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
|
|
1966
|
+
const reviewResult = context.artifacts[reviewArtifact];
|
|
1967
|
+
if (!isReviewResult(reviewResult)) {
|
|
1968
|
+
throw new Error(`Workflow code-quality-report node "${nodeId}" needs a ReviewResult artifact.`);
|
|
1969
|
+
}
|
|
1970
|
+
const reportPath = hasActionOption(node, 'path')
|
|
1971
|
+
? requireStringActionOption(nodeId, node, 'path', context)
|
|
1972
|
+
: renderNodeWritesPath(nodeId, node, context);
|
|
1973
|
+
if (!reportPath) {
|
|
1974
|
+
throw new Error(`Workflow code-quality-report node "${nodeId}" must define with.path or writes.`);
|
|
1975
|
+
}
|
|
1976
|
+
const report = generateCodeQualityReport(reviewResult.issues);
|
|
1977
|
+
await writeWorkflowFile(workingDir, reportPath, formatCodeQualityReport(report));
|
|
1978
|
+
return {
|
|
1979
|
+
id: nodeId,
|
|
1980
|
+
type: 'action',
|
|
1981
|
+
action: node.action,
|
|
1982
|
+
response: `wrote GitLab code quality report to ${reportPath}`,
|
|
1983
|
+
output: {
|
|
1984
|
+
path: reportPath,
|
|
1985
|
+
issues: report.length,
|
|
1986
|
+
},
|
|
1987
|
+
writes: reportPath,
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
async function runDescribeWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
1991
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
1992
|
+
const source = context.artifacts[sourceArtifact];
|
|
1993
|
+
if (!isReviewSource(source)) {
|
|
1994
|
+
throw new Error(`Workflow describe node "${nodeId}" needs a ReviewSource artifact. ` +
|
|
1995
|
+
'Set with.source to a change-source output.');
|
|
1996
|
+
}
|
|
1997
|
+
const target = resolvePostTarget(nodeId, node, context, executionContext, source);
|
|
1998
|
+
if (!target.pullRequest) {
|
|
1999
|
+
throw new Error(`Workflow describe node "${nodeId}" needs a platform change-source target.`);
|
|
2000
|
+
}
|
|
2001
|
+
const shouldPostDescription = getBooleanActionOption(node, 'post', context) ||
|
|
2002
|
+
getBooleanActionOption(node, 'postDescription', context);
|
|
2003
|
+
const runtimeClient = await connectToRuntime(config, source.workingDir ?? workingDir, {
|
|
2004
|
+
debug: options.debug,
|
|
2005
|
+
modelOverrides: getDescriberModelOverride(config),
|
|
2006
|
+
thinkingLevel: options.thinkingLevel,
|
|
2007
|
+
});
|
|
2008
|
+
let description = null;
|
|
2009
|
+
try {
|
|
2010
|
+
description = await runDescribeIfEnabled(runtimeClient, config, target.platformClient, target.projectId, target.pullRequest, getDescribeFiles(source, target), shouldPostDescription, source.workingDir ?? workingDir, options.debug);
|
|
2011
|
+
}
|
|
2012
|
+
finally {
|
|
2013
|
+
await runtimeClient.shutdown();
|
|
2014
|
+
}
|
|
2015
|
+
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
2016
|
+
if (writes) {
|
|
2017
|
+
await writeWorkflowFile(workingDir, writes, JSON.stringify(description, null, 2));
|
|
2018
|
+
}
|
|
2019
|
+
return {
|
|
2020
|
+
id: nodeId,
|
|
2021
|
+
type: 'action',
|
|
2022
|
+
action: node.action,
|
|
2023
|
+
response: description ? JSON.stringify(description, null, 2) : 'description generation skipped',
|
|
2024
|
+
output: description,
|
|
2025
|
+
writes,
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
function formatMarkedComment(body, marker) {
|
|
2029
|
+
if (!marker) {
|
|
2030
|
+
return body;
|
|
2031
|
+
}
|
|
2032
|
+
const markerComment = `<!-- drs-comment-id: ${marker} -->`;
|
|
2033
|
+
return body.includes(markerComment) ? body : `${markerComment}\n${body}`;
|
|
2034
|
+
}
|
|
2035
|
+
async function runPostCommentWorkflowNode(nodeId, node, options, workingDir, context, executionContext) {
|
|
2036
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
2037
|
+
const source = isReviewSource(context.artifacts[sourceArtifact])
|
|
2038
|
+
? context.artifacts[sourceArtifact]
|
|
2039
|
+
: undefined;
|
|
2040
|
+
const target = resolvePostTarget(nodeId, node, context, executionContext, source);
|
|
2041
|
+
const rawBody = node.input === undefined
|
|
2042
|
+
? requireStringActionOption(nodeId, node, 'body', context)
|
|
2043
|
+
: renderTemplate(node.input, context);
|
|
2044
|
+
const marker = getStringActionOption(node, 'marker', context)?.trim();
|
|
2045
|
+
const body = formatMarkedComment(rawBody, marker);
|
|
2046
|
+
let operation = 'created';
|
|
2047
|
+
if (marker) {
|
|
2048
|
+
const comments = await target.platformClient.getComments(target.projectId, target.prNumber);
|
|
2049
|
+
const existingComment = findExistingCommentById(comments, marker);
|
|
2050
|
+
if (existingComment) {
|
|
2051
|
+
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, () => target.platformClient.updateComment(target.projectId, target.prNumber, existingComment.id, body));
|
|
2052
|
+
operation = 'updated';
|
|
2053
|
+
}
|
|
2054
|
+
else {
|
|
2055
|
+
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, () => target.platformClient.createComment(target.projectId, target.prNumber, body));
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, () => target.platformClient.createComment(target.projectId, target.prNumber, body));
|
|
2060
|
+
}
|
|
2061
|
+
return {
|
|
2062
|
+
id: nodeId,
|
|
2063
|
+
type: 'action',
|
|
2064
|
+
action: node.action,
|
|
2065
|
+
response: `${operation} comment on ${target.platform} ${target.projectId}#${target.prNumber}`,
|
|
2066
|
+
output: {
|
|
2067
|
+
platform: target.platform,
|
|
2068
|
+
projectId: target.projectId,
|
|
2069
|
+
prNumber: target.prNumber,
|
|
2070
|
+
marker,
|
|
2071
|
+
operation,
|
|
2072
|
+
},
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
async function runPostReviewCommentsWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
2076
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
2077
|
+
const reviewArtifact = getStringActionOption(node, 'review', context) ?? 'review';
|
|
2078
|
+
const source = context.artifacts[sourceArtifact];
|
|
2079
|
+
const reviewResult = context.artifacts[reviewArtifact];
|
|
2080
|
+
if (!isReviewSource(source)) {
|
|
2081
|
+
throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewSource artifact.`);
|
|
2082
|
+
}
|
|
2083
|
+
if (!isReviewResult(reviewResult)) {
|
|
2084
|
+
throw new Error(`Workflow post-review-comments node "${nodeId}" needs a ReviewResult artifact.`);
|
|
2085
|
+
}
|
|
2086
|
+
const target = resolvePostTarget(nodeId, node, context, executionContext, source);
|
|
2087
|
+
const pullRequest = target.pullRequest;
|
|
2088
|
+
const platformData = pullRequest?.platformData;
|
|
2089
|
+
const lineValidator = createWorkflowLineValidator(target.platform, source);
|
|
2090
|
+
const createInlinePosition = lineValidator
|
|
2091
|
+
? createWorkflowInlinePosition(target.platform, source)
|
|
2092
|
+
: undefined;
|
|
2093
|
+
const shouldRemoveErrorComment = !hasActionOption(node, 'removeErrorComment') ||
|
|
2094
|
+
getBooleanActionOption(node, 'removeErrorComment', context);
|
|
2095
|
+
await withWorkflowConsoleSuppressed(executionContext, options.jsonOutput === true, async () => {
|
|
2096
|
+
if (shouldRemoveErrorComment) {
|
|
2097
|
+
await removeErrorComment(target.platformClient, target.projectId, target.prNumber);
|
|
2098
|
+
}
|
|
2099
|
+
const cursorFixLinks = resolveCursorFixLinkOptions(config, target.projectId, workingDir);
|
|
2100
|
+
await postReviewComments(target.platformClient, target.projectId, target.prNumber, reviewResult.summary, reviewResult.issues, reviewResult.changeSummary, reviewResult.usage, platformData, lineValidator, createInlinePosition, cursorFixLinks, pullRequest
|
|
2101
|
+
? {
|
|
2102
|
+
headSha: pullRequest.headSha,
|
|
2103
|
+
sourceBranch: pullRequest.sourceBranch,
|
|
2104
|
+
targetBranch: pullRequest.targetBranch,
|
|
2105
|
+
}
|
|
2106
|
+
: undefined);
|
|
2107
|
+
});
|
|
2108
|
+
return {
|
|
2109
|
+
id: nodeId,
|
|
2110
|
+
type: 'action',
|
|
2111
|
+
action: node.action,
|
|
2112
|
+
response: `posted review comments on ${target.platform} ${target.projectId}#${target.prNumber}`,
|
|
2113
|
+
output: {
|
|
2114
|
+
platform: target.platform,
|
|
2115
|
+
projectId: target.projectId,
|
|
2116
|
+
prNumber: target.prNumber,
|
|
2117
|
+
issues: reviewResult.issues.length,
|
|
2118
|
+
},
|
|
2119
|
+
};
|
|
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
|
+
}
|
|
2371
|
+
function isReviewSource(value) {
|
|
2372
|
+
if (!value || typeof value !== 'object') {
|
|
2373
|
+
return false;
|
|
2374
|
+
}
|
|
2375
|
+
const candidate = value;
|
|
2376
|
+
return (typeof candidate.name === 'string' &&
|
|
2377
|
+
Array.isArray(candidate.files) &&
|
|
2378
|
+
typeof candidate.context === 'object' &&
|
|
2379
|
+
candidate.context !== null);
|
|
2380
|
+
}
|
|
2381
|
+
async function runReviewWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
2382
|
+
const sourceArtifact = getStringActionOption(node, 'source', context) ?? 'change';
|
|
2383
|
+
const source = context.artifacts[sourceArtifact];
|
|
2384
|
+
if (!isReviewSource(source)) {
|
|
2385
|
+
throw new Error(`Workflow review node "${nodeId}" needs a ReviewSource artifact. ` +
|
|
2386
|
+
'Set with.source to a change-source output.');
|
|
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
|
+
}
|
|
2427
|
+
const reviewResult = await withWorkflowLock(executionContext.locks.exit, async () => {
|
|
2428
|
+
const originalLog = console.log;
|
|
2429
|
+
const originalWarn = console.warn;
|
|
2430
|
+
if (options.jsonOutput) {
|
|
2431
|
+
console.log = () => undefined;
|
|
2432
|
+
console.warn = () => undefined;
|
|
2433
|
+
}
|
|
2434
|
+
try {
|
|
2435
|
+
return await executeReview(config, {
|
|
2436
|
+
...sourceForReview,
|
|
2437
|
+
workingDir: sourceForReview.workingDir ?? workingDir,
|
|
2438
|
+
debug: options.debug,
|
|
2439
|
+
thinkingLevel: options.thinkingLevel,
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
finally {
|
|
2443
|
+
if (options.jsonOutput) {
|
|
2444
|
+
console.log = originalLog;
|
|
2445
|
+
console.warn = originalWarn;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
});
|
|
2449
|
+
const writes = renderNodeWritesPath(nodeId, node, context);
|
|
2450
|
+
if (writes) {
|
|
2451
|
+
await writeWorkflowFile(workingDir, writes, JSON.stringify(reviewResult, null, 2));
|
|
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
|
+
}
|
|
2467
|
+
return {
|
|
2468
|
+
id: nodeId,
|
|
2469
|
+
type: 'action',
|
|
2470
|
+
action: node.action,
|
|
2471
|
+
response: `${JSON.stringify(reviewResult.summary, null, 2)}${artifactResponse}`,
|
|
2472
|
+
output: reviewResult,
|
|
2473
|
+
outputs,
|
|
2474
|
+
writes,
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
function recordNodeArtifact(nodeId, node, result, artifacts) {
|
|
2478
|
+
const artifactValue = result.output ?? result.response ?? result.responses;
|
|
2479
|
+
artifacts[nodeId] = artifactValue;
|
|
2480
|
+
if (node.output) {
|
|
2481
|
+
artifacts[node.output] = artifactValue;
|
|
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
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
function formatWorkflowJson(result) {
|
|
2983
|
+
return JSON.stringify(result, null, 2);
|
|
2984
|
+
}
|
|
2985
|
+
async function runSingleWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext) {
|
|
2986
|
+
const kind = getNodeKind(node);
|
|
2987
|
+
if (getWorkflowNodeSkipReason(node, context)) {
|
|
2988
|
+
return createSkippedWorkflowNodeResult(nodeId);
|
|
2989
|
+
}
|
|
2990
|
+
if (kind === 'agent') {
|
|
2991
|
+
return runAgentWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
2992
|
+
}
|
|
2993
|
+
if (kind === 'agents') {
|
|
2994
|
+
return runAgentsWorkflowNode(config, nodeId, node, options, workingDir, context);
|
|
2995
|
+
}
|
|
2996
|
+
if (kind === 'control') {
|
|
2997
|
+
throw new Error(`Workflow control node "${nodeId}" cannot run in the static DAG executor.`);
|
|
2998
|
+
}
|
|
2999
|
+
return runActionWorkflowNode(config, nodeId, node, options, workingDir, context, executionContext);
|
|
3000
|
+
}
|
|
3001
|
+
export async function runWorkflow(config, workflowName, options = {}) {
|
|
3002
|
+
const workflow = config.workflows?.[workflowName];
|
|
3003
|
+
if (!workflow) {
|
|
3004
|
+
throw new Error(`Unknown workflow "${workflowName}".`);
|
|
3005
|
+
}
|
|
3006
|
+
const workflowNodes = getWorkflowNodes(workflowName, workflow);
|
|
3007
|
+
const workingDir = options.workingDir ?? process.cwd();
|
|
3008
|
+
const inputs = await resolveWorkflowInputs(workflow, options, workingDir);
|
|
3009
|
+
const nodes = {};
|
|
3010
|
+
const artifacts = {};
|
|
3011
|
+
const loop = {};
|
|
3012
|
+
const context = { inputs, nodes, artifacts, loop };
|
|
3013
|
+
const executionContext = {
|
|
3014
|
+
gitClients: new Map(),
|
|
3015
|
+
platformClients: {},
|
|
3016
|
+
traceCollector: options.trace ? new TraceCollector() : undefined,
|
|
3017
|
+
locks: {
|
|
3018
|
+
exit: createWorkflowLock(),
|
|
3019
|
+
console: createWorkflowLock(),
|
|
3020
|
+
},
|
|
3021
|
+
};
|
|
3022
|
+
const executionOrder = getWorkflowExecutionOrder(workflowNodes);
|
|
3023
|
+
const executionWaves = getWorkflowExecutionWaves(workflowNodes, executionOrder);
|
|
3024
|
+
if (!options.jsonOutput) {
|
|
3025
|
+
console.log(chalk.gray(`Running workflow ${workflowName}...\n`));
|
|
3026
|
+
}
|
|
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);
|
|
3064
|
+
}
|
|
3065
|
+
catch (flushError) {
|
|
3066
|
+
console.error(chalk.yellow('Warning:'), 'Failed to persist workflow trace on failure:', flushError instanceof Error ? flushError.message : String(flushError));
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
throw error;
|
|
3070
|
+
}
|
|
3071
|
+
const lastNodeId = executionOrder[executionOrder.length - 1];
|
|
3072
|
+
const lastNode = workflowNodes[lastNodeId];
|
|
3073
|
+
const outputKey = workflow.output ?? lastNode.output ?? lastNodeId;
|
|
3074
|
+
const result = {
|
|
3075
|
+
timestamp: new Date().toISOString(),
|
|
3076
|
+
workflow: workflowName,
|
|
3077
|
+
inputs,
|
|
3078
|
+
nodes,
|
|
3079
|
+
artifacts,
|
|
3080
|
+
loop,
|
|
3081
|
+
output: artifacts[outputKey],
|
|
3082
|
+
};
|
|
3083
|
+
if (options.outputPath) {
|
|
3084
|
+
await writeWorkflowFile(workingDir, options.outputPath, formatWorkflowJson(result));
|
|
3085
|
+
if (!options.jsonOutput) {
|
|
3086
|
+
console.log(chalk.green(`\n✓ Workflow output saved to ${options.outputPath}`));
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
if (executionContext.traceCollector && executionContext.traceCollector.getTraces().length > 0) {
|
|
3090
|
+
await flushWorkflowTrace(executionContext.traceCollector, workflowName, inputs, result.timestamp, workingDir, options);
|
|
3091
|
+
}
|
|
3092
|
+
if (options.jsonOutput) {
|
|
3093
|
+
console.log(formatWorkflowJson(result));
|
|
3094
|
+
}
|
|
3095
|
+
else if (typeof result.output === 'string' && result.output.trim()) {
|
|
3096
|
+
console.log(`\n${result.output}`);
|
|
3097
|
+
}
|
|
3098
|
+
return result;
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* List available workflows and their source origin.
|
|
3102
|
+
*
|
|
3103
|
+
* Packaged workflows are always returned. Project-defined workflows
|
|
3104
|
+
* appear as 'project' and mark any packaged workflow they replace
|
|
3105
|
+
* as overridden.
|
|
3106
|
+
*/
|
|
3107
|
+
export function listWorkflows(config, options = {}) {
|
|
3108
|
+
const workingDir = options.workingDir ?? process.cwd();
|
|
3109
|
+
const sourceInfo = loadWorkflowSourceInfo(workingDir);
|
|
3110
|
+
const workflows = config.workflows ?? {};
|
|
3111
|
+
const entries = Object.entries(workflows)
|
|
3112
|
+
.map(([name, workflow]) => {
|
|
3113
|
+
const info = sourceInfo[name] ?? {
|
|
3114
|
+
source: 'packaged',
|
|
3115
|
+
overridesPackaged: false,
|
|
3116
|
+
};
|
|
3117
|
+
return {
|
|
3118
|
+
name,
|
|
3119
|
+
source: info.source,
|
|
3120
|
+
overridden: info.overridesPackaged,
|
|
3121
|
+
description: workflow.description,
|
|
3122
|
+
};
|
|
3123
|
+
})
|
|
3124
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
3125
|
+
if (options.json) {
|
|
3126
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
3127
|
+
}
|
|
3128
|
+
else {
|
|
3129
|
+
const sourceLabel = (source, overridden) => {
|
|
3130
|
+
const label = source === 'packaged' ? chalk.gray('packaged') : chalk.cyan('project');
|
|
3131
|
+
return overridden ? `${label} ${chalk.yellow('(overrides packaged)')}` : label;
|
|
3132
|
+
};
|
|
3133
|
+
console.log(chalk.bold('\n📋 Available Workflows:\n'));
|
|
3134
|
+
for (const entry of entries) {
|
|
3135
|
+
console.log(` ${chalk.white(entry.name)}`);
|
|
3136
|
+
console.log(` Source: ${sourceLabel(entry.source, entry.overridden)}`);
|
|
3137
|
+
if (entry.description) {
|
|
3138
|
+
console.log(` ${chalk.gray(entry.description)}`);
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
console.log('');
|
|
3142
|
+
}
|
|
3143
|
+
return entries;
|
|
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
|
+
}
|
|
3309
|
+
//# sourceMappingURL=workflow.js.map
|