@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 +5 -1
- package/dist/index.mjs +183 -1
- package/package.json +8 -8
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.
|
|
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
|
|
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.
|
|
43
|
+
"tsdown": "^0.23.5",
|
|
44
44
|
"tsx": "4.23.15",
|
|
45
45
|
"typescript": "^5.9.3",
|
|
46
|
-
"vitest": "^5.0
|
|
47
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
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.
|
|
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.
|
|
61
|
+
"@theholocron/cli": "5.0.0-alpha.370"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">=22"
|