agentweaver 0.1.4 → 0.1.6
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/Dockerfile.codex +55 -0
- package/README.md +11 -7
- package/dist/artifacts.js +64 -6
- package/dist/executors/configs/fetch-gitlab-review-config.js +3 -0
- package/dist/executors/fetch-gitlab-review-executor.js +25 -0
- package/dist/gitlab.js +153 -0
- package/dist/index.js +123 -98
- package/dist/interactive-ui.js +433 -28
- package/dist/pipeline/context.js +1 -0
- package/dist/pipeline/flow-specs/auto.json +176 -2
- package/dist/pipeline/flow-specs/gitlab-review.json +347 -0
- package/dist/pipeline/flow-specs/implement.json +12 -9
- package/dist/pipeline/flow-specs/mr-description.json +28 -0
- package/dist/pipeline/flow-specs/plan.json +52 -0
- package/dist/pipeline/flow-specs/preflight.json +32 -0
- package/dist/pipeline/flow-specs/review-fix.json +79 -10
- package/dist/pipeline/flow-specs/review.json +79 -0
- package/dist/pipeline/flow-specs/run-linter-loop.json +17 -11
- package/dist/pipeline/flow-specs/run-tests-loop.json +17 -11
- package/dist/pipeline/flow-specs/task-describe.json +29 -1
- package/dist/pipeline/node-registry.js +35 -0
- package/dist/pipeline/nodes/fetch-gitlab-review-node.js +34 -0
- package/dist/pipeline/nodes/gitlab-review-artifacts-node.js +105 -0
- package/dist/pipeline/nodes/local-script-check-node.js +81 -0
- package/dist/pipeline/nodes/review-findings-form-node.js +65 -0
- package/dist/pipeline/nodes/user-input-node.js +93 -0
- package/dist/pipeline/prompt-registry.js +1 -3
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/value-resolver.js +37 -4
- package/dist/prompts.js +26 -17
- package/dist/structured-artifacts.js +208 -81
- package/dist/user-input.js +171 -0
- package/docker-compose.yml +384 -0
- package/package.json +7 -3
- package/run_linter.sh +89 -0
- package/run_tests.sh +113 -0
- package/verify_build.sh +104 -0
- package/dist/executors/claude-summary-executor.js +0 -31
- package/dist/executors/configs/claude-summary-config.js +0 -8
- package/dist/pipeline/flow-runner.js +0 -13
- package/dist/pipeline/flow-specs/test-fix.json +0 -24
- package/dist/pipeline/flow-specs/test-linter-fix.json +0 -24
- package/dist/pipeline/flow-specs/test.json +0 -19
- package/dist/pipeline/flow-types.js +0 -1
- package/dist/pipeline/flows/implement-flow.js +0 -47
- package/dist/pipeline/flows/plan-flow.js +0 -42
- package/dist/pipeline/flows/review-fix-flow.js +0 -62
- package/dist/pipeline/flows/review-flow.js +0 -124
- package/dist/pipeline/flows/test-fix-flow.js +0 -12
- package/dist/pipeline/flows/test-flow.js +0 -32
- package/dist/pipeline/nodes/claude-summary-node.js +0 -38
- package/dist/pipeline/nodes/implement-codex-node.js +0 -16
- package/dist/pipeline/nodes/task-summary-node.js +0 -42
package/Dockerfile.codex
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
FROM golang:1.25.5-bookworm
|
|
2
|
+
|
|
3
|
+
ARG GOLANGCI_LINT_VERSION=v2.7.2
|
|
4
|
+
ARG MOCKGEN_VERSION=v1.6.0
|
|
5
|
+
ARG SWAG_VERSION=latest
|
|
6
|
+
ARG PROTOC_GEN_GO_VERSION=latest
|
|
7
|
+
ARG PROTOC_GEN_GO_GRPC_VERSION=latest
|
|
8
|
+
|
|
9
|
+
RUN apt-get update \
|
|
10
|
+
&& apt-get install -y --no-install-recommends \
|
|
11
|
+
ca-certificates \
|
|
12
|
+
nodejs \
|
|
13
|
+
npm \
|
|
14
|
+
curl \
|
|
15
|
+
jq \
|
|
16
|
+
less \
|
|
17
|
+
file \
|
|
18
|
+
make \
|
|
19
|
+
procps \
|
|
20
|
+
ripgrep \
|
|
21
|
+
git \
|
|
22
|
+
openssh-client \
|
|
23
|
+
docker.io \
|
|
24
|
+
protobuf-compiler \
|
|
25
|
+
unzip \
|
|
26
|
+
zip \
|
|
27
|
+
findutils \
|
|
28
|
+
&& update-ca-certificates \
|
|
29
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
30
|
+
|
|
31
|
+
RUN if ! getent group 1000 >/dev/null; then groupadd -g 1000 codex; fi \
|
|
32
|
+
&& if ! getent passwd 1000 >/dev/null; then useradd -u 1000 -g 1000 -d /codex-home/home -M -s /bin/bash codex; fi
|
|
33
|
+
|
|
34
|
+
RUN npm install -g @openai/codex@latest \
|
|
35
|
+
&& npm cache clean --force
|
|
36
|
+
|
|
37
|
+
RUN GOBIN=/usr/local/bin go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} \
|
|
38
|
+
&& GOBIN=/usr/local/bin go install github.com/golang/mock/mockgen@${MOCKGEN_VERSION} \
|
|
39
|
+
&& GOBIN=/usr/local/bin go install github.com/swaggo/swag/cmd/swag@${SWAG_VERSION} \
|
|
40
|
+
&& GOBIN=/usr/local/bin go install google.golang.org/protobuf/cmd/protoc-gen-go@${PROTOC_GEN_GO_VERSION} \
|
|
41
|
+
&& GOBIN=/usr/local/bin go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@${PROTOC_GEN_GO_GRPC_VERSION} \
|
|
42
|
+
&& ln -sf /usr/local/go/bin/go /usr/bin/go \
|
|
43
|
+
&& ln -sf /usr/local/go/bin/gofmt /usr/bin/gofmt
|
|
44
|
+
|
|
45
|
+
ENV PATH="/usr/local/go/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
46
|
+
|
|
47
|
+
COPY verify_build.sh /usr/local/bin/verify_build.sh
|
|
48
|
+
COPY run_tests.sh /usr/local/bin/run_tests.sh
|
|
49
|
+
COPY run_linter.sh /usr/local/bin/run_linter.sh
|
|
50
|
+
RUN chmod +x /usr/local/bin/verify_build.sh /usr/local/bin/run_tests.sh /usr/local/bin/run_linter.sh
|
|
51
|
+
|
|
52
|
+
WORKDIR /workspace
|
|
53
|
+
|
|
54
|
+
ENTRYPOINT ["codex"]
|
|
55
|
+
CMD ["--dangerously-bypass-approvals-and-sandbox"]
|
package/README.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# AgentWeaver
|
|
2
2
|
|
|
3
|
-
`AgentWeaver` is a TypeScript/Node.js CLI for engineering workflows around Jira, Codex, and Claude.
|
|
3
|
+
`AgentWeaver` is a TypeScript/Node.js CLI for engineering workflows around Jira, GitLab review artifacts, Codex, and Claude.
|
|
4
4
|
|
|
5
5
|
It orchestrates a flow like:
|
|
6
6
|
|
|
7
|
-
`plan -> implement ->
|
|
7
|
+
`plan -> implement -> run-linter-loop -> run-tests-loop -> review -> review-fix`
|
|
8
8
|
|
|
9
9
|
The package is designed to run as an npm CLI and includes an interactive terminal UI built on `neo-blessed`.
|
|
10
10
|
|
|
11
11
|
## What It Does
|
|
12
12
|
|
|
13
13
|
- Fetches a Jira issue by key or browse URL
|
|
14
|
+
- Fetches GitLab merge request review comments into reusable markdown and JSON artifacts
|
|
14
15
|
- Generates workflow artifacts such as design, implementation plan, QA plan, bug analysis, reviews, and summaries
|
|
15
|
-
-
|
|
16
|
-
- Runs workflow stages like `bug-analyze`, `bug-fix`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `
|
|
16
|
+
- Machine-readable JSON artifacts are stored under `.agentweaver-<TASK>/.artifacts/` and act as the source of truth between workflow steps; Markdown artifacts remain for human inspection
|
|
17
|
+
- Runs workflow stages like `bug-analyze`, `bug-fix`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `run-tests-loop`, `run-linter-loop`, and `auto`
|
|
17
18
|
- Persists compact `auto` pipeline state on disk so runs can resume without storing large agent outputs
|
|
18
19
|
- Uses Docker runtime services for isolated Codex execution and build verification
|
|
19
20
|
|
|
@@ -22,9 +23,9 @@ The package is designed to run as an npm CLI and includes an interactive termina
|
|
|
22
23
|
The CLI now uses an executor + node + declarative flow architecture.
|
|
23
24
|
|
|
24
25
|
- `src/index.ts` remains the CLI entrypoint and high-level orchestration layer
|
|
25
|
-
- `src/executors/` contains first-class executors for external actions such as Jira fetch, local Codex, Docker-based build verification, Claude, Claude summaries, and process execution
|
|
26
|
+
- `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
|
|
26
27
|
- `src/pipeline/nodes/` contains reusable runtime nodes built on top of executors
|
|
27
|
-
- `src/pipeline/flow-specs/` contains declarative JSON flow specs for `preflight`, `bug-analyze`, `bug-fix`, `mr-description`, `plan`, `task-describe`, `implement`, `review`, `review-fix`, `
|
|
28
|
+
- `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-tests-loop`, `run-linter-loop`, and `auto`
|
|
28
29
|
- `src/runtime/` contains shared runtime services such as command resolution, Docker runtime environment setup, and subprocess execution
|
|
29
30
|
|
|
30
31
|
This keeps command handlers focused on choosing a flow and providing parameters instead of assembling prompts and subprocess wiring inline.
|
|
@@ -86,6 +87,7 @@ Required:
|
|
|
86
87
|
Common optional variables:
|
|
87
88
|
|
|
88
89
|
- `JIRA_BASE_URL` — required when you pass only an issue key like `DEMO-123`
|
|
90
|
+
- `GITLAB_TOKEN` — personal access token for `gitlab-review`
|
|
89
91
|
- `AGENTWEAVER_HOME` — path to the AgentWeaver installation directory
|
|
90
92
|
- `DOCKER_COMPOSE_BIN` — override compose command, for example `docker compose`
|
|
91
93
|
- `CODEX_BIN` — override `codex` executable path
|
|
@@ -117,6 +119,7 @@ Direct CLI usage:
|
|
|
117
119
|
agentweaver plan DEMO-3288
|
|
118
120
|
agentweaver bug-analyze DEMO-3288
|
|
119
121
|
agentweaver bug-fix DEMO-3288
|
|
122
|
+
agentweaver gitlab-review DEMO-3288
|
|
120
123
|
agentweaver mr-description DEMO-3288
|
|
121
124
|
agentweaver task-describe DEMO-3288
|
|
122
125
|
agentweaver implement DEMO-3288
|
|
@@ -132,6 +135,7 @@ From source checkout:
|
|
|
132
135
|
node dist/index.js plan DEMO-3288
|
|
133
136
|
node dist/index.js bug-analyze DEMO-3288
|
|
134
137
|
node dist/index.js bug-fix DEMO-3288
|
|
138
|
+
node dist/index.js gitlab-review DEMO-3288
|
|
135
139
|
node dist/index.js mr-description DEMO-3288
|
|
136
140
|
node dist/index.js task-describe DEMO-3288
|
|
137
141
|
node dist/index.js auto DEMO-3288
|
|
@@ -189,7 +193,7 @@ Activity pane behavior:
|
|
|
189
193
|
|
|
190
194
|
## Docker Runtime
|
|
191
195
|
|
|
192
|
-
Docker is used as an isolated execution environment for Codex
|
|
196
|
+
Docker is used as an isolated execution environment for Codex-related runtime scenarios that still require container orchestration.
|
|
193
197
|
|
|
194
198
|
Main services:
|
|
195
199
|
|
package/dist/artifacts.js
CHANGED
|
@@ -11,61 +11,98 @@ export function taskWorkspaceDir(taskKey) {
|
|
|
11
11
|
export function ensureTaskWorkspaceDir(taskKey) {
|
|
12
12
|
const workspaceDir = taskWorkspaceDir(taskKey);
|
|
13
13
|
mkdirSync(workspaceDir, { recursive: true });
|
|
14
|
+
mkdirSync(taskArtifactsDir(taskKey), { recursive: true });
|
|
14
15
|
return workspaceDir;
|
|
15
16
|
}
|
|
16
17
|
export function taskWorkspaceFile(taskKey, fileName) {
|
|
17
18
|
return path.join(taskWorkspaceDir(taskKey), fileName);
|
|
18
19
|
}
|
|
20
|
+
export function taskArtifactsDir(taskKey) {
|
|
21
|
+
return path.join(taskWorkspaceDir(taskKey), ".artifacts");
|
|
22
|
+
}
|
|
23
|
+
export function taskArtifactsFile(taskKey, fileName) {
|
|
24
|
+
return path.join(taskArtifactsDir(taskKey), fileName);
|
|
25
|
+
}
|
|
19
26
|
export function artifactFile(prefix, taskKey, iteration) {
|
|
20
27
|
return taskWorkspaceFile(taskKey, `${prefix}-${taskKey}-${iteration}.md`);
|
|
21
28
|
}
|
|
29
|
+
export function artifactJsonFile(prefix, taskKey, iteration) {
|
|
30
|
+
return taskArtifactsFile(taskKey, `${prefix}-${taskKey}-${iteration}.json`);
|
|
31
|
+
}
|
|
22
32
|
export function designFile(taskKey) {
|
|
23
33
|
return artifactFile("design", taskKey, 1);
|
|
24
34
|
}
|
|
35
|
+
export function designJsonFile(taskKey) {
|
|
36
|
+
return artifactJsonFile("design", taskKey, 1);
|
|
37
|
+
}
|
|
25
38
|
export function planFile(taskKey) {
|
|
26
39
|
return artifactFile("plan", taskKey, 1);
|
|
27
40
|
}
|
|
41
|
+
export function planJsonFile(taskKey) {
|
|
42
|
+
return artifactJsonFile("plan", taskKey, 1);
|
|
43
|
+
}
|
|
28
44
|
export function bugAnalyzeFile(taskKey) {
|
|
29
45
|
return taskWorkspaceFile(taskKey, `bug-analyze-${taskKey}.md`);
|
|
30
46
|
}
|
|
31
47
|
export function bugAnalyzeJsonFile(taskKey) {
|
|
32
|
-
return
|
|
48
|
+
return taskArtifactsFile(taskKey, `bug-analyze-${taskKey}.json`);
|
|
33
49
|
}
|
|
34
50
|
export function bugFixDesignFile(taskKey) {
|
|
35
51
|
return taskWorkspaceFile(taskKey, `bug-fix-design-${taskKey}.md`);
|
|
36
52
|
}
|
|
37
53
|
export function bugFixDesignJsonFile(taskKey) {
|
|
38
|
-
return
|
|
54
|
+
return taskArtifactsFile(taskKey, `bug-fix-design-${taskKey}.json`);
|
|
39
55
|
}
|
|
40
56
|
export function bugFixPlanFile(taskKey) {
|
|
41
57
|
return taskWorkspaceFile(taskKey, `bug-fix-plan-${taskKey}.md`);
|
|
42
58
|
}
|
|
43
59
|
export function bugFixPlanJsonFile(taskKey) {
|
|
44
|
-
return
|
|
60
|
+
return taskArtifactsFile(taskKey, `bug-fix-plan-${taskKey}.json`);
|
|
45
61
|
}
|
|
46
62
|
export function qaFile(taskKey) {
|
|
47
63
|
return artifactFile("qa", taskKey, 1);
|
|
48
64
|
}
|
|
65
|
+
export function qaJsonFile(taskKey) {
|
|
66
|
+
return artifactJsonFile("qa", taskKey, 1);
|
|
67
|
+
}
|
|
49
68
|
export function taskSummaryFile(taskKey) {
|
|
50
69
|
return artifactFile("task", taskKey, 1);
|
|
51
70
|
}
|
|
71
|
+
export function taskSummaryJsonFile(taskKey) {
|
|
72
|
+
return artifactJsonFile("task", taskKey, 1);
|
|
73
|
+
}
|
|
52
74
|
export function readyToMergeFile(taskKey) {
|
|
53
75
|
return taskWorkspaceFile(taskKey, READY_TO_MERGE_FILE);
|
|
54
76
|
}
|
|
55
77
|
export function jiraTaskFile(taskKey) {
|
|
56
|
-
return
|
|
78
|
+
return taskArtifactsFile(taskKey, `${taskKey}.json`);
|
|
57
79
|
}
|
|
58
80
|
export function jiraDescriptionFile(taskKey) {
|
|
59
81
|
return taskWorkspaceFile(taskKey, `jira-${taskKey}-description.md`);
|
|
60
82
|
}
|
|
83
|
+
export function jiraDescriptionJsonFile(taskKey) {
|
|
84
|
+
return taskArtifactsFile(taskKey, `jira-${taskKey}-description.json`);
|
|
85
|
+
}
|
|
61
86
|
export function mrDescriptionFile(taskKey) {
|
|
62
87
|
return taskWorkspaceFile(taskKey, `mr-description-${taskKey}.md`);
|
|
63
88
|
}
|
|
89
|
+
export function mrDescriptionJsonFile(taskKey) {
|
|
90
|
+
return taskArtifactsFile(taskKey, `mr-description-${taskKey}.json`);
|
|
91
|
+
}
|
|
92
|
+
export function gitlabReviewFile(taskKey) {
|
|
93
|
+
return taskWorkspaceFile(taskKey, `gitlab-review-${taskKey}.md`);
|
|
94
|
+
}
|
|
95
|
+
export function gitlabReviewJsonFile(taskKey) {
|
|
96
|
+
return taskArtifactsFile(taskKey, `gitlab-review-${taskKey}.json`);
|
|
97
|
+
}
|
|
98
|
+
export function gitlabReviewInputJsonFile(taskKey) {
|
|
99
|
+
return taskArtifactsFile(taskKey, `gitlab-review-input-${taskKey}.json`);
|
|
100
|
+
}
|
|
64
101
|
export function autoStateFile(taskKey) {
|
|
65
|
-
return
|
|
102
|
+
return taskArtifactsFile(taskKey, `.agentweaver-state-${taskKey}.json`);
|
|
66
103
|
}
|
|
67
104
|
export function planArtifacts(taskKey) {
|
|
68
|
-
return [designFile(taskKey), planFile(taskKey), qaFile(taskKey)];
|
|
105
|
+
return [designFile(taskKey), designJsonFile(taskKey), planFile(taskKey), planJsonFile(taskKey), qaFile(taskKey), qaJsonFile(taskKey)];
|
|
69
106
|
}
|
|
70
107
|
export function bugAnalyzeArtifacts(taskKey) {
|
|
71
108
|
return [
|
|
@@ -77,6 +114,27 @@ export function bugAnalyzeArtifacts(taskKey) {
|
|
|
77
114
|
bugFixPlanJsonFile(taskKey),
|
|
78
115
|
];
|
|
79
116
|
}
|
|
117
|
+
export function reviewFile(taskKey, iteration) {
|
|
118
|
+
return artifactFile("review", taskKey, iteration);
|
|
119
|
+
}
|
|
120
|
+
export function reviewJsonFile(taskKey, iteration) {
|
|
121
|
+
return artifactJsonFile("review", taskKey, iteration);
|
|
122
|
+
}
|
|
123
|
+
export function reviewReplyFile(taskKey, iteration) {
|
|
124
|
+
return artifactFile("review-reply", taskKey, iteration);
|
|
125
|
+
}
|
|
126
|
+
export function reviewReplyJsonFile(taskKey, iteration) {
|
|
127
|
+
return artifactJsonFile("review-reply", taskKey, iteration);
|
|
128
|
+
}
|
|
129
|
+
export function reviewFixFile(taskKey, iteration) {
|
|
130
|
+
return artifactFile("review-fix", taskKey, iteration);
|
|
131
|
+
}
|
|
132
|
+
export function reviewFixJsonFile(taskKey, iteration) {
|
|
133
|
+
return artifactJsonFile("review-fix", taskKey, iteration);
|
|
134
|
+
}
|
|
135
|
+
export function reviewFixSelectionJsonFile(taskKey, iteration) {
|
|
136
|
+
return artifactJsonFile("review-fix-selection", taskKey, iteration);
|
|
137
|
+
}
|
|
80
138
|
export function requireArtifacts(paths, message) {
|
|
81
139
|
const missing = paths.filter((filePath) => !existsSync(filePath));
|
|
82
140
|
if (missing.length > 0) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { fetchGitLabReviewExecutorDefaultConfig } from "./configs/fetch-gitlab-review-config.js";
|
|
2
|
+
import { buildGitLabReviewFetchTarget, fetchGitLabReview } from "../gitlab.js";
|
|
3
|
+
export const fetchGitLabReviewExecutor = {
|
|
4
|
+
kind: "fetch-gitlab-review",
|
|
5
|
+
version: 1,
|
|
6
|
+
defaultConfig: fetchGitLabReviewExecutorDefaultConfig,
|
|
7
|
+
async execute(context, input) {
|
|
8
|
+
const target = buildGitLabReviewFetchTarget(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 discussions API URL: ${target.discussionsApiUrl}\n`);
|
|
14
|
+
context.ui.writeStdout(`Saving GitLab review markdown to: ${input.outputFile}\n`);
|
|
15
|
+
context.ui.writeStdout(`Saving GitLab review JSON to: ${input.outputJsonFile}\n`);
|
|
16
|
+
}
|
|
17
|
+
const artifact = await fetchGitLabReview(input.mergeRequestUrl, input.outputFile, input.outputJsonFile);
|
|
18
|
+
return {
|
|
19
|
+
outputFile: input.outputFile,
|
|
20
|
+
outputJsonFile: input.outputJsonFile,
|
|
21
|
+
mergeRequestUrl: artifact.merge_request_url,
|
|
22
|
+
commentsCount: artifact.comments.length,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
package/dist/gitlab.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { TaskRunnerError } from "./errors.js";
|
|
5
|
+
const MERGE_REQUEST_PATH_RE = /^(?<projectPath>.+?)\/-\/merge_requests\/(?<iid>\d+)(?:\/.*)?$/;
|
|
6
|
+
function normalizeUrl(value) {
|
|
7
|
+
return value.trim().replace(/\/+$/, "");
|
|
8
|
+
}
|
|
9
|
+
function normalizeProjectPath(value) {
|
|
10
|
+
return value.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
11
|
+
}
|
|
12
|
+
export function parseGitLabMergeRequestUrl(mergeRequestUrl) {
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = new URL(normalizeUrl(mergeRequestUrl));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
throw new TaskRunnerError("Expected GitLab merge request URL like https://gitlab.example.com/group/project/-/merge_requests/123");
|
|
19
|
+
}
|
|
20
|
+
const match = MERGE_REQUEST_PATH_RE.exec(parsed.pathname);
|
|
21
|
+
const projectPath = normalizeProjectPath(match?.groups?.projectPath ?? "");
|
|
22
|
+
const iidRaw = match?.groups?.iid;
|
|
23
|
+
if (!projectPath || !iidRaw) {
|
|
24
|
+
throw new TaskRunnerError("Expected GitLab merge request URL like https://gitlab.example.com/group/project/-/merge_requests/123");
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
apiBaseUrl: `${parsed.protocol}//${parsed.host}/api/v4`,
|
|
28
|
+
mergeRequestUrl: `${parsed.protocol}//${parsed.host}${parsed.pathname}`,
|
|
29
|
+
projectPath,
|
|
30
|
+
mergeRequestIid: Number.parseInt(iidRaw, 10),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function buildGitLabReviewFetchTarget(mergeRequestUrl) {
|
|
34
|
+
const mergeRequestRef = parseGitLabMergeRequestUrl(mergeRequestUrl);
|
|
35
|
+
return {
|
|
36
|
+
...mergeRequestRef,
|
|
37
|
+
discussionsApiUrl: `${mergeRequestRef.apiBaseUrl}/projects/${encodeURIComponent(mergeRequestRef.projectPath)}/merge_requests/${mergeRequestRef.mergeRequestIid}/discussions`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function fetchDiscussionPage(target, page, token) {
|
|
41
|
+
const apiUrl = `${target.discussionsApiUrl}?per_page=100&page=${page}`;
|
|
42
|
+
const response = await fetch(apiUrl, {
|
|
43
|
+
headers: {
|
|
44
|
+
"PRIVATE-TOKEN": token,
|
|
45
|
+
Accept: "application/json",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new TaskRunnerError([
|
|
50
|
+
`Failed to fetch GitLab merge request discussions: HTTP ${response.status}`,
|
|
51
|
+
`MR URL: ${target.mergeRequestUrl}`,
|
|
52
|
+
`GitLab project path: ${target.projectPath}`,
|
|
53
|
+
`GitLab merge request IID: ${target.mergeRequestIid}`,
|
|
54
|
+
`GitLab discussions API URL: ${apiUrl}`,
|
|
55
|
+
].join("\n"));
|
|
56
|
+
}
|
|
57
|
+
const nextPageHeader = response.headers.get("x-next-page");
|
|
58
|
+
const nextPage = nextPageHeader && nextPageHeader.trim().length > 0 ? Number.parseInt(nextPageHeader, 10) : null;
|
|
59
|
+
const discussions = (await response.json());
|
|
60
|
+
return { discussions, nextPage: Number.isNaN(nextPage ?? Number.NaN) ? null : nextPage };
|
|
61
|
+
}
|
|
62
|
+
async function fetchMergeRequestDiscussions(target, token) {
|
|
63
|
+
const discussions = [];
|
|
64
|
+
let page = 1;
|
|
65
|
+
while (true) {
|
|
66
|
+
const chunk = await fetchDiscussionPage(target, page, token);
|
|
67
|
+
discussions.push(...chunk.discussions);
|
|
68
|
+
if (!chunk.nextPage) {
|
|
69
|
+
return discussions;
|
|
70
|
+
}
|
|
71
|
+
page = chunk.nextPage;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function normalizeDiscussionNotes(discussions) {
|
|
75
|
+
return discussions.flatMap((discussion) => {
|
|
76
|
+
const discussionId = String(discussion.id ?? "");
|
|
77
|
+
if (!discussionId) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return (discussion.notes ?? [])
|
|
81
|
+
.filter((note) => typeof note.body === "string" && note.body.trim().length > 0)
|
|
82
|
+
.filter((note) => note.system !== true)
|
|
83
|
+
.map((note) => ({
|
|
84
|
+
id: String(note.id ?? `${discussionId}-${note.created_at ?? "unknown"}`),
|
|
85
|
+
discussion_id: discussionId,
|
|
86
|
+
body: note.body?.trim() ?? "",
|
|
87
|
+
author: note.author?.username?.trim() || note.author?.name?.trim() || "unknown",
|
|
88
|
+
created_at: note.created_at ?? new Date(0).toISOString(),
|
|
89
|
+
system: Boolean(note.system),
|
|
90
|
+
resolvable: Boolean(note.resolvable),
|
|
91
|
+
resolved: Boolean(note.resolved),
|
|
92
|
+
file_path: note.position?.new_path ?? note.position?.old_path ?? null,
|
|
93
|
+
new_line: typeof note.position?.new_line === "number" ? note.position.new_line : null,
|
|
94
|
+
old_line: typeof note.position?.old_line === "number" ? note.position.old_line : null,
|
|
95
|
+
}));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function buildGitLabReviewMarkdown(artifact) {
|
|
99
|
+
const lines = [
|
|
100
|
+
"# GitLab Review",
|
|
101
|
+
"",
|
|
102
|
+
`- MR: ${artifact.merge_request_url}`,
|
|
103
|
+
`- Project: ${artifact.project_path}`,
|
|
104
|
+
`- IID: ${artifact.merge_request_iid}`,
|
|
105
|
+
`- Fetched at: ${artifact.fetched_at}`,
|
|
106
|
+
`- Comments: ${artifact.comments.length}`,
|
|
107
|
+
"",
|
|
108
|
+
];
|
|
109
|
+
if (artifact.comments.length === 0) {
|
|
110
|
+
lines.push("Код-ревью комментариев не найдено.");
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
artifact.comments.forEach((comment, index) => {
|
|
114
|
+
lines.push(`## Comment ${index + 1}`);
|
|
115
|
+
lines.push(`- Author: ${comment.author}`);
|
|
116
|
+
lines.push(`- Created at: ${comment.created_at}`);
|
|
117
|
+
lines.push(`- Discussion: ${comment.discussion_id}`);
|
|
118
|
+
if (comment.file_path) {
|
|
119
|
+
const location = [comment.file_path, comment.new_line ?? comment.old_line].filter((item) => item !== null).join(":");
|
|
120
|
+
lines.push(`- Location: ${location}`);
|
|
121
|
+
}
|
|
122
|
+
if (comment.resolvable) {
|
|
123
|
+
lines.push(`- Resolved: ${comment.resolved ? "yes" : "no"}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push("");
|
|
126
|
+
lines.push(comment.body);
|
|
127
|
+
lines.push("");
|
|
128
|
+
});
|
|
129
|
+
return lines.join("\n");
|
|
130
|
+
}
|
|
131
|
+
export async function fetchGitLabReview(mergeRequestUrl, outputFile, outputJsonFile) {
|
|
132
|
+
const token = process.env.GITLAB_TOKEN?.trim();
|
|
133
|
+
if (!token) {
|
|
134
|
+
throw new TaskRunnerError("GITLAB_TOKEN is required for gitlab-review flow.");
|
|
135
|
+
}
|
|
136
|
+
const target = buildGitLabReviewFetchTarget(mergeRequestUrl);
|
|
137
|
+
const discussions = await fetchMergeRequestDiscussions(target, token);
|
|
138
|
+
const comments = normalizeDiscussionNotes(discussions);
|
|
139
|
+
const fetchedAt = new Date().toISOString();
|
|
140
|
+
const artifact = {
|
|
141
|
+
summary: comments.length > 0 ? `Fetched ${comments.length} GitLab review comments.` : "No GitLab review comments found.",
|
|
142
|
+
merge_request_url: target.mergeRequestUrl,
|
|
143
|
+
project_path: target.projectPath,
|
|
144
|
+
merge_request_iid: target.mergeRequestIid,
|
|
145
|
+
fetched_at: fetchedAt,
|
|
146
|
+
comments,
|
|
147
|
+
};
|
|
148
|
+
mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
149
|
+
mkdirSync(path.dirname(outputJsonFile), { recursive: true });
|
|
150
|
+
await writeFile(outputJsonFile, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
151
|
+
await writeFile(outputFile, `${buildGitLabReviewMarkdown(artifact)}\n`, "utf8");
|
|
152
|
+
return artifact;
|
|
153
|
+
}
|