agentweaver 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/dist/artifacts.js +24 -0
- package/dist/executors/configs/fetch-gitlab-diff-config.js +3 -0
- package/dist/executors/configs/opencode-config.js +6 -0
- package/dist/executors/fetch-gitlab-diff-executor.js +26 -0
- package/dist/executors/jira-fetch-executor.js +8 -2
- package/dist/executors/opencode-executor.js +35 -0
- package/dist/gitlab.js +199 -5
- package/dist/index.js +155 -116
- package/dist/interactive-ui.js +21 -2
- package/dist/jira.js +116 -14
- package/dist/pipeline/auto-flow.js +1 -1
- package/dist/pipeline/declarative-flows.js +41 -6
- package/dist/pipeline/flow-catalog.js +66 -0
- package/dist/pipeline/flow-specs/auto.json +183 -1
- package/dist/pipeline/flow-specs/gitlab-diff-review.json +226 -0
- package/dist/pipeline/flow-specs/gitlab-review.json +1 -31
- package/dist/pipeline/flow-specs/plan-opencode.json +603 -0
- package/dist/pipeline/flow-specs/plan.json +183 -1
- package/dist/pipeline/node-registry.js +80 -8
- package/dist/pipeline/nodes/fetch-gitlab-diff-node.js +34 -0
- package/dist/pipeline/nodes/flow-run-node.js +2 -2
- package/dist/pipeline/nodes/jira-fetch-node.js +26 -2
- package/dist/pipeline/nodes/opencode-prompt-node.js +32 -0
- package/dist/pipeline/nodes/planning-questions-form-node.js +69 -0
- package/dist/pipeline/nodes/user-input-node.js +9 -1
- package/dist/pipeline/prompt-registry.js +3 -1
- package/dist/pipeline/registry.js +10 -0
- package/dist/pipeline/spec-loader.js +37 -3
- package/dist/pipeline/spec-types.js +43 -1
- package/dist/pipeline/spec-validator.js +53 -7
- package/dist/pipeline/value-resolver.js +15 -1
- package/dist/prompts.js +47 -12
- package/dist/scope.js +24 -32
- package/dist/structured-artifact-schemas.json +154 -1
- package/dist/structured-artifacts.js +2 -0
- package/dist/user-input.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,10 +12,11 @@ The package is designed to run as an npm CLI and includes an interactive termina
|
|
|
12
12
|
|
|
13
13
|
- Fetches a Jira issue by key or browse URL
|
|
14
14
|
- Fetches GitLab merge request review comments into reusable markdown and JSON artifacts
|
|
15
|
+
- Fetches GitLab merge request diffs into reusable markdown and JSON artifacts and can run Claude-based diff review directly from MR
|
|
15
16
|
- Generates workflow artifacts such as design, implementation plan, QA plan, bug analysis, reviews, and summaries
|
|
16
17
|
- Machine-readable JSON artifacts are stored under `.agentweaver/scopes/<scope-key>/.artifacts/` and act as the source of truth between workflow steps; Markdown artifacts remain for human inspection
|
|
17
18
|
- Workflow artifacts are isolated by scope; for Jira-driven flows the scope key defaults to the Jira task key, otherwise it defaults to `<git-branch>--<worktree-hash>`
|
|
18
|
-
- Runs workflow stages like `bug-analyze`, `bug-fix`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
|
|
19
|
+
- Runs workflow stages like `bug-analyze`, `bug-fix`, `gitlab-diff-review`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
|
|
19
20
|
- Persists compact `auto` pipeline state on disk so runs can resume without storing large agent outputs
|
|
20
21
|
- Uses Docker runtime services for isolated Codex execution and build verification
|
|
21
22
|
|
|
@@ -26,7 +27,8 @@ The CLI now uses an executor + node + declarative flow architecture.
|
|
|
26
27
|
- `src/index.ts` remains the CLI entrypoint and high-level orchestration layer
|
|
27
28
|
- `src/executors/` contains first-class executors for external actions such as Jira fetch, GitLab review fetch, local Codex, Docker-based build verification, Claude, Claude summaries, and process execution
|
|
28
29
|
- `src/pipeline/nodes/` contains reusable runtime nodes built on top of executors
|
|
29
|
-
- `src/pipeline/flow-specs/` contains declarative JSON flow specs for `preflight`, `bug-analyze`, `bug-fix`, `gitlab-review`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
|
|
30
|
+
- `src/pipeline/flow-specs/` contains declarative JSON flow specs for `preflight`, `bug-analyze`, `bug-fix`, `gitlab-diff-review`, `gitlab-review`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-go-tests-loop`, `run-go-linter-loop`, and `auto`
|
|
31
|
+
- project-local flow may additionally be placed in `.agentweaver/.flows/*.json`; they are discovered at runtime from the current workspace
|
|
30
32
|
- `src/runtime/` contains shared runtime services such as command resolution, Docker runtime environment setup, and subprocess execution
|
|
31
33
|
|
|
32
34
|
This keeps command handlers focused on choosing a flow and providing parameters instead of assembling prompts and subprocess wiring inline.
|
|
@@ -36,6 +38,7 @@ This keeps command handlers focused on choosing a flow and providing parameters
|
|
|
36
38
|
- `src/` — main TypeScript sources
|
|
37
39
|
- `src/index.ts` — CLI entrypoint and workflow orchestration
|
|
38
40
|
- `src/pipeline/flow-specs/` — declarative JSON specs for workflow stages
|
|
41
|
+
- `.agentweaver/.flows/` — optional project-local declarative flow specs loaded from the current repository
|
|
39
42
|
- `src/pipeline/nodes/` — reusable pipeline nodes executed by the declarative runner
|
|
40
43
|
- `src/interactive-ui.ts` — interactive TUI built with `neo-blessed`
|
|
41
44
|
- `src/markdown.ts` — markdown-to-terminal renderer for the TUI
|
|
@@ -89,7 +92,7 @@ Required:
|
|
|
89
92
|
Common optional variables:
|
|
90
93
|
|
|
91
94
|
- `JIRA_BASE_URL` — required when you pass only an issue key like `DEMO-123`
|
|
92
|
-
- `GITLAB_TOKEN` — personal access token for `gitlab-review`
|
|
95
|
+
- `GITLAB_TOKEN` — personal access token for `gitlab-review` and `gitlab-diff-review`
|
|
93
96
|
- `AGENTWEAVER_HOME` — path to the AgentWeaver installation directory
|
|
94
97
|
- `DOCKER_COMPOSE_BIN` — override compose command, for example `docker compose`
|
|
95
98
|
- `CODEX_BIN` — override `codex` executable path
|
|
@@ -122,7 +125,8 @@ agentweaver plan DEMO-3288
|
|
|
122
125
|
agentweaver plan
|
|
123
126
|
agentweaver bug-analyze DEMO-3288
|
|
124
127
|
agentweaver bug-fix DEMO-3288
|
|
125
|
-
agentweaver gitlab-review
|
|
128
|
+
agentweaver gitlab-diff-review
|
|
129
|
+
agentweaver gitlab-review
|
|
126
130
|
agentweaver mr-description DEMO-3288
|
|
127
131
|
agentweaver task-describe DEMO-3288
|
|
128
132
|
agentweaver implement DEMO-3288
|
|
@@ -142,7 +146,8 @@ node dist/index.js plan DEMO-3288
|
|
|
142
146
|
node dist/index.js plan
|
|
143
147
|
node dist/index.js bug-analyze DEMO-3288
|
|
144
148
|
node dist/index.js bug-fix DEMO-3288
|
|
145
|
-
node dist/index.js gitlab-review
|
|
149
|
+
node dist/index.js gitlab-diff-review
|
|
150
|
+
node dist/index.js gitlab-review
|
|
146
151
|
node dist/index.js mr-description DEMO-3288
|
|
147
152
|
node dist/index.js task-describe DEMO-3288
|
|
148
153
|
node dist/index.js review
|
|
@@ -175,7 +180,8 @@ Notes:
|
|
|
175
180
|
|
|
176
181
|
- `--verbose` streams child process `stdout/stderr` in direct CLI mode
|
|
177
182
|
- task-only commands such as `plan` and `auto` ask for Jira task via interactive `user-input` when it is omitted
|
|
178
|
-
- scope-flexible commands such as `review`, `review-fix`, `run-go-tests-loop`, and `run-go-linter-loop` use the current git branch by default when Jira task is omitted
|
|
183
|
+
- scope-flexible commands such as `gitlab-diff-review`, `gitlab-review`, `review`, `review-fix`, `run-go-tests-loop`, and `run-go-linter-loop` use the current git branch by default when Jira task is omitted
|
|
184
|
+
- `gitlab-review` and `gitlab-diff-review` ask for GitLab merge request URL via interactive `user-input`
|
|
179
185
|
- `--scope <name>` lets you override the default project scope name
|
|
180
186
|
- the interactive `Activity` pane is intentionally structured: it shows launch separators, prompts, summaries, and short status messages instead of raw Codex/Claude logs by default
|
|
181
187
|
|
|
@@ -197,6 +203,14 @@ Current navigation:
|
|
|
197
203
|
- `h` — help overlay
|
|
198
204
|
- `q` or `Ctrl+C` — exit
|
|
199
205
|
|
|
206
|
+
Flow discovery and highlighting:
|
|
207
|
+
|
|
208
|
+
- built-in flow are loaded from the packaged `src/pipeline/flow-specs/`
|
|
209
|
+
- project-local flow are loaded from `.agentweaver/.flows/*.json`
|
|
210
|
+
- project-local flow are shown in a different color in the `Flows` pane
|
|
211
|
+
- when a project-local flow is selected, the description pane also shows its source file path
|
|
212
|
+
- if a local flow conflicts with a built-in flow id or uses unknown node / executor / prompt / schema types, interactive startup fails fast with a validation error
|
|
213
|
+
|
|
200
214
|
Activity pane behavior:
|
|
201
215
|
|
|
202
216
|
- each external launch is separated with a framed block that shows the current `node`, `executor`, and `model` when available
|
package/dist/artifacts.js
CHANGED
|
@@ -59,6 +59,12 @@ export function planFile(taskKey) {
|
|
|
59
59
|
export function planJsonFile(taskKey) {
|
|
60
60
|
return artifactJsonFile("plan", taskKey, 1);
|
|
61
61
|
}
|
|
62
|
+
export function planningQuestionsJsonFile(taskKey) {
|
|
63
|
+
return taskArtifactsFile(taskKey, `planning-questions-${taskKey}.json`);
|
|
64
|
+
}
|
|
65
|
+
export function planningAnswersJsonFile(taskKey) {
|
|
66
|
+
return taskArtifactsFile(taskKey, `planning-answers-${taskKey}.json`);
|
|
67
|
+
}
|
|
62
68
|
export function bugAnalyzeFile(taskKey) {
|
|
63
69
|
return taskWorkspaceFile(taskKey, `bug-analyze-${taskKey}.md`);
|
|
64
70
|
}
|
|
@@ -95,6 +101,15 @@ export function readyToMergeFile(taskKey) {
|
|
|
95
101
|
export function jiraTaskFile(taskKey) {
|
|
96
102
|
return taskArtifactsFile(taskKey, `${taskKey}.json`);
|
|
97
103
|
}
|
|
104
|
+
export function jiraAttachmentsDir(taskKey) {
|
|
105
|
+
return path.join(taskArtifactsDir(taskKey), "jira-attachments");
|
|
106
|
+
}
|
|
107
|
+
export function jiraAttachmentsManifestFile(taskKey) {
|
|
108
|
+
return taskArtifactsFile(taskKey, `jira-attachments-${taskKey}.json`);
|
|
109
|
+
}
|
|
110
|
+
export function jiraAttachmentsContextFile(taskKey) {
|
|
111
|
+
return taskWorkspaceFile(taskKey, `jira-attachments-context-${taskKey}.txt`);
|
|
112
|
+
}
|
|
98
113
|
export function jiraDescriptionFile(taskKey) {
|
|
99
114
|
return taskWorkspaceFile(taskKey, `jira-${taskKey}-description.md`);
|
|
100
115
|
}
|
|
@@ -116,6 +131,15 @@ export function gitlabReviewJsonFile(taskKey) {
|
|
|
116
131
|
export function gitlabReviewInputJsonFile(taskKey) {
|
|
117
132
|
return taskArtifactsFile(taskKey, `gitlab-review-input-${taskKey}.json`);
|
|
118
133
|
}
|
|
134
|
+
export function gitlabDiffFile(taskKey) {
|
|
135
|
+
return taskWorkspaceFile(taskKey, `gitlab-diff-${taskKey}.md`);
|
|
136
|
+
}
|
|
137
|
+
export function gitlabDiffJsonFile(taskKey) {
|
|
138
|
+
return taskArtifactsFile(taskKey, `gitlab-diff-${taskKey}.json`);
|
|
139
|
+
}
|
|
140
|
+
export function gitlabDiffReviewInputJsonFile(taskKey) {
|
|
141
|
+
return taskArtifactsFile(taskKey, `gitlab-diff-review-input-${taskKey}.json`);
|
|
142
|
+
}
|
|
119
143
|
export function autoStateFile(taskKey) {
|
|
120
144
|
return taskArtifactsFile(taskKey, `.agentweaver-state-${taskKey}.json`);
|
|
121
145
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { fetchGitLabDiffExecutorDefaultConfig } from "./configs/fetch-gitlab-diff-config.js";
|
|
2
|
+
import { buildGitLabMergeRequestDiffFetchTarget, fetchGitLabMergeRequestDiff } from "../gitlab.js";
|
|
3
|
+
export const fetchGitLabDiffExecutor = {
|
|
4
|
+
kind: "fetch-gitlab-diff",
|
|
5
|
+
version: 1,
|
|
6
|
+
defaultConfig: fetchGitLabDiffExecutorDefaultConfig,
|
|
7
|
+
async execute(context, input) {
|
|
8
|
+
const target = buildGitLabMergeRequestDiffFetchTarget(input.mergeRequestUrl);
|
|
9
|
+
if (context.verbose) {
|
|
10
|
+
context.ui.writeStdout(`GitLab MR URL: ${target.mergeRequestUrl}\n`);
|
|
11
|
+
context.ui.writeStdout(`GitLab project path: ${target.projectPath}\n`);
|
|
12
|
+
context.ui.writeStdout(`GitLab merge request IID: ${target.mergeRequestIid}\n`);
|
|
13
|
+
context.ui.writeStdout(`GitLab merge request API URL: ${target.mergeRequestApiUrl}\n`);
|
|
14
|
+
context.ui.writeStdout(`GitLab diffs API URL: ${target.diffsApiUrl}\n`);
|
|
15
|
+
context.ui.writeStdout(`Saving GitLab diff markdown to: ${input.outputFile}\n`);
|
|
16
|
+
context.ui.writeStdout(`Saving GitLab diff JSON to: ${input.outputJsonFile}\n`);
|
|
17
|
+
}
|
|
18
|
+
const artifact = await fetchGitLabMergeRequestDiff(input.mergeRequestUrl, input.outputFile, input.outputJsonFile);
|
|
19
|
+
return {
|
|
20
|
+
outputFile: input.outputFile,
|
|
21
|
+
outputJsonFile: input.outputJsonFile,
|
|
22
|
+
mergeRequestUrl: artifact.merge_request_url,
|
|
23
|
+
filesCount: artifact.files.length,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -5,7 +5,13 @@ export const jiraFetchExecutor = {
|
|
|
5
5
|
version: 1,
|
|
6
6
|
defaultConfig: jiraFetchExecutorDefaultConfig,
|
|
7
7
|
async execute(_context, input) {
|
|
8
|
-
await fetchJiraIssue(input.jiraApiUrl, input.outputFile);
|
|
9
|
-
return {
|
|
8
|
+
const artifacts = await fetchJiraIssue(input.jiraApiUrl, input.outputFile, input.attachmentsManifestFile, input.attachmentsContextFile);
|
|
9
|
+
return {
|
|
10
|
+
outputFile: artifacts.issueFile,
|
|
11
|
+
downloadedAttachments: artifacts.downloadedAttachments,
|
|
12
|
+
planningContextAttachments: artifacts.planningContextAttachments,
|
|
13
|
+
...(artifacts.attachmentsManifestFile ? { attachmentsManifestFile: artifacts.attachmentsManifestFile } : {}),
|
|
14
|
+
...(artifacts.attachmentsContextFile ? { attachmentsContextFile: artifacts.attachmentsContextFile } : {}),
|
|
15
|
+
};
|
|
10
16
|
},
|
|
11
17
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { opencodeExecutorDefaultConfig } from "./configs/opencode-config.js";
|
|
2
|
+
import { processExecutor } from "./process-executor.js";
|
|
3
|
+
function resolveModel(config, inputModel, env) {
|
|
4
|
+
const explicitModel = inputModel?.trim();
|
|
5
|
+
if (explicitModel) {
|
|
6
|
+
return explicitModel;
|
|
7
|
+
}
|
|
8
|
+
const envModel = env[config.modelEnvVar]?.trim();
|
|
9
|
+
return envModel || undefined;
|
|
10
|
+
}
|
|
11
|
+
export const opencodeExecutor = {
|
|
12
|
+
kind: "opencode",
|
|
13
|
+
version: 1,
|
|
14
|
+
defaultConfig: opencodeExecutorDefaultConfig,
|
|
15
|
+
async execute(context, input, config) {
|
|
16
|
+
const env = input.env ?? context.env;
|
|
17
|
+
const command = input.command ?? context.runtime.resolveCmd(config.defaultCommand, config.commandEnvVar);
|
|
18
|
+
const model = resolveModel(config, input.model, env);
|
|
19
|
+
const argv = [command, config.subcommand];
|
|
20
|
+
if (model) {
|
|
21
|
+
argv.push("--model", model);
|
|
22
|
+
}
|
|
23
|
+
argv.push(input.prompt);
|
|
24
|
+
const result = await processExecutor.execute(context, {
|
|
25
|
+
argv,
|
|
26
|
+
env,
|
|
27
|
+
label: model ? `opencode:${model}` : "opencode",
|
|
28
|
+
}, processExecutor.defaultConfig);
|
|
29
|
+
return {
|
|
30
|
+
output: result.output,
|
|
31
|
+
command,
|
|
32
|
+
...(model ? { model } : {}),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
package/dist/gitlab.js
CHANGED
|
@@ -37,8 +37,17 @@ export function buildGitLabReviewFetchTarget(mergeRequestUrl) {
|
|
|
37
37
|
discussionsApiUrl: `${mergeRequestRef.apiBaseUrl}/projects/${encodeURIComponent(mergeRequestRef.projectPath)}/merge_requests/${mergeRequestRef.mergeRequestIid}/discussions`,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
const
|
|
40
|
+
export function buildGitLabMergeRequestDiffFetchTarget(mergeRequestUrl) {
|
|
41
|
+
const mergeRequestRef = parseGitLabMergeRequestUrl(mergeRequestUrl);
|
|
42
|
+
const mergeRequestApiUrl = `${mergeRequestRef.apiBaseUrl}/projects/${encodeURIComponent(mergeRequestRef.projectPath)}` +
|
|
43
|
+
`/merge_requests/${mergeRequestRef.mergeRequestIid}`;
|
|
44
|
+
return {
|
|
45
|
+
...mergeRequestRef,
|
|
46
|
+
mergeRequestApiUrl,
|
|
47
|
+
diffsApiUrl: `${mergeRequestApiUrl}/diffs`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function fetchGitLabJson(apiUrl, token) {
|
|
42
51
|
const response = await fetch(apiUrl, {
|
|
43
52
|
headers: {
|
|
44
53
|
"PRIVATE-TOKEN": token,
|
|
@@ -46,17 +55,31 @@ async function fetchDiscussionPage(target, page, token) {
|
|
|
46
55
|
},
|
|
47
56
|
});
|
|
48
57
|
if (!response.ok) {
|
|
58
|
+
throw new TaskRunnerError(`GitLab API request failed: HTTP ${response.status}\nURL: ${apiUrl}`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
body: (await response.json()),
|
|
62
|
+
headers: response.headers,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function fetchDiscussionPage(target, page, token) {
|
|
66
|
+
const apiUrl = `${target.discussionsApiUrl}?per_page=100&page=${page}`;
|
|
67
|
+
let payload;
|
|
68
|
+
try {
|
|
69
|
+
payload = await fetchGitLabJson(apiUrl, token);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
49
72
|
throw new TaskRunnerError([
|
|
50
|
-
`Failed to fetch GitLab merge request discussions:
|
|
73
|
+
`Failed to fetch GitLab merge request discussions: ${error.message}`,
|
|
51
74
|
`MR URL: ${target.mergeRequestUrl}`,
|
|
52
75
|
`GitLab project path: ${target.projectPath}`,
|
|
53
76
|
`GitLab merge request IID: ${target.mergeRequestIid}`,
|
|
54
77
|
`GitLab discussions API URL: ${apiUrl}`,
|
|
55
78
|
].join("\n"));
|
|
56
79
|
}
|
|
57
|
-
const nextPageHeader =
|
|
80
|
+
const nextPageHeader = payload.headers.get("x-next-page");
|
|
58
81
|
const nextPage = nextPageHeader && nextPageHeader.trim().length > 0 ? Number.parseInt(nextPageHeader, 10) : null;
|
|
59
|
-
const discussions =
|
|
82
|
+
const discussions = payload.body;
|
|
60
83
|
return { discussions, nextPage: Number.isNaN(nextPage ?? Number.NaN) ? null : nextPage };
|
|
61
84
|
}
|
|
62
85
|
async function fetchMergeRequestDiscussions(target, token) {
|
|
@@ -128,6 +151,142 @@ function buildGitLabReviewMarkdown(artifact) {
|
|
|
128
151
|
});
|
|
129
152
|
return lines.join("\n");
|
|
130
153
|
}
|
|
154
|
+
function normalizeMergeRequestAuthor(details) {
|
|
155
|
+
return details.author?.username?.trim() || details.author?.name?.trim() || "unknown";
|
|
156
|
+
}
|
|
157
|
+
function normalizeDiffContent(diff, response) {
|
|
158
|
+
const trimmed = diff?.trimEnd() ?? "";
|
|
159
|
+
if (trimmed.length > 0) {
|
|
160
|
+
return trimmed;
|
|
161
|
+
}
|
|
162
|
+
if (response.too_large) {
|
|
163
|
+
return "[Diff omitted by GitLab because it is too large.]";
|
|
164
|
+
}
|
|
165
|
+
if (response.collapsed) {
|
|
166
|
+
return "[Diff omitted by GitLab because it is collapsed.]";
|
|
167
|
+
}
|
|
168
|
+
return "[Diff content is empty.]";
|
|
169
|
+
}
|
|
170
|
+
function normalizeDiffFiles(diffs) {
|
|
171
|
+
return diffs.map((file, index) => {
|
|
172
|
+
const newPath = file.new_path?.trim() || file.old_path?.trim() || `unknown-file-${index + 1}`;
|
|
173
|
+
const oldPath = file.old_path?.trim() || newPath;
|
|
174
|
+
let changeType = "modified";
|
|
175
|
+
if (file.new_file) {
|
|
176
|
+
changeType = "added";
|
|
177
|
+
}
|
|
178
|
+
else if (file.deleted_file) {
|
|
179
|
+
changeType = "deleted";
|
|
180
|
+
}
|
|
181
|
+
else if (file.renamed_file) {
|
|
182
|
+
changeType = "renamed";
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
old_path: oldPath,
|
|
186
|
+
new_path: newPath,
|
|
187
|
+
change_type: changeType,
|
|
188
|
+
new_file: Boolean(file.new_file),
|
|
189
|
+
renamed_file: Boolean(file.renamed_file),
|
|
190
|
+
deleted_file: Boolean(file.deleted_file),
|
|
191
|
+
generated_file: Boolean(file.generated_file),
|
|
192
|
+
too_large: Boolean(file.too_large),
|
|
193
|
+
collapsed: Boolean(file.collapsed),
|
|
194
|
+
diff: normalizeDiffContent(file.diff, file),
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
async function fetchMergeRequestDetails(target, token) {
|
|
199
|
+
try {
|
|
200
|
+
return (await fetchGitLabJson(target.mergeRequestApiUrl, token)).body;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
throw new TaskRunnerError([
|
|
204
|
+
`Failed to fetch GitLab merge request details: ${error.message}`,
|
|
205
|
+
`MR URL: ${target.mergeRequestUrl}`,
|
|
206
|
+
`GitLab project path: ${target.projectPath}`,
|
|
207
|
+
`GitLab merge request IID: ${target.mergeRequestIid}`,
|
|
208
|
+
`GitLab merge request API URL: ${target.mergeRequestApiUrl}`,
|
|
209
|
+
].join("\n"));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async function fetchMergeRequestDiffsPage(target, page, token) {
|
|
213
|
+
const apiUrl = `${target.diffsApiUrl}?per_page=100&page=${page}`;
|
|
214
|
+
let payload;
|
|
215
|
+
try {
|
|
216
|
+
payload = await fetchGitLabJson(apiUrl, token);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
throw new TaskRunnerError([
|
|
220
|
+
`Failed to fetch GitLab merge request diffs: ${error.message}`,
|
|
221
|
+
`MR URL: ${target.mergeRequestUrl}`,
|
|
222
|
+
`GitLab project path: ${target.projectPath}`,
|
|
223
|
+
`GitLab merge request IID: ${target.mergeRequestIid}`,
|
|
224
|
+
`GitLab diffs API URL: ${apiUrl}`,
|
|
225
|
+
].join("\n"));
|
|
226
|
+
}
|
|
227
|
+
const nextPageHeader = payload.headers.get("x-next-page");
|
|
228
|
+
const nextPage = nextPageHeader && nextPageHeader.trim().length > 0 ? Number.parseInt(nextPageHeader, 10) : null;
|
|
229
|
+
return {
|
|
230
|
+
diffs: payload.body,
|
|
231
|
+
nextPage: Number.isNaN(nextPage ?? Number.NaN) ? null : nextPage,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function fetchMergeRequestDiffs(target, token) {
|
|
235
|
+
const diffs = [];
|
|
236
|
+
let page = 1;
|
|
237
|
+
while (true) {
|
|
238
|
+
const chunk = await fetchMergeRequestDiffsPage(target, page, token);
|
|
239
|
+
diffs.push(...chunk.diffs);
|
|
240
|
+
if (!chunk.nextPage) {
|
|
241
|
+
return diffs;
|
|
242
|
+
}
|
|
243
|
+
page = chunk.nextPage;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function buildGitLabMergeRequestDiffMarkdown(artifact) {
|
|
247
|
+
const lines = [
|
|
248
|
+
"# GitLab MR Diff",
|
|
249
|
+
"",
|
|
250
|
+
`- MR: ${artifact.merge_request_url}`,
|
|
251
|
+
`- Title: ${artifact.merge_request.title}`,
|
|
252
|
+
`- Project: ${artifact.project_path}`,
|
|
253
|
+
`- IID: ${artifact.merge_request_iid}`,
|
|
254
|
+
`- State: ${artifact.merge_request.state}`,
|
|
255
|
+
`- Draft: ${artifact.merge_request.draft ? "yes" : "no"}`,
|
|
256
|
+
`- Author: ${artifact.merge_request.author}`,
|
|
257
|
+
`- Branches: ${artifact.merge_request.source_branch} -> ${artifact.merge_request.target_branch}`,
|
|
258
|
+
`- SHA: ${artifact.merge_request.sha}`,
|
|
259
|
+
`- Fetched at: ${artifact.fetched_at}`,
|
|
260
|
+
`- Files changed: ${artifact.files.length}`,
|
|
261
|
+
"",
|
|
262
|
+
];
|
|
263
|
+
const description = artifact.merge_request.description.trim();
|
|
264
|
+
if (description) {
|
|
265
|
+
lines.push("## Description", "", description, "");
|
|
266
|
+
}
|
|
267
|
+
if (artifact.files.length === 0) {
|
|
268
|
+
lines.push("Изменений в diff не найдено.");
|
|
269
|
+
return lines.join("\n");
|
|
270
|
+
}
|
|
271
|
+
artifact.files.forEach((file, index) => {
|
|
272
|
+
lines.push(`## File ${index + 1}: ${file.new_path}`);
|
|
273
|
+
lines.push(`- Change type: ${file.change_type}`);
|
|
274
|
+
if (file.old_path !== file.new_path) {
|
|
275
|
+
lines.push(`- Old path: ${file.old_path}`);
|
|
276
|
+
}
|
|
277
|
+
if (file.generated_file) {
|
|
278
|
+
lines.push("- Generated: yes");
|
|
279
|
+
}
|
|
280
|
+
if (file.too_large) {
|
|
281
|
+
lines.push("- Too large: yes");
|
|
282
|
+
}
|
|
283
|
+
if (file.collapsed) {
|
|
284
|
+
lines.push("- Collapsed: yes");
|
|
285
|
+
}
|
|
286
|
+
lines.push("", "```diff", file.diff, "```", "");
|
|
287
|
+
});
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
131
290
|
export async function fetchGitLabReview(mergeRequestUrl, outputFile, outputJsonFile) {
|
|
132
291
|
const token = process.env.GITLAB_TOKEN?.trim();
|
|
133
292
|
if (!token) {
|
|
@@ -151,3 +310,38 @@ export async function fetchGitLabReview(mergeRequestUrl, outputFile, outputJsonF
|
|
|
151
310
|
await writeFile(outputFile, `${buildGitLabReviewMarkdown(artifact)}\n`, "utf8");
|
|
152
311
|
return artifact;
|
|
153
312
|
}
|
|
313
|
+
export async function fetchGitLabMergeRequestDiff(mergeRequestUrl, outputFile, outputJsonFile) {
|
|
314
|
+
const token = process.env.GITLAB_TOKEN?.trim();
|
|
315
|
+
if (!token) {
|
|
316
|
+
throw new TaskRunnerError("GITLAB_TOKEN is required for gitlab-diff-review flow.");
|
|
317
|
+
}
|
|
318
|
+
const target = buildGitLabMergeRequestDiffFetchTarget(mergeRequestUrl);
|
|
319
|
+
const [details, diffs] = await Promise.all([fetchMergeRequestDetails(target, token), fetchMergeRequestDiffs(target, token)]);
|
|
320
|
+
const files = normalizeDiffFiles(diffs);
|
|
321
|
+
const fetchedAt = new Date().toISOString();
|
|
322
|
+
const artifact = {
|
|
323
|
+
summary: files.length > 0 ? `Fetched GitLab MR diff with ${files.length} changed files.` : "GitLab MR diff is empty.",
|
|
324
|
+
merge_request_url: target.mergeRequestUrl,
|
|
325
|
+
project_path: target.projectPath,
|
|
326
|
+
merge_request_iid: target.mergeRequestIid,
|
|
327
|
+
fetched_at: fetchedAt,
|
|
328
|
+
merge_request: {
|
|
329
|
+
title: details.title?.trim() || `MR !${target.mergeRequestIid}`,
|
|
330
|
+
description: details.description?.trim() || "",
|
|
331
|
+
state: details.state?.trim() || "unknown",
|
|
332
|
+
draft: Boolean(details.draft),
|
|
333
|
+
source_branch: details.source_branch?.trim() || "unknown",
|
|
334
|
+
target_branch: details.target_branch?.trim() || "unknown",
|
|
335
|
+
sha: details.sha?.trim() || "unknown",
|
|
336
|
+
author: normalizeMergeRequestAuthor(details),
|
|
337
|
+
created_at: details.created_at ?? new Date(0).toISOString(),
|
|
338
|
+
updated_at: details.updated_at ?? new Date(0).toISOString(),
|
|
339
|
+
},
|
|
340
|
+
files,
|
|
341
|
+
};
|
|
342
|
+
mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
343
|
+
mkdirSync(path.dirname(outputJsonFile), { recursive: true });
|
|
344
|
+
await writeFile(outputJsonFile, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
345
|
+
await writeFile(outputFile, `${buildGitLabMergeRequestDiffMarkdown(artifact)}\n`, "utf8");
|
|
346
|
+
return artifact;
|
|
347
|
+
}
|