@ipul-labs/forge 1.13.0
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 +273 -0
- package/bin/README.md +210 -0
- package/bin/cli/analyze.js +516 -0
- package/bin/cli/apply.js +264 -0
- package/bin/cli/cifix.js +300 -0
- package/bin/cli/close.js +125 -0
- package/bin/cli/doctor.js +161 -0
- package/bin/cli/init.js +306 -0
- package/bin/cli/ink.js +57 -0
- package/bin/cli/issue.js +116 -0
- package/bin/cli/issues.js +155 -0
- package/bin/cli/merge.js +175 -0
- package/bin/cli/refine.js +253 -0
- package/bin/cli/repos.js +91 -0
- package/bin/cli/resolution.js +126 -0
- package/bin/cli/session.js +230 -0
- package/bin/cli/sessionStore.js +214 -0
- package/bin/cli/ship.js +762 -0
- package/bin/cli/utils.js +145 -0
- package/bin/cli/validate.js +280 -0
- package/bin/cli/watch.js +274 -0
- package/bin/forge-agent.js +174 -0
- package/dist/assets/index-CNTCMbW9.js +80 -0
- package/dist/assets/index-InpYS-zg.css +1 -0
- package/dist/favicon.svg +8 -0
- package/dist/index.html +19 -0
- package/dist-lib/favicon.svg +8 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/style.css +1 -0
- package/dist-lib/ui.d.ts +2 -0
- package/dist-lib/ui.js +7390 -0
- package/dist-lib/views/Forge/ForgeDashboard.d.ts +5 -0
- package/dist-lib/views/Forge/activeJobs.d.ts +29 -0
- package/dist-lib/views/Forge/analysisCache.d.ts +64 -0
- package/dist-lib/views/Forge/api.d.ts +4 -0
- package/dist-lib/views/Forge/components/AnalysisProgress.d.ts +13 -0
- package/dist-lib/views/Forge/components/CITerminal.d.ts +44 -0
- package/dist-lib/views/Forge/components/DiffViewer.d.ts +37 -0
- package/dist-lib/views/Forge/components/ForgeConnect.d.ts +6 -0
- package/dist-lib/views/Forge/components/IssueList.d.ts +28 -0
- package/dist-lib/views/Forge/components/MRStatus.d.ts +9 -0
- package/dist-lib/views/Forge/components/NewTaskModal.d.ts +1 -0
- package/dist-lib/views/Forge/components/OpenMRList.d.ts +11 -0
- package/dist-lib/views/Forge/components/PipelineStatus.d.ts +12 -0
- package/dist-lib/views/Forge/components/PipelinesList.d.ts +9 -0
- package/dist-lib/views/Forge/components/RolloutStatus.d.ts +5 -0
- package/dist-lib/views/Forge/components/UsageWidget.d.ts +8 -0
- package/dist-lib/views/Forge/components/ValidationPanel.d.ts +22 -0
- package/dist-lib/views/Forge/context/ForgeProvider.d.ts +23 -0
- package/dist-lib/views/Forge/hooks/useConfig.d.ts +54 -0
- package/dist-lib/views/Forge/hooks/useForgeDashboard.d.ts +107 -0
- package/dist-lib/views/Forge/hooks/useForgeQuota.d.ts +22 -0
- package/dist-lib/views/Forge/phases/AnalyzingView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/IdleView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/MonitoringIssueTab.d.ts +1 -0
- package/dist-lib/views/Forge/phases/MonitoringPipelineTab.d.ts +1 -0
- package/dist-lib/views/Forge/phases/MonitoringView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/ReviewingView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/SelectedIssueTab.d.ts +11 -0
- package/dist-lib/views/Forge/phases/SelectedMRTab.d.ts +1 -0
- package/dist-lib/views/Forge/phases/SelectedView.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/CloseIssueConfirm.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/DetailTabs.d.ts +14 -0
- package/dist-lib/views/Forge/phases/shared/EnvironmentsPanel.d.ts +9 -0
- package/dist-lib/views/Forge/phases/shared/ForgeActivity.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/IssueHeaderBlock.d.ts +13 -0
- package/dist-lib/views/Forge/phases/shared/MergedMRList.d.ts +4 -0
- package/dist-lib/views/Forge/phases/shared/PreviousAnalyses.d.ts +1 -0
- package/dist-lib/views/Forge/phases/shared/ResolutionCheckPanel.d.ts +6 -0
- package/dist-lib/views/Forge/settings/ConnectionRow.d.ts +16 -0
- package/dist-lib/views/Forge/settings/ConnectionsSection.d.ts +7 -0
- package/dist-lib/views/Forge/settings/DefaultsSection.d.ts +7 -0
- package/dist-lib/views/Forge/settings/ReposSection.d.ts +7 -0
- package/dist-lib/views/Forge/settings/SettingsView.d.ts +1 -0
- package/dist-lib/views/Forge/shell/ConfigBar.d.ts +1 -0
- package/dist-lib/views/Forge/shell/ForgeRouter.d.ts +9 -0
- package/dist-lib/views/Forge/shell/ForgeShell.d.ts +7 -0
- package/dist-lib/views/Forge/shell/Sidebar.d.ts +1 -0
- package/dist-lib/views/Forge/shell/SidebarFooterMenu.d.ts +1 -0
- package/dist-lib/views/Forge/shell/SidebarNav.d.ts +1 -0
- package/dist-lib/views/Forge/shell/SidebarNavItem.d.ts +10 -0
- package/dist-lib/views/Forge/shell/TopBar.d.ts +5 -0
- package/dist-lib/views/Forge/types.d.ts +235 -0
- package/dist-lib/views/Forge/utils.d.ts +11 -0
- package/package.json +88 -0
- package/server/forge/core/ForgeAgent.js +1144 -0
- package/server/forge/core/agentClient.js +36 -0
- package/server/forge/core/branchName.js +41 -0
- package/server/forge/core/claudeClient.js +356 -0
- package/server/forge/core/configStore.js +214 -0
- package/server/forge/core/gitlabClient.js +340 -0
- package/server/forge/core/jobs.js +180 -0
- package/server/forge/core/k8s.js +54 -0
- package/server/forge/core/localRepo.js +136 -0
- package/server/forge/core/openclawClient.js +125 -0
- package/server/forge/core/prompts.js +369 -0
- package/server/forge/core/providers/azuredevops.js +408 -0
- package/server/forge/core/providers/base.js +208 -0
- package/server/forge/core/providers/github.js +349 -0
- package/server/forge/core/providers/gitlab.js +529 -0
- package/server/forge/core/providers/index.js +47 -0
- package/server/forge/core/quotaClient.js +101 -0
- package/server/forge/core/validator/attribute.js +84 -0
- package/server/forge/core/validator/clone.js +91 -0
- package/server/forge/core/validator/cmd.js +98 -0
- package/server/forge/core/validator/fs.js +9 -0
- package/server/forge/core/validator/overlay.js +52 -0
- package/server/forge/core/validator/projectType.js +87 -0
- package/server/forge/core/validator/runners/go.js +107 -0
- package/server/forge/core/validator/runners/node.js +84 -0
- package/server/forge/core/validator/tdd.js +105 -0
- package/server/forge/core/validator.js +166 -0
- package/server/forge/routes.js +1012 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/forge/core/providers/github.js
|
|
3
|
+
*
|
|
4
|
+
* GitHub REST API v3 implementation of ScmProvider.
|
|
5
|
+
* Multi-file commits use the Git Data API (blob → tree → commit → ref).
|
|
6
|
+
*
|
|
7
|
+
* Required env vars:
|
|
8
|
+
* GITHUB_TOKEN — personal access token (repo + workflow scopes)
|
|
9
|
+
* GITHUB_OWNER — default org or user, e.g. "ipul-labs"
|
|
10
|
+
* GITHUB_REPO — default repository name, e.g. "web-app"
|
|
11
|
+
*
|
|
12
|
+
* Optional:
|
|
13
|
+
* GITHUB_BASE_BRANCH — default target branch (default: "main")
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getConnection } from "../configStore.js";
|
|
17
|
+
import { ScmProvider } from "./base.js";
|
|
18
|
+
|
|
19
|
+
export class GitHubProvider extends ScmProvider {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} [repo] "owner/repo" format — overrides env vars
|
|
22
|
+
*/
|
|
23
|
+
constructor(repo) {
|
|
24
|
+
super();
|
|
25
|
+
if (repo && repo.includes("/")) {
|
|
26
|
+
const [owner, repoName] = repo.split("/");
|
|
27
|
+
this._owner = owner;
|
|
28
|
+
this._repo = repoName;
|
|
29
|
+
} else {
|
|
30
|
+
this._owner = process.env.GITHUB_OWNER ?? "";
|
|
31
|
+
this._repo = repo ?? process.env.GITHUB_REPO ?? "";
|
|
32
|
+
}
|
|
33
|
+
this._base = "https://api.github.com";
|
|
34
|
+
this._defaultBranch = process.env.GITHUB_BASE_BRANCH ?? "main";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get name() { return "github"; }
|
|
38
|
+
get supportsRebase() { return false; }
|
|
39
|
+
|
|
40
|
+
// ─── Internal helpers ──────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
_headers() {
|
|
43
|
+
return {
|
|
44
|
+
Authorization: `Bearer ${getConnection("github").token}`,
|
|
45
|
+
Accept: "application/vnd.github+json",
|
|
46
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_repoBase() {
|
|
52
|
+
return `${this._base}/repos/${this._owner}/${this._repo}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async _req(path, options = {}) {
|
|
56
|
+
const url = path.startsWith("http") ? path : `${this._repoBase()}${path}`;
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
...options,
|
|
59
|
+
headers: { ...this._headers(), ...options.headers },
|
|
60
|
+
});
|
|
61
|
+
if (res.status === 204) return null;
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
const text = await res.text().catch(() => "");
|
|
64
|
+
throw new Error(`GitHub ${res.status} on ${path}: ${text}`);
|
|
65
|
+
}
|
|
66
|
+
return res.json();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Normalizers ──────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/** GitHub issues map closely to NormalIssue */
|
|
72
|
+
_issue(raw) {
|
|
73
|
+
return {
|
|
74
|
+
iid: raw.number,
|
|
75
|
+
title: raw.title,
|
|
76
|
+
description: raw.body ?? null,
|
|
77
|
+
labels: (raw.labels ?? []).map((l) => l.name),
|
|
78
|
+
author: { name: raw.user?.login ?? "" },
|
|
79
|
+
created_at: raw.created_at,
|
|
80
|
+
web_url: raw.html_url,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** GitHub PRs → NormalMR. GitHub has no server-side rebase API. */
|
|
85
|
+
_mr(raw) {
|
|
86
|
+
return {
|
|
87
|
+
iid: raw.number,
|
|
88
|
+
title: raw.title,
|
|
89
|
+
web_url: raw.html_url,
|
|
90
|
+
source_branch: raw.head?.ref ?? "",
|
|
91
|
+
target_branch: raw.base?.ref ?? "",
|
|
92
|
+
state: raw.merged_at ? "merged" : raw.state,
|
|
93
|
+
has_conflicts: raw.mergeable === false,
|
|
94
|
+
rebase_in_progress: false,
|
|
95
|
+
merge_status: raw.mergeable_state ?? "",
|
|
96
|
+
detailed_merge_status: null,
|
|
97
|
+
draft: raw.draft ?? false,
|
|
98
|
+
head_pipeline: raw.head?.sha
|
|
99
|
+
? null // populated separately via getCheckRuns if needed
|
|
100
|
+
: null,
|
|
101
|
+
author: { name: raw.user?.login ?? "" },
|
|
102
|
+
created_at: raw.created_at,
|
|
103
|
+
merged_at: raw.merged_at ?? null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** GitHub Actions workflow runs → NormalPipeline */
|
|
108
|
+
_pipeline(raw) {
|
|
109
|
+
return {
|
|
110
|
+
id: raw.id,
|
|
111
|
+
status: this._runStatus(raw),
|
|
112
|
+
web_url: raw.html_url,
|
|
113
|
+
created_at: raw.created_at,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_runStatus(run) {
|
|
118
|
+
if (run.conclusion) return run.conclusion; // success / failure / cancelled
|
|
119
|
+
if (run.status === "in_progress") return "running";
|
|
120
|
+
return run.status ?? "unknown";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** GitHub Actions jobs → NormalJob */
|
|
124
|
+
_job(raw) {
|
|
125
|
+
return {
|
|
126
|
+
id: raw.id,
|
|
127
|
+
name: raw.name,
|
|
128
|
+
stage: raw.workflow_name ?? "",
|
|
129
|
+
status: raw.conclusion ?? raw.status ?? "unknown",
|
|
130
|
+
web_url: raw.html_url,
|
|
131
|
+
duration: raw.completed_at && raw.started_at
|
|
132
|
+
? Math.round((new Date(raw.completed_at) - new Date(raw.started_at)) / 1000)
|
|
133
|
+
: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_diff(raw) {
|
|
138
|
+
return {
|
|
139
|
+
old_path: raw.previous_filename ?? raw.filename,
|
|
140
|
+
new_path: raw.filename,
|
|
141
|
+
diff: raw.patch ?? "",
|
|
142
|
+
new_file: raw.status === "added",
|
|
143
|
+
renamed_file: raw.status === "renamed",
|
|
144
|
+
deleted_file: raw.status === "removed",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Issues ───────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
async listOpenIssues() {
|
|
151
|
+
const raw = await this._req(
|
|
152
|
+
`/issues?state=open&sort=updated&per_page=25&pulls=false`,
|
|
153
|
+
);
|
|
154
|
+
// GitHub returns PRs as issues; filter them out
|
|
155
|
+
return (raw ?? []).filter((i) => !i.pull_request).map((i) => this._issue(i));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getIssue(iid) {
|
|
159
|
+
const raw = await this._req(`/issues/${iid}`);
|
|
160
|
+
return this._issue(raw);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async createIssue(title, description = "") {
|
|
164
|
+
const raw = await this._req("/issues", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: JSON.stringify({ title, body: description }),
|
|
167
|
+
});
|
|
168
|
+
return this._issue(raw);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async updateIssue(iid, { title, description }) {
|
|
172
|
+
const raw = await this._req(`/issues/${iid}`, {
|
|
173
|
+
method: "PATCH",
|
|
174
|
+
body: JSON.stringify({ title, body: description }),
|
|
175
|
+
});
|
|
176
|
+
return this._issue(raw);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async closeIssue(iid) {
|
|
180
|
+
const raw = await this._req(`/issues/${iid}`, {
|
|
181
|
+
method: "PATCH",
|
|
182
|
+
body: JSON.stringify({ state: "closed" }),
|
|
183
|
+
});
|
|
184
|
+
return this._issue(raw);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Repository ───────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
async getFileTree(srcPath = "src", ref = this._defaultBranch) {
|
|
190
|
+
// Use recursive tree API — much faster than walking
|
|
191
|
+
const data = await this._req(`/git/trees/${ref}?recursive=1`);
|
|
192
|
+
return (data.tree ?? [])
|
|
193
|
+
.filter((item) => item.type === "blob")
|
|
194
|
+
.filter((item) => item.path.startsWith(srcPath + "/") || item.path === srcPath)
|
|
195
|
+
.filter((item) => /\.(ts|tsx|js|jsx|json|css|yaml|yml|md)$/.test(item.path))
|
|
196
|
+
.map((item) => item.path);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getFileContent(filePath, ref = this._defaultBranch) {
|
|
200
|
+
const encoded = encodeURIComponent(filePath);
|
|
201
|
+
const data = await this._req(`/contents/${encoded}?ref=${ref}`);
|
|
202
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Mutations ────────────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
async createBranch(branchName, ref = this._defaultBranch) {
|
|
208
|
+
// Resolve ref to SHA
|
|
209
|
+
const refData = await this._req(`/git/refs/heads/${ref}`);
|
|
210
|
+
const sha = refData.object.sha;
|
|
211
|
+
return this._req("/git/refs", {
|
|
212
|
+
method: "POST",
|
|
213
|
+
body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha }),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Multi-file commit using GitHub Git Data API.
|
|
219
|
+
* @param {string} branch
|
|
220
|
+
* @param {string} message
|
|
221
|
+
* @param {import('./base.js').CommitAction[]} actions
|
|
222
|
+
*/
|
|
223
|
+
async commitFiles(branch, message, actions) {
|
|
224
|
+
// 1. Get current HEAD
|
|
225
|
+
const refData = await this._req(`/git/refs/heads/${branch}`);
|
|
226
|
+
const headSha = refData.object.sha;
|
|
227
|
+
|
|
228
|
+
// 2. Get base tree SHA from commit
|
|
229
|
+
const commitData = await this._req(`/git/commits/${headSha}`);
|
|
230
|
+
const baseTreeSha = commitData.tree.sha;
|
|
231
|
+
|
|
232
|
+
// 3. Create blobs for each file (skip deletes)
|
|
233
|
+
const treeEntries = await Promise.all(
|
|
234
|
+
actions.map(async (action) => {
|
|
235
|
+
if (action.action === "delete") {
|
|
236
|
+
return { path: action.file_path, mode: "100644", type: "blob", sha: null };
|
|
237
|
+
}
|
|
238
|
+
const blob = await this._req("/git/blobs", {
|
|
239
|
+
method: "POST",
|
|
240
|
+
body: JSON.stringify({ content: action.content, encoding: "utf-8" }),
|
|
241
|
+
});
|
|
242
|
+
return { path: action.file_path, mode: "100644", type: "blob", sha: blob.sha };
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// 4. Create new tree
|
|
247
|
+
const tree = await this._req("/git/trees", {
|
|
248
|
+
method: "POST",
|
|
249
|
+
body: JSON.stringify({ base_tree: baseTreeSha, tree: treeEntries }),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// 5. Create commit
|
|
253
|
+
const newCommit = await this._req("/git/commits", {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: JSON.stringify({ message, tree: tree.sha, parents: [headSha] }),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// 6. Update ref
|
|
259
|
+
return this._req(`/git/refs/heads/${branch}`, {
|
|
260
|
+
method: "PATCH",
|
|
261
|
+
body: JSON.stringify({ sha: newCommit.sha }),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async createMR({ sourceBranch, targetBranch = this._defaultBranch, title, description = "" }) {
|
|
266
|
+
const raw = await this._req("/pulls", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
body: JSON.stringify({
|
|
269
|
+
head: sourceBranch,
|
|
270
|
+
base: targetBranch,
|
|
271
|
+
title,
|
|
272
|
+
body: description,
|
|
273
|
+
}),
|
|
274
|
+
});
|
|
275
|
+
return this._mr(raw);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── PR queries ───────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
async listAllForgeMRs(state = "open") {
|
|
281
|
+
const ghState = state === "open" ? "open" : "closed";
|
|
282
|
+
const all = await this._req(`/pulls?state=${ghState}&per_page=50`);
|
|
283
|
+
const forge = (all ?? []).filter((pr) => pr.head?.ref?.startsWith("forge/"));
|
|
284
|
+
// GitHub rolls merged + closed-without-merge into `state=closed`; split
|
|
285
|
+
// them apart on merged_at so the UI tabs stay meaningful.
|
|
286
|
+
const filtered =
|
|
287
|
+
state === "merged" ? forge.filter((pr) => pr.merged_at)
|
|
288
|
+
: state === "closed" ? forge.filter((pr) => !pr.merged_at)
|
|
289
|
+
: forge;
|
|
290
|
+
return filtered.map((pr) => this._mr(pr));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async listForgeMRsForIssue(issueIid) {
|
|
294
|
+
const all = await this._req("/pulls?state=open&per_page=50");
|
|
295
|
+
return (all ?? [])
|
|
296
|
+
.filter((pr) => pr.head?.ref?.startsWith(`forge/issue-${issueIid}-`))
|
|
297
|
+
.map((pr) => this._mr(pr));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async listMergedForgeMRsForIssue(issueIid) {
|
|
301
|
+
const all = await this._req("/pulls?state=closed&per_page=50");
|
|
302
|
+
return (all ?? [])
|
|
303
|
+
.filter((pr) => pr.merged_at && pr.head?.ref?.startsWith(`forge/issue-${issueIid}-`))
|
|
304
|
+
.map((pr) => this._mr(pr));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async getMR(mrIid) {
|
|
308
|
+
const raw = await this._req(`/pulls/${mrIid}`);
|
|
309
|
+
return this._mr(raw);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async getMRDiffs(mrIid) {
|
|
313
|
+
const raw = await this._req(`/pulls/${mrIid}/files?per_page=100`);
|
|
314
|
+
return (raw ?? []).map((f) => this._diff(f));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// rebaseMR not supported — supportsRebase is false
|
|
318
|
+
|
|
319
|
+
// ─── CI / Pipelines (GitHub Actions) ─────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
async getMRPipelines(mrIid) {
|
|
322
|
+
const pr = await this._req(`/pulls/${mrIid}`);
|
|
323
|
+
const sha = pr.head?.sha;
|
|
324
|
+
if (!sha) return [];
|
|
325
|
+
const data = await this._req(`/actions/runs?head_sha=${sha}&per_page=10`);
|
|
326
|
+
return (data.workflow_runs ?? []).map((r) => this._pipeline(r));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async getPipeline(pipelineId) {
|
|
330
|
+
const raw = await this._req(`/actions/runs/${pipelineId}`);
|
|
331
|
+
return this._pipeline(raw);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async getPipelineJobs(pipelineId) {
|
|
335
|
+
const data = await this._req(`/actions/runs/${pipelineId}/jobs`);
|
|
336
|
+
return (data.jobs ?? []).map((j) => this._job(j));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async getJobTrace(jobId) {
|
|
340
|
+
const url = `${this._repoBase()}/actions/jobs/${jobId}/logs`;
|
|
341
|
+
const res = await fetch(url, { headers: this._headers() });
|
|
342
|
+
if (!res.ok) {
|
|
343
|
+
const text = await res.text().catch(() => "");
|
|
344
|
+
throw new Error(`GitHub ${res.status} on jobs/${jobId}/logs: ${text}`);
|
|
345
|
+
}
|
|
346
|
+
// GitHub redirects to a pre-signed S3 URL; fetch follows it automatically
|
|
347
|
+
return res.text();
|
|
348
|
+
}
|
|
349
|
+
}
|