@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.
@@ -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: baseURL.endsWith('/') ? `${baseURL}api/v4` : `${baseURL}/api/v4`,
24
+ baseURL: `${root}api/v4`,
23
25
  timeout: HTTP_TIMEOUT_MS,
24
- headers: {
25
- 'Authorization': `Bearer ${token}`,
26
- 'Content-Type': 'application/json'
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({ _, destUrl, sourceGroup, destinationGroupPath, sourceProjects }) {
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; // ends with .git
306
+ const oldRepoUrl = project.http_url_to_repo || project.httpUrlToRepo;
261
307
 
262
- // path_with_namespace is like "group/subgroup/project-1"
263
- let relativePath;
264
- if (project.path_with_namespace.startsWith(groupPrefix)) {
265
- relativePath = project.path_with_namespace.slice(groupPrefix.length);
266
- } else {
267
- // Fallback if for some reason full_path is not a prefix
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
- const sourceProjects = await source.getGroupProjects(sourceGroup.id);
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(`${p.name_with_namespace}`));
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ibm-cloud/cd-tools",
3
- "version": "1.11.0",
3
+ "version": "1.11.1",
4
4
  "description": "Tools and utilities for the IBM Cloud Continuous Delivery service and resources",
5
5
  "repository": {
6
6
  "type": "git",