@ibm-cloud/cd-tools 1.11.0 → 1.11.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/cmd/direct-transfer.js +74 -21
- package/package.json +1 -1
package/cmd/direct-transfer.js
CHANGED
|
@@ -18,13 +18,17 @@ const HTTP_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes default
|
|
|
18
18
|
|
|
19
19
|
class GitLabClient {
|
|
20
20
|
constructor(baseURL, token) {
|
|
21
|
+
const root = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
|
|
22
|
+
|
|
21
23
|
this.client = axios.create({
|
|
22
|
-
baseURL:
|
|
24
|
+
baseURL: `${root}api/v4`,
|
|
23
25
|
timeout: HTTP_TIMEOUT_MS,
|
|
24
|
-
headers: {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
26
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
27
|
+
});
|
|
28
|
+
this.graph = axios.create({
|
|
29
|
+
baseURL: `${root}api`,
|
|
30
|
+
timeout: HTTP_TIMEOUT_MS,
|
|
31
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
28
32
|
});
|
|
29
33
|
}
|
|
30
34
|
|
|
@@ -110,6 +114,50 @@ class GitLabClient {
|
|
|
110
114
|
return projects;
|
|
111
115
|
}
|
|
112
116
|
|
|
117
|
+
async listGroupProjectsGraphQL(groupFullPath, { includeSubgroups = true, pageSize = 200, maxProjects = 5000 } = {}) {
|
|
118
|
+
const out = [];
|
|
119
|
+
let after = null;
|
|
120
|
+
|
|
121
|
+
const query = `
|
|
122
|
+
query($fullPath: ID!, $after: String, $includeSubgroups: Boolean!, $pageSize: Int!) {
|
|
123
|
+
group(fullPath: $fullPath) {
|
|
124
|
+
projects(includeSubgroups: $includeSubgroups, first: $pageSize, after: $after) {
|
|
125
|
+
nodes {
|
|
126
|
+
fullPath
|
|
127
|
+
nameWithNamespace
|
|
128
|
+
httpUrlToRepo
|
|
129
|
+
}
|
|
130
|
+
pageInfo {
|
|
131
|
+
hasNextPage
|
|
132
|
+
endCursor
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
|
|
139
|
+
while (out.length < maxProjects) {
|
|
140
|
+
const resp = await this.graph.post("/graphql", {
|
|
141
|
+
query,
|
|
142
|
+
variables: { fullPath: groupFullPath, after, includeSubgroups, pageSize },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (resp.data?.errors?.length) {
|
|
146
|
+
throw new Error(`GraphQL errors: ${JSON.stringify(resp.data.errors)}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const projects = resp.data?.data?.group?.projects?.nodes || [];
|
|
150
|
+
const pageInfo = resp.data?.data?.group?.projects?.pageInfo;
|
|
151
|
+
|
|
152
|
+
out.push(...projects);
|
|
153
|
+
|
|
154
|
+
if (!pageInfo?.hasNextPage) break;
|
|
155
|
+
after = pageInfo.endCursor;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
113
161
|
async getGroup(groupId) {
|
|
114
162
|
const response = await this.client.get(`/groups/${groupId}`);
|
|
115
163
|
return response.data;
|
|
@@ -250,23 +298,19 @@ function validateAndConvertRegion(region) {
|
|
|
250
298
|
}
|
|
251
299
|
|
|
252
300
|
// Build a mapping of: old http_url_to_repo -> new http_url_to_repo
|
|
253
|
-
async function generateUrlMappingFile({
|
|
301
|
+
async function generateUrlMappingFile({ destUrl, sourceGroup, destinationGroupPath, sourceProjects }) {
|
|
254
302
|
const destBase = destUrl.endsWith('/') ? destUrl.slice(0, -1) : destUrl;
|
|
255
303
|
const urlMapping = {};
|
|
256
304
|
|
|
257
|
-
const groupPrefix = `${sourceGroup.full_path}/`;
|
|
258
|
-
|
|
259
305
|
for (const project of sourceProjects) {
|
|
260
|
-
const oldRepoUrl = project.http_url_to_repo
|
|
306
|
+
const oldRepoUrl = project.http_url_to_repo || project.httpUrlToRepo;
|
|
261
307
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
relativePath = project.path_with_namespace;
|
|
269
|
-
}
|
|
308
|
+
const fullPath = project.path_with_namespace || project.fullPath || "";
|
|
309
|
+
const groupPrefix = `${sourceGroup.full_path}/`;
|
|
310
|
+
|
|
311
|
+
const relativePath = fullPath.startsWith(groupPrefix)
|
|
312
|
+
? fullPath.slice(groupPrefix.length)
|
|
313
|
+
: fullPath;
|
|
270
314
|
|
|
271
315
|
const newRepoUrl = `${destBase}/${destinationGroupPath}/${relativePath}.git`;
|
|
272
316
|
urlMapping[oldRepoUrl] = newRepoUrl;
|
|
@@ -463,14 +507,24 @@ async function directTransfer(options) {
|
|
|
463
507
|
console.log(`Fetching source group from ID: ${options.groupId}...`);
|
|
464
508
|
const sourceGroup = await source.getGroup(options.groupId);
|
|
465
509
|
|
|
466
|
-
// let destinationGroupName = options.newName || sourceGroup.name;
|
|
467
510
|
let destinationGroupPath = options.newName || sourceGroup.path;
|
|
468
511
|
|
|
469
|
-
|
|
512
|
+
let sourceProjects;
|
|
513
|
+
try {
|
|
514
|
+
sourceProjects = await source.listGroupProjectsGraphQL(sourceGroup.full_path, {
|
|
515
|
+
includeSubgroups: true,
|
|
516
|
+
pageSize: 100,
|
|
517
|
+
maxProjects: 10000,
|
|
518
|
+
});
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.warn(`[WARN] GraphQL listing failed (${e.message}). Falling back to REST safe listing...`);
|
|
521
|
+
sourceProjects = await source.getGroupProjects(sourceGroup.id);
|
|
522
|
+
}
|
|
523
|
+
|
|
470
524
|
console.log(`Found ${sourceProjects.length} projects in source group`);
|
|
471
525
|
if (sourceProjects.length > 0) {
|
|
472
526
|
console.log('Projects to be migrated:');
|
|
473
|
-
sourceProjects.forEach(p => console.log(
|
|
527
|
+
sourceProjects.forEach(p => console.log(p.name_with_namespace || p.nameWithNamespace || p.fullPath));
|
|
474
528
|
}
|
|
475
529
|
|
|
476
530
|
if (options.newName) {
|
|
@@ -479,7 +533,6 @@ async function directTransfer(options) {
|
|
|
479
533
|
|
|
480
534
|
// Generate URL mapping JSON before starting the migration
|
|
481
535
|
await generateUrlMappingFile({
|
|
482
|
-
sourceUrl,
|
|
483
536
|
destUrl,
|
|
484
537
|
sourceGroup,
|
|
485
538
|
destinationGroupPath,
|