@theholocron/holocron-plugin-github 5.0.0-alpha.369 → 5.0.0-alpha.370

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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Auth, AuthError, BoardItem, BoardState, BoardSummary, BoardTemplate, Ci, CiRun, CiRunFilter, EnsureReport, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, MoveReport, PagesConfig, PluginToken, Projects, PullRequest, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, SourceBranch, SourceFile, SourceRepoInfo, SourceRulesetDetail, SourceSecurity, SourceTeamMember, TeamEntry, TeamEntry as TeamEntry$1, TeamPermission, TeamPermission as TeamPermission$1, TrackerDoctorReport, TrackerUser, VerifyTokenFailure, VerifyTokenResult, VerifyTokenResult as VerifyTokenResult$1, VerifyTokenSuccess, createFeatureResolver } from "@theholocron/cli";
1
+ import { Auth, AuthError, BoardItem, BoardState, BoardSummary, BoardTemplate, Ci, CiRun, CiRunFilter, EnsureReport, Environment, Environments, ImportOptions, ImportResult, Issue, IssueRecord, IssueSearchFilter, IssueSelector, Issues, LabelDef, LifecycleResult, LifecycleSlot, MoveReport, PagesConfig, PluginToken, Projects, PullRequest, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, SourceBranch, SourceFile, SourceRepoInfo, SourceRulesetDetail, SourceSecurity, SourceTeamMember, TeamEntry, TeamEntry as TeamEntry$1, TeamPermission, TeamPermission as TeamPermission$1, TrackerDoctorReport, TrackerUser, VerifyTokenFailure, VerifyTokenResult, VerifyTokenResult as VerifyTokenResult$1, VerifyTokenSuccess, createFeatureResolver } from "@theholocron/cli";
2
2
  import { GitHubClient, GitHubClientOptions, GraphQLClient, createGitHubClient } from "@theholocron/github-client";
3
3
  /** Resolves the token for read-only GitHub access (`HOLOCRON_READ_TOKEN`, keyring key `github.read`). */
4
4
  export declare const resolveReadToken: import("@theholocron/cli").FeatureResolver;
@@ -104,6 +104,10 @@ export declare class GitHubIssues implements Issues {
104
104
  comment(key: string, body: string): Promise<void>;
105
105
  /** Checks the authenticated user, the repo and the label-backed lifecycle slots, and reports what resolves. */
106
106
  doctor(): Promise<TrackerDoctorReport>;
107
+ /** Reads issues with their comments, priority label, parent and blockers, for `holocron migrate`. */
108
+ export(selector: IssueSelector): Promise<IssueRecord[]>;
109
+ /** Makes the records as issues; one already made by an earlier run is skipped. */
110
+ import(records: IssueRecord[], options?: ImportOptions): Promise<ImportResult[]>;
107
111
  private resolveMilestone;
108
112
  private removeStatusLabels;
109
113
  private mapIssue;
package/dist/index.mjs CHANGED
@@ -151,6 +151,166 @@ var GitHubEnvironments = class {
151
151
  }));
152
152
  }
153
153
  };
154
+ // src/capabilities/issues-migration.ts
155
+ const MARKER = /<!-- holocron:migrated-from (\S+) -->/;
156
+ const PRIORITY_LABEL = /^priority\s*[:/-]\s*(urgent|high|medium|low)$/i;
157
+ const LINK_QUERY = `query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) {
158
+ issue(number: $number) { parent { number } blockedBy(first: 50) { nodes { number } } } } }`;
159
+ /** Splits `priority: high` labels from the rest. */
160
+ function splitLabels(names) {
161
+ let priority;
162
+ const labels = [];
163
+ for (const name of names) {
164
+ const match = PRIORITY_LABEL.exec(name);
165
+ if (match) priority = match[1].toLowerCase();
166
+ else labels.push(name);
167
+ }
168
+ return {
169
+ labels,
170
+ ...priority ? { priority } : {}
171
+ };
172
+ }
173
+ function categoryOf(issue) {
174
+ if (issue.state === "closed") return "done";
175
+ const names = issue.labels.map((l) => l.name.toLowerCase());
176
+ if (names.some((n) => n === "status:in-review" || n === "status: in review")) return "in-review";
177
+ if (names.some((n) => n === "status:in-progress" || n === "status: in progress")) return "in-progress";
178
+ return "open";
179
+ }
180
+ /** Reads the issues `selector` names, with their comments, priority label, parent and blockers. */
181
+ async function exportIssues(client, repo, selector) {
182
+ const [owner = "", name = ""] = repo.split("/", 2);
183
+ let raw;
184
+ if (selector.keys) raw = await Promise.all(selector.keys.map((k) => client.issues.getIssue(repo, parseNumber(k))));
185
+ else if (selector.milestone) {
186
+ const milestone = await findMilestone(client, repo, selector.milestone);
187
+ if (!milestone) throw new Error(`Milestone "${selector.milestone}" not found in ${repo}.`);
188
+ raw = await listAll(client, repo, { milestone: String(milestone.number) });
189
+ } else if (selector.all) raw = await listAll(client, repo, {});
190
+ else throw new Error("export needs keys, a milestone or all");
191
+ const issues = raw.filter((i) => !i.pull_request && !(selector.openOnly && i.state === "closed"));
192
+ const records = [];
193
+ for (const issue of issues) {
194
+ const comments = await client.issues.listComments(repo, issue.number);
195
+ const links = (await client.graphql.query(LINK_QUERY, {
196
+ owner,
197
+ name,
198
+ number: issue.number
199
+ }).catch(() => void 0))?.repository.issue;
200
+ const { labels, priority } = splitLabels(issue.labels.map((l) => l.name));
201
+ const body = issue.body?.replace(MARKER, "").trimEnd();
202
+ records.push({
203
+ key: `#${issue.number}`,
204
+ url: issue.html_url,
205
+ title: issue.title,
206
+ ...body ? { body } : {},
207
+ statusCategory: categoryOf(issue),
208
+ closed: issue.state === "closed",
209
+ ...priority ? { priority } : {},
210
+ labels: labels.filter((l) => !l.toLowerCase().startsWith("status:")),
211
+ ...issue.assignee ? { assignee: {
212
+ id: issue.assignee.login,
213
+ displayName: issue.assignee.name ?? issue.assignee.login,
214
+ ...issue.assignee.email ? { emailAddress: issue.assignee.email } : {}
215
+ } } : {},
216
+ ...issue.milestone ? { milestone: issue.milestone.title } : {},
217
+ ...links?.parent ? { parent: `#${links.parent.number}` } : {},
218
+ blockedBy: (links?.blockedBy.nodes ?? []).map((n) => `#${n.number}`),
219
+ comments: comments.map((c) => ({
220
+ ...c.user ? { author: c.user.login } : {},
221
+ body: c.body,
222
+ createdAt: c.created_at
223
+ })),
224
+ createdAt: issue.created_at ?? issue.updated_at,
225
+ updatedAt: issue.updated_at
226
+ });
227
+ }
228
+ return records;
229
+ }
230
+ /** Makes the records as issues, skipping any whose source address an earlier run already recorded. */
231
+ async function importIssues(client, repo, records, options = {}) {
232
+ const existing = /* @__PURE__ */ new Map();
233
+ for (const issue of await listAll(client, repo, {})) {
234
+ const url = issue.body ? MARKER.exec(issue.body)?.[1] : void 0;
235
+ if (url) existing.set(url, issue.number);
236
+ }
237
+ const results = [];
238
+ for (const record of records) {
239
+ const found = existing.get(record.url);
240
+ if (found !== void 0) {
241
+ results.push({
242
+ source: record.key,
243
+ key: `#${found}`,
244
+ created: false,
245
+ dropped: []
246
+ });
247
+ continue;
248
+ }
249
+ const dropped = ["createdAt (GitHub sets it; the original date is in the text)"];
250
+ if (record.assignee) dropped.push("assignee (not mapped)");
251
+ if (record.parent) dropped.push("parent (not set on GitHub)");
252
+ if (record.blockedBy.length > 0) dropped.push("blockedBy (not set on GitHub)");
253
+ if (options.dryRun) {
254
+ results.push({
255
+ source: record.key,
256
+ created: true,
257
+ dropped
258
+ });
259
+ continue;
260
+ }
261
+ const milestoneName = options.milestone ?? record.milestone;
262
+ const milestone = milestoneName ? await ensureMilestone(client, repo, milestoneName) : void 0;
263
+ const labels = [...record.labels, ...record.priority && record.priority !== "none" ? [`priority: ${record.priority}`] : []];
264
+ const created = await client.issues.createIssue(repo, {
265
+ title: record.title,
266
+ body: withProvenance(record),
267
+ ...labels.length > 0 ? { labels } : {},
268
+ ...milestone !== void 0 ? { milestone } : {}
269
+ });
270
+ for (const comment of record.comments) {
271
+ const who = comment.author ? `**${comment.author}**` : "Someone";
272
+ await client.issues.createComment(repo, created.number, `${who} commented on ${comment.createdAt}:\n\n${comment.body}`);
273
+ }
274
+ if (record.closed) await client.issues.updateIssue(repo, created.number, {
275
+ state: "closed",
276
+ state_reason: "completed"
277
+ });
278
+ results.push({
279
+ source: record.key,
280
+ key: `#${created.number}`,
281
+ created: true,
282
+ dropped
283
+ });
284
+ }
285
+ return results;
286
+ }
287
+ function withProvenance(record) {
288
+ return `${record.body ? `${record.body}\n\n` : ""}---\n_Migrated from [${record.key}](${record.url}); created ${record.createdAt}._\n<!-- holocron:migrated-from ${record.url} -->`;
289
+ }
290
+ function parseNumber(key) {
291
+ const n = key.trim().replace(/^.*#/, "");
292
+ if (!/^\d+$/.test(n)) throw new Error(`Invalid GitHub issue key "${key}" — expected #N or N.`);
293
+ return parseInt(n, 10);
294
+ }
295
+ async function listAll(client, repo, extra) {
296
+ const all = [];
297
+ for (let page = 1;; page++) {
298
+ const batch = await client.issues.listIssues(repo, {
299
+ state: "all",
300
+ per_page: 100,
301
+ page,
302
+ ...extra
303
+ });
304
+ all.push(...batch);
305
+ if (batch.length < 100) return all;
306
+ }
307
+ }
308
+ async function findMilestone(client, repo, ref) {
309
+ return (await client.issues.listMilestones(repo, { state: "all" })).find((m) => String(m.number) === ref || m.title.toLowerCase() === ref.toLowerCase());
310
+ }
311
+ async function ensureMilestone(client, repo, title) {
312
+ return (await findMilestone(client, repo, title))?.number ?? (await client.issues.createMilestone(repo, title)).number;
313
+ }
154
314
  // src/capabilities/issues.ts
155
315
  /** The GitHub implementation of the `issues` capability, backed by GitHub Issues and status labels. */
156
316
  var GitHubIssues = class {
@@ -327,6 +487,14 @@ var GitHubIssues = class {
327
487
  lifecycle
328
488
  };
329
489
  }
490
+ /** Reads issues with their comments, priority label, parent and blockers, for `holocron migrate`. */
491
+ export(selector) {
492
+ return exportIssues(this.client, this.repo, selector);
493
+ }
494
+ /** Makes the records as issues; one already made by an earlier run is skipped. */
495
+ import(records, options) {
496
+ return importIssues(this.client, this.repo, records, options);
497
+ }
330
498
  async resolveMilestone(ref) {
331
499
  if (/^\d+$/.test(ref)) return parseInt(ref, 10);
332
500
  const match = (await this.client.issues.listMilestones(this.repo, { state: "all" })).find((m) => m.title.toLowerCase() === ref.toLowerCase());
@@ -381,6 +549,18 @@ function parseIssueNumber(key, owner, repoName) {
381
549
  const STATUS_FIELD = "Status";
382
550
  /** The name of the field `move` carries over besides the status. */
383
551
  const PRIORITY_FIELD = "Priority";
552
+ const P_LEVELS = {
553
+ p0: "urgent",
554
+ p1: "high",
555
+ p2: "medium",
556
+ p3: "low"
557
+ };
558
+ /** The neutral priority a `Priority` option stands for, by its name; undefined for a name that is not one of them. */
559
+ function priorityOf(value) {
560
+ const name = value?.trim().toLowerCase();
561
+ if (name === "urgent" || name === "high" || name === "medium" || name === "low") return name;
562
+ return name ? P_LEVELS[name] : void 0;
563
+ }
384
564
  const PROJECT_FIELDS = "id number title closed url";
385
565
  const BOARD_QUERY = `query($id: ID!) { node(id: $id) { ... on ProjectV2 {
386
566
  ${PROJECT_FIELDS}
@@ -693,13 +873,15 @@ var GitHubProjects = class {
693
873
  return (await this.fetchItems(board.id)).map((i) => {
694
874
  const status = i.values[STATUS_FIELD];
695
875
  const category = status ? categories.get(status) : void 0;
876
+ const priority = priorityOf(i.values[PRIORITY_FIELD]);
696
877
  return {
697
878
  key: this.keyOf(i.repo, i.number),
698
879
  title: i.title,
699
880
  open: i.open,
700
881
  ...status ? { status } : {},
701
882
  ...category ? { category } : {},
702
- blockedBy: i.blockers.map((b) => this.keyOf(b.repo, b.number))
883
+ blockedBy: i.blockers.map((b) => this.keyOf(b.repo, b.number)),
884
+ ...priority ? { priority } : {}
703
885
  };
704
886
  });
705
887
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-github",
3
- "version": "5.0.0-alpha.369",
3
+ "version": "5.0.0-alpha.370",
4
4
  "description": "Holocron plugin for GitHub. Implements source, ci, secrets, environments, and issues capabilities against the GitHub REST API.",
5
5
  "keywords": [
6
6
  "ci",
@@ -35,30 +35,30 @@
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^26",
38
- "@vitest/coverage-v8": "^5.0.5",
38
+ "@vitest/coverage-v8": "^5.1.0",
39
39
  "@vitest/eslint-plugin": "^1.6.28",
40
40
  "eslint": "^10.14.0",
41
41
  "eslint-plugin-n": "^18.5.0",
42
42
  "globals": "^17.14.0",
43
- "tsdown": "^0.23.4",
43
+ "tsdown": "^0.23.5",
44
44
  "tsx": "4.23.15",
45
45
  "typescript": "^5.9.3",
46
- "vitest": "^5.0.5",
47
- "@theholocron/cli": "5.0.0-alpha.369",
46
+ "vitest": "^5.1.0",
47
+ "@theholocron/cli": "5.0.0-alpha.370",
48
48
  "@theholocron/eslint-config": "^8.9.0-alpha.11",
49
49
  "@theholocron/fs-utils": "^1.17.0-alpha.2",
50
- "@theholocron/github-client": "^1.58.0-alpha.16",
50
+ "@theholocron/github-client": "^1.58.0-alpha.17",
51
51
  "@theholocron/misc-utils": "^1.17.0-alpha.2",
52
52
  "@theholocron/tsconfig": "^8.9.0-alpha.11",
53
- "@theholocron/http-client": "^1.58.0-alpha.16",
54
53
  "@theholocron/tsdown-config": "^8.9.0-alpha.11",
54
+ "@theholocron/http-client": "^1.58.0-alpha.17",
55
55
  "@theholocron/vitest-config": "^8.9.0-alpha.11"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@theholocron/fs-utils": "^1.16.0",
59
59
  "@theholocron/github-client": "^1.58.0",
60
60
  "@theholocron/misc-utils": "^1.16.0",
61
- "@theholocron/cli": "5.0.0-alpha.369"
61
+ "@theholocron/cli": "5.0.0-alpha.370"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=22"