@theholocron/holocron-plugin-github 5.0.0-alpha.376 → 5.0.0-alpha.378

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.
Files changed (2) hide show
  1. package/dist/index.mjs +38 -1
  2. package/package.json +6 -6
package/dist/index.mjs CHANGED
@@ -152,6 +152,18 @@ var GitHubEnvironments = class {
152
152
  }
153
153
  };
154
154
  // src/capabilities/issues-migration.ts
155
+ /**
156
+ * Export and import of GitHub issues for `holocron migrate`.
157
+ *
158
+ * GitHub holds less than a planning tool, so what does not fit is named in each result's `dropped` list rather than
159
+ * lost quietly:
160
+ * - priority is a label (`priority: high`), because on GitHub it lives in a project field, not on the issue;
161
+ * - an import cannot backdate an issue or a comment, and cannot post as someone else, so the original date and
162
+ * author go in the text;
163
+ * - an import does not set parent or blocked-by links (they need GraphQL node ids of issues made in the same run);
164
+ * - an assignee is not mapped (a Linear user is not a GitHub login).
165
+ * A migrated issue ends with `<!-- holocron:migrated-from <url> -->`, which is how a second run finds it.
166
+ */
155
167
  const MARKER = /<!-- holocron:migrated-from (\S+) -->/;
156
168
  const PRIORITY_LABEL = /^priority\s*[:/-]\s*(urgent|high|medium|low)$/i;
157
169
  const LINK_QUERY = `query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) {
@@ -177,11 +189,36 @@ function categoryOf(issue) {
177
189
  if (names.some((n) => n === "status:in-progress" || n === "status: in progress")) return "in-progress";
178
190
  return "open";
179
191
  }
192
+ /** How many issues an export fetches at once. GitHub's secondary rate limit punishes bursts of requests, not volume. */
193
+ const EXPORT_CONCURRENCY = 4;
194
+ /** Runs `fn` over `items`, at most `limit` at a time, and returns the results in the order of `items`. */
195
+ async function mapLimit(items, limit, fn) {
196
+ const results = new Array(items.length);
197
+ let next = 0;
198
+ const worker = async () => {
199
+ while (next < items.length) {
200
+ const at = next++;
201
+ results[at] = await fn(items[at]);
202
+ }
203
+ };
204
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
205
+ return results;
206
+ }
207
+ /** GitHub's own answer to a burst of requests (403 or 429 naming the secondary limit) becomes a message that says what to do. */
208
+ function explainSecondaryLimit(err) {
209
+ if (err instanceof ProviderApiError && (err.status === 403 || err.status === 429) && String(err.details).includes("secondary rate limit")) return /* @__PURE__ */ new Error("GitHub's secondary rate limit stopped the export. It limits bursts of requests, not the hourly quota, and nothing has been written. Wait a couple of minutes and run the command again.");
210
+ return err;
211
+ }
180
212
  /** Reads the issues `selector` names, with their comments, priority label, parent and blockers. */
181
213
  async function exportIssues(client, repo, selector) {
214
+ return readIssues(client, repo, selector).catch((err) => {
215
+ throw explainSecondaryLimit(err);
216
+ });
217
+ }
218
+ async function readIssues(client, repo, selector) {
182
219
  const [owner = "", name = ""] = repo.split("/", 2);
183
220
  let raw;
184
- if (selector.keys) raw = await Promise.all(selector.keys.map((k) => client.issues.getIssue(repo, parseNumber(k))));
221
+ if (selector.keys) raw = await mapLimit(selector.keys, EXPORT_CONCURRENCY, (k) => client.issues.getIssue(repo, parseNumber(k)));
185
222
  else if (selector.milestone) {
186
223
  const milestone = await findMilestone(client, repo, selector.milestone);
187
224
  if (!milestone) throw new Error(`Milestone "${selector.milestone}" not found in ${repo}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-github",
3
- "version": "5.0.0-alpha.376",
3
+ "version": "5.0.0-alpha.378",
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",
@@ -44,21 +44,21 @@
44
44
  "tsx": "4.23.15",
45
45
  "typescript": "^5.9.3",
46
46
  "vitest": "^5.1.0",
47
- "@theholocron/cli": "5.0.0-alpha.376",
48
- "@theholocron/eslint-config": "^8.9.0-alpha.12",
49
47
  "@theholocron/fs-utils": "^1.17.0-alpha.2",
48
+ "@theholocron/eslint-config": "^8.9.0-alpha.12",
50
49
  "@theholocron/github-client": "^1.58.0-alpha.22",
51
- "@theholocron/misc-utils": "^1.17.0-alpha.2",
52
50
  "@theholocron/http-client": "^1.58.0-alpha.22",
51
+ "@theholocron/misc-utils": "^1.17.0-alpha.2",
53
52
  "@theholocron/tsdown-config": "^8.9.0-alpha.12",
54
53
  "@theholocron/vitest-config": "^8.9.0-alpha.12",
55
- "@theholocron/tsconfig": "^8.9.0-alpha.12"
54
+ "@theholocron/tsconfig": "^8.9.0-alpha.12",
55
+ "@theholocron/cli": "5.0.0-alpha.378"
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.376"
61
+ "@theholocron/cli": "5.0.0-alpha.378"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=22"