@theholocron/holocron-plugin-github 2.0.0-alpha.45 → 2.0.0-alpha.46

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,15 +1,9 @@
1
- import { Auth, AuthError, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, RepoRef, RepoSettings, ResolveTokenInput, RestClient, RestClient as RestClient$1, Ruleset, SecretScope, Secrets, Source, TrackerDoctorReport, TrackerUser } from "@theholocron/cli";
1
+ import { GitHubClient, createGitHubClient } from "@theholocron/github-client";
2
+ import { Auth, AuthError, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, TrackerDoctorReport, TrackerUser } from "@theholocron/cli";
2
3
 
3
4
  //#region src/auth.d.ts
4
5
  declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
5
6
  //#endregion
6
- //#region src/rest.d.ts
7
- declare function createGitHubRestClient(opts: {
8
- token: string;
9
- baseUrl?: string;
10
- fetch?: typeof fetch;
11
- }): RestClient$1;
12
- //#endregion
13
7
  //#region src/sodium.d.ts
14
8
  /**
15
9
  * Encrypt `value` for the given GitHub-provided base64 public key.
@@ -20,19 +14,18 @@ declare function encryptSecret(publicKeyBase64: string, value: string): Promise<
20
14
  //#region src/capabilities/source.d.ts
21
15
  interface SourceOptions {
22
16
  repo: string;
23
- /** Absolute path to the repo root. Workflow file ops are scoped here. */
24
17
  repoRoot: string;
25
18
  }
26
19
  declare class GitHubSource implements Source {
27
- private readonly rest;
20
+ private readonly client;
28
21
  readonly key: "source";
29
22
  readonly providerName = "github";
30
23
  private readonly owner;
31
24
  private readonly name;
32
- private readonly repoPath;
25
+ private readonly repo;
33
26
  private readonly repoRoot;
34
27
  private readonly workflowDir;
35
- constructor(rest: RestClient, opts: SourceOptions);
28
+ constructor(client: GitHubClient, opts: SourceOptions);
36
29
  whoami(): Promise<{
37
30
  login: string;
38
31
  }>;
@@ -64,16 +57,14 @@ interface SecretsOptions {
64
57
  repo: string;
65
58
  }
66
59
  declare class GitHubSecrets implements Secrets {
67
- private readonly rest;
60
+ private readonly client;
61
+ private readonly opts;
68
62
  readonly key: "secrets";
69
63
  readonly providerName = "github";
70
- private readonly repoBase;
71
- constructor(rest: RestClient, opts: SecretsOptions);
64
+ constructor(client: GitHubClient, opts: SecretsOptions);
72
65
  listSecrets(scope: SecretScope): Promise<string[]>;
73
66
  setSecret(scope: SecretScope, name: string, value: string): Promise<void>;
74
67
  deleteSecret(scope: SecretScope, name: string): Promise<void>;
75
- /** Endpoint root for the given scope. */
76
- private scopeBase;
77
68
  }
78
69
  //#endregion
79
70
  //#region src/capabilities/environments.d.ts
@@ -81,11 +72,11 @@ interface EnvironmentsOptions {
81
72
  repo: string;
82
73
  }
83
74
  declare class GitHubEnvironments implements Environments {
84
- private readonly rest;
75
+ private readonly client;
76
+ private readonly opts;
85
77
  readonly key: "environments";
86
78
  readonly providerName = "github";
87
- private readonly base;
88
- constructor(rest: RestClient, opts: EnvironmentsOptions);
79
+ constructor(client: GitHubClient, opts: EnvironmentsOptions);
89
80
  listEnvironments(): Promise<Environment[]>;
90
81
  upsertEnvironment(env: Environment): Promise<void>;
91
82
  deleteEnvironment(name: string): Promise<void>;
@@ -97,19 +88,13 @@ interface CiOptions {
97
88
  repo: string;
98
89
  }
99
90
  declare class GitHubCi implements Ci {
100
- private readonly rest;
91
+ private readonly client;
92
+ private readonly opts;
101
93
  readonly key: "ci";
102
94
  readonly providerName = "github";
103
- private readonly base;
104
- constructor(rest: RestClient, opts: CiOptions);
95
+ constructor(client: GitHubClient, opts: CiOptions);
105
96
  listRuns(filter?: CiRunFilter): Promise<CiRun[]>;
106
97
  getRun(id: string | number): Promise<CiRun>;
107
- /**
108
- * GitHub reports run state as `status` plus `conclusion` (the latter
109
- * is set once `status === 'completed'`). Holocron's `CiRunStatus`
110
- * collapses these onto one axis — when complete, the conclusion is
111
- * the meaningful value.
112
- */
113
98
  private mapRun;
114
99
  private mapConclusion;
115
100
  }
@@ -121,9 +106,7 @@ interface IssuesOptions {
121
106
  * Lifecycle slot → status label name. Optional at construction; if
122
107
  * omitted, capability methods that need labels (`transition`,
123
108
  * `doctor`) degrade gracefully — `transition` throws a clear error,
124
- * `doctor` reports the two label-backed slots as unresolved. This
125
- * lets the plugin load in configs where the operator hasn't picked
126
- * labels yet without failing the whole runtime.
109
+ * `doctor` reports the two label-backed slots as unresolved.
127
110
  */
128
111
  labels?: {
129
112
  inProgress: string;
@@ -131,14 +114,14 @@ interface IssuesOptions {
131
114
  };
132
115
  }
133
116
  declare class GitHubIssues implements Issues {
134
- private readonly rest;
117
+ private readonly client;
135
118
  readonly key: "issues";
136
119
  readonly providerName = "github";
137
120
  private readonly owner;
138
121
  private readonly repoName;
139
- private readonly base;
122
+ private readonly repo;
140
123
  private readonly labels;
141
- constructor(rest: RestClient, opts: IssuesOptions);
124
+ constructor(client: GitHubClient, opts: IssuesOptions);
142
125
  getMyself(): Promise<TrackerUser>;
143
126
  search(filter: IssueSearchFilter): Promise<Issue[]>;
144
127
  get(key: string): Promise<Issue>;
@@ -172,7 +155,7 @@ interface CanonicalLabel {
172
155
  * - DELETEs labels listed in `stale`.
173
156
  * - Leaves unrecognised labels that aren't in `stale` untouched.
174
157
  */
175
- declare function syncLabels(rest: RestClient, repo: string, canonical: ReadonlyArray<CanonicalLabel>, stale: ReadonlyArray<string>): Promise<string>;
158
+ declare function syncLabels(client: GitHubClient, repo: string, canonical: ReadonlyArray<CanonicalLabel>, stale: ReadonlyArray<string>): Promise<string>;
176
159
  //#endregion
177
160
  //#region src/verify-token.d.ts
178
161
  /**
@@ -219,7 +202,7 @@ interface GitHubPluginOptions extends ResolveTokenInput {
219
202
  }
220
203
  interface PluginContext {
221
204
  options: GitHubPluginOptions;
222
- rest: RestClient;
205
+ client: GitHubClient;
223
206
  repo: string;
224
207
  repoRoot: string;
225
208
  }
@@ -247,4 +230,4 @@ declare function createPlugin(options: GitHubPluginOptions): {
247
230
  */
248
231
  declare const AUTH_HINT: string;
249
232
  //#endregion
250
- export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext, createGitHubRestClient, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, syncLabels, verifyToken };
233
+ export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, syncLabels, verifyToken };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
- import { AuthError, createResolveToken, createRestClient } from "@theholocron/cli";
2
+ import { createGitHubClient, createGitHubClient as createGitHubClient$1 } from "@theholocron/github-client";
3
+ import { AuthError, createResolveToken } from "@theholocron/cli";
3
4
  import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
5
  import { dirname, join } from "node:path";
5
6
  //#region src/auth.ts
@@ -10,50 +11,27 @@ const resolveToken = createResolveToken({
10
11
  errorMessage: "no GitHub token found. Pass --token <PAT>, set HOLOCRON_GH_TOKEN / GITHUB_TOKEN, or run: holocron auth set github <PAT>"
11
12
  });
12
13
  //#endregion
13
- //#region src/repo.ts
14
- var RepoError = class extends Error {
15
- name = "RepoError";
16
- };
17
- function parseRepo(repo) {
18
- if (!repo) throw new RepoError("repo is required; pass { repo: \"owner/name\" } in plugin options");
19
- const [owner, name] = repo.split("/");
20
- if (!owner || !name) throw new RepoError(`invalid repo "${repo}" — expected "owner/name"`);
21
- return {
22
- owner,
23
- name
24
- };
25
- }
26
- //#endregion
27
14
  //#region src/capabilities/ci.ts
28
15
  var GitHubCi = class {
29
- rest;
16
+ client;
17
+ opts;
30
18
  key = "ci";
31
19
  providerName = "github";
32
- base;
33
- constructor(rest, opts) {
34
- this.rest = rest;
35
- const { owner, name } = parseRepo(opts.repo);
36
- this.base = `/repos/${owner}/${name}/actions/runs`;
20
+ constructor(client, opts) {
21
+ this.client = client;
22
+ this.opts = opts;
37
23
  }
38
24
  async listRuns(filter) {
39
- const params = new URLSearchParams();
40
- if (filter?.branch) params.set("branch", filter.branch);
41
- if (filter?.limit) params.set("per_page", String(filter.limit));
42
- if (filter?.status) params.set("status", filter.status);
43
- const qs = params.toString();
44
- const path = qs ? `${this.base}?${qs}` : this.base;
45
- return (await this.rest.request(path)).workflow_runs.map((r) => this.mapRun(r));
25
+ return (await this.client.workflows.listRuns(this.opts.repo, {
26
+ branch: filter?.branch,
27
+ limit: filter?.limit,
28
+ status: filter?.status
29
+ })).map((r) => this.mapRun(r));
46
30
  }
47
31
  async getRun(id) {
48
- const raw = await this.rest.request(`${this.base}/${id}`);
32
+ const raw = await this.client.workflows.getRun(this.opts.repo, id);
49
33
  return this.mapRun(raw);
50
34
  }
51
- /**
52
- * GitHub reports run state as `status` plus `conclusion` (the latter
53
- * is set once `status === 'completed'`). Holocron's `CiRunStatus`
54
- * collapses these onto one axis — when complete, the conclusion is
55
- * the meaningful value.
56
- */
57
35
  mapRun(raw) {
58
36
  const status = raw.status === "completed" && raw.conclusion ? this.mapConclusion(raw.conclusion) : raw.status;
59
37
  return {
@@ -80,17 +58,16 @@ var GitHubCi = class {
80
58
  //#endregion
81
59
  //#region src/capabilities/environments.ts
82
60
  var GitHubEnvironments = class {
83
- rest;
61
+ client;
62
+ opts;
84
63
  key = "environments";
85
64
  providerName = "github";
86
- base;
87
- constructor(rest, opts) {
88
- this.rest = rest;
89
- const { owner, name } = parseRepo(opts.repo);
90
- this.base = `/repos/${owner}/${name}/environments`;
65
+ constructor(client, opts) {
66
+ this.client = client;
67
+ this.opts = opts;
91
68
  }
92
69
  async listEnvironments() {
93
- return (await this.rest.request(this.base)).environments.map((e) => ({
70
+ return (await this.client.environments.listEnvironments(this.opts.repo)).map((e) => ({
94
71
  name: e.name,
95
72
  waitTimer: e.wait_timer,
96
73
  preventSelfReview: e.prevent_self_review,
@@ -105,16 +82,10 @@ var GitHubEnvironments = class {
105
82
  type: r.type,
106
83
  id: r.id
107
84
  }));
108
- await this.rest.request(`${this.base}/${encodeURIComponent(env.name)}`, {
109
- method: "PUT",
110
- body
111
- });
85
+ await this.client.environments.upsertEnvironment(this.opts.repo, env.name, body);
112
86
  }
113
87
  async deleteEnvironment(name) {
114
- await this.rest.request(`${this.base}/${encodeURIComponent(name)}`, {
115
- method: "DELETE",
116
- expectNoContent: true
117
- });
88
+ await this.client.environments.deleteEnvironment(this.opts.repo, name);
118
89
  }
119
90
  flattenReviewers(rules) {
120
91
  if (!rules) return void 0;
@@ -129,41 +100,51 @@ var GitHubEnvironments = class {
129
100
  //#endregion
130
101
  //#region src/capabilities/issues.ts
131
102
  var GitHubIssues = class {
132
- rest;
103
+ client;
133
104
  key = "issues";
134
105
  providerName = "github";
135
106
  owner;
136
107
  repoName;
137
- base;
108
+ repo;
138
109
  labels;
139
- constructor(rest, opts) {
140
- this.rest = rest;
141
- const { owner, name } = parseRepo(opts.repo);
110
+ constructor(client, opts) {
111
+ this.client = client;
112
+ const [owner = "", repoName = ""] = opts.repo.split("/", 2);
142
113
  this.owner = owner;
143
- this.repoName = name;
144
- this.base = `/repos/${owner}/${name}`;
114
+ this.repoName = repoName;
115
+ this.repo = opts.repo;
145
116
  this.labels = opts.labels;
146
117
  }
147
118
  async getMyself() {
148
- return mapUser(await this.rest.request("/user"));
119
+ const raw = await this.client.user.getCurrentUser();
120
+ return {
121
+ id: raw.login,
122
+ displayName: raw.name ?? raw.login,
123
+ ...raw.email ? { emailAddress: raw.email } : {}
124
+ };
149
125
  }
150
126
  async search(filter) {
151
- const params = new URLSearchParams({
127
+ const params = {
152
128
  state: filter.openOnly ? "open" : "all",
153
129
  sort: "updated",
154
130
  direction: "desc",
155
- per_page: String(filter.limit ?? 50),
131
+ per_page: filter.limit ?? 50,
156
132
  filter: "all"
157
- });
158
- if (filter.assignee === "currentUser") {
159
- const me = await this.getMyself();
160
- params.set("assignee", me.id);
161
- } else if (filter.assignee) params.set("assignee", filter.assignee);
162
- return (await this.rest.request(`${this.base}/issues?${params.toString()}`)).filter((i) => !i.pull_request).map((i) => this.mapIssue(i));
133
+ };
134
+ if (filter.assignee === "currentUser") params.assignee = (await this.getMyself()).id;
135
+ else if (filter.assignee) params.assignee = filter.assignee;
136
+ return (await this.client.issues.listIssues(this.repo, {
137
+ state: params.state,
138
+ sort: params.sort,
139
+ direction: params.direction,
140
+ per_page: Number(params.per_page),
141
+ filter: params.filter,
142
+ assignee: params.assignee
143
+ })).filter((i) => !i.pull_request).map((i) => this.mapIssue(i));
163
144
  }
164
145
  async get(key) {
165
146
  const number = parseIssueNumber(key, this.owner, this.repoName);
166
- const raw = await this.rest.request(`${this.base}/issues/${number}`);
147
+ const raw = await this.client.issues.getIssue(this.repo, number);
167
148
  return this.mapIssue(raw);
168
149
  }
169
150
  async create(input) {
@@ -171,14 +152,11 @@ var GitHubIssues = class {
171
152
  if (input.body) body.body = input.body;
172
153
  if (input.labels?.length) body.labels = input.labels;
173
154
  if (input.milestone) body.milestone = await this.resolveMilestone(input.milestone);
174
- return { key: `#${(await this.rest.request(`${this.base}/issues`, {
175
- method: "POST",
176
- body
177
- })).number}` };
155
+ return { key: `#${(await this.client.issues.createIssue(this.repo, body)).number}` };
178
156
  }
179
157
  async transition(key, slot) {
180
158
  const number = parseIssueNumber(key, this.owner, this.repoName);
181
- const issue = await this.rest.request(`${this.base}/issues/${number}`);
159
+ const issue = await this.client.issues.getIssue(this.repo, number);
182
160
  const currentStatusLabels = issue.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
183
161
  if (slot === "done") {
184
162
  if (issue.state === "closed") return {
@@ -187,12 +165,9 @@ var GitHubIssues = class {
187
165
  via: "already closed"
188
166
  };
189
167
  await this.removeStatusLabels(number, currentStatusLabels);
190
- await this.rest.request(`${this.base}/issues/${number}`, {
191
- method: "PATCH",
192
- body: {
193
- state: "closed",
194
- state_reason: "completed"
195
- }
168
+ await this.client.issues.updateIssue(this.repo, number, {
169
+ state: "closed",
170
+ state_reason: "completed"
196
171
  });
197
172
  return {
198
173
  transitioned: true,
@@ -209,16 +184,10 @@ var GitHubIssues = class {
209
184
  status: `open + ${targetLabel}`,
210
185
  via: "already there"
211
186
  };
212
- if (!alreadyOpen) await this.rest.request(`${this.base}/issues/${number}`, {
213
- method: "PATCH",
214
- body: { state: "open" }
215
- });
187
+ if (!alreadyOpen) await this.client.issues.updateIssue(this.repo, number, { state: "open" });
216
188
  const toRemove = currentStatusLabels.filter((n) => n !== targetLabel);
217
189
  if (toRemove.length) await this.removeStatusLabels(number, toRemove);
218
- if (!alreadyLabeled) await this.rest.request(`${this.base}/issues/${number}/labels`, {
219
- method: "POST",
220
- body: { labels: [targetLabel] }
221
- });
190
+ if (!alreadyLabeled) await this.client.issues.addLabels(this.repo, number, [targetLabel]);
222
191
  return {
223
192
  transitioned: true,
224
193
  status: `open + ${targetLabel}`,
@@ -227,15 +196,12 @@ var GitHubIssues = class {
227
196
  }
228
197
  async comment(key, body) {
229
198
  const number = parseIssueNumber(key, this.owner, this.repoName);
230
- await this.rest.request(`${this.base}/issues/${number}/comments`, {
231
- method: "POST",
232
- body: { body }
233
- });
199
+ await this.client.issues.createComment(this.repo, number, body);
234
200
  }
235
201
  async doctor() {
236
202
  const me = await this.getMyself();
237
- const repo = await this.rest.request(this.base);
238
- const allLabels = await this.rest.request(`${this.base}/labels?per_page=100`);
203
+ const repo = await this.client.repos.getRepo(this.repo);
204
+ const allLabels = await this.client.labels.listLabels(this.repo);
239
205
  const labelNames = new Set(allLabels.map((l) => l.name));
240
206
  const statuses = [
241
207
  {
@@ -302,12 +268,12 @@ var GitHubIssues = class {
302
268
  }
303
269
  async resolveMilestone(ref) {
304
270
  if (/^\d+$/.test(ref)) return parseInt(ref, 10);
305
- const match = (await this.rest.request(`${this.base}/milestones?state=all&per_page=100`)).find((m) => m.title.toLowerCase() === ref.toLowerCase());
271
+ const match = (await this.client.issues.listMilestones(this.repo, { state: "all" })).find((m) => m.title.toLowerCase() === ref.toLowerCase());
306
272
  if (!match) throw new Error(`Milestone "${ref}" not found in ${this.owner}/${this.repoName}. Use the numeric id or an exact title.`);
307
273
  return match.number;
308
274
  }
309
275
  async removeStatusLabels(number, labels) {
310
- for (const label of labels) await this.rest.request(`${this.base}/issues/${number}/labels/${encodeURIComponent(label)}`, { method: "DELETE" });
276
+ for (const label of labels) await this.client.issues.removeLabel(this.repo, number, label);
311
277
  }
312
278
  mapIssue(raw) {
313
279
  const statusLabels = raw.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
@@ -328,17 +294,16 @@ var GitHubIssues = class {
328
294
  ...raw.body ? { body: raw.body } : {},
329
295
  status: statusName,
330
296
  statusCategory: category,
331
- assignee: raw.assignee ? mapUser(raw.assignee) : null,
297
+ assignee: raw.assignee ? {
298
+ id: raw.assignee.login,
299
+ displayName: raw.assignee.name ?? raw.assignee.login,
300
+ ...raw.assignee.email ? { emailAddress: raw.assignee.email } : {}
301
+ } : null,
332
302
  updated: raw.updated_at,
333
303
  url: raw.html_url
334
304
  };
335
305
  }
336
306
  };
337
- /**
338
- * Accept "#42", "42", or "owner/repo#42" and return the numeric id.
339
- * Cross-repo refs are allowed in input but the adapter is bound to one
340
- * repo — we error if the owner/repo doesn't match.
341
- */
342
307
  function parseIssueNumber(key, owner, repoName) {
343
308
  const trimmed = key.trim();
344
309
  const crossMatch = trimmed.match(/^([^/]+)\/([^#]+)#(\d+)$/);
@@ -350,13 +315,6 @@ function parseIssueNumber(key, owner, repoName) {
350
315
  if (!/^\d+$/.test(num)) throw new Error(`Invalid GitHub issue key "${key}" — expected #N or N.`);
351
316
  return parseInt(num, 10);
352
317
  }
353
- function mapUser(raw) {
354
- return {
355
- id: raw.login,
356
- displayName: raw.name ?? raw.login,
357
- ...raw.email ? { emailAddress: raw.email } : {}
358
- };
359
- }
360
318
  //#endregion
361
319
  //#region src/sodium.ts
362
320
  /**
@@ -392,49 +350,38 @@ async function encryptSecret(publicKeyBase64, value) {
392
350
  //#endregion
393
351
  //#region src/capabilities/secrets.ts
394
352
  var GitHubSecrets = class {
395
- rest;
353
+ client;
354
+ opts;
396
355
  key = "secrets";
397
356
  providerName = "github";
398
- repoBase;
399
- constructor(rest, opts) {
400
- this.rest = rest;
401
- const { owner, name } = parseRepo(opts.repo);
402
- this.repoBase = `/repos/${owner}/${name}`;
357
+ constructor(client, opts) {
358
+ this.client = client;
359
+ this.opts = opts;
403
360
  }
404
361
  async listSecrets(scope) {
405
- const base = this.scopeBase(scope);
406
- return (await this.rest.request(`${base}/secrets`)).secrets.map((s) => s.name);
362
+ return this.client.secrets.listSecrets(this.opts.repo, toGHScope(scope));
407
363
  }
408
364
  async setSecret(scope, name, value) {
409
- const base = this.scopeBase(scope);
410
- const pk = await this.rest.request(`${base}/secrets/public-key`);
365
+ const ghScope = toGHScope(scope);
366
+ const pk = await this.client.secrets.getPublicKey(this.opts.repo, ghScope);
411
367
  const body = {
412
368
  encrypted_value: await encryptSecret(pk.key, value),
413
369
  key_id: pk.key_id
414
370
  };
415
371
  if (scope.kind === "organization") body.visibility = "all";
416
- await this.rest.request(`${base}/secrets/${name}`, {
417
- method: "PUT",
418
- body,
419
- expectNoContent: true
420
- });
372
+ await this.client.secrets.putSecret(this.opts.repo, ghScope, name, body);
421
373
  }
422
374
  async deleteSecret(scope, name) {
423
- const base = this.scopeBase(scope);
424
- await this.rest.request(`${base}/secrets/${name}`, {
425
- method: "DELETE",
426
- expectNoContent: true
427
- });
428
- }
429
- /** Endpoint root for the given scope. */
430
- scopeBase(scope) {
431
- switch (scope.kind) {
432
- case "repo": return `${this.repoBase}/actions`;
433
- case "environment": return `${this.repoBase}/environments/${scope.name}`;
434
- case "organization": return `/orgs/${scope.name}/actions`;
435
- }
375
+ await this.client.secrets.deleteSecret(this.opts.repo, toGHScope(scope), name);
436
376
  }
437
377
  };
378
+ function toGHScope(scope) {
379
+ if (scope.kind === "organization") return {
380
+ kind: "organization",
381
+ org: scope.name
382
+ };
383
+ return scope;
384
+ }
438
385
  //#endregion
439
386
  //#region src/capabilities/labels.ts
440
387
  /**
@@ -445,10 +392,8 @@ var GitHubSecrets = class {
445
392
  * - DELETEs labels listed in `stale`.
446
393
  * - Leaves unrecognised labels that aren't in `stale` untouched.
447
394
  */
448
- async function syncLabels(rest, repo, canonical, stale) {
449
- const { owner, name } = parseRepo(repo);
450
- const base = `/repos/${owner}/${name}`;
451
- const existing = await rest.request(`${base}/labels?per_page=100`);
395
+ async function syncLabels(client, repo, canonical, stale) {
396
+ const existing = await client.labels.listLabels(repo);
452
397
  const existingMap = new Map(existing.map((l) => [l.name.toLowerCase(), l]));
453
398
  let created = 0;
454
399
  let updated = 0;
@@ -456,103 +401,63 @@ async function syncLabels(rest, repo, canonical, stale) {
456
401
  for (const label of canonical) {
457
402
  const current = existingMap.get(label.name);
458
403
  if (!current) {
459
- await rest.request(`${base}/labels`, {
460
- method: "POST",
461
- body: {
462
- name: label.name,
463
- color: label.color,
464
- description: label.description
465
- }
404
+ await client.labels.createLabel(repo, {
405
+ name: label.name,
406
+ color: label.color,
407
+ description: label.description
466
408
  });
467
409
  created++;
468
410
  } else if (current.color !== label.color || (current.description ?? "") !== label.description) {
469
- await rest.request(`${base}/labels/${encodeURIComponent(label.name)}`, {
470
- method: "PATCH",
471
- body: {
472
- color: label.color,
473
- description: label.description
474
- }
411
+ await client.labels.updateLabel(repo, label.name, {
412
+ color: label.color,
413
+ description: label.description
475
414
  });
476
415
  updated++;
477
416
  }
478
417
  }
479
418
  for (const staleName of stale) if (existingMap.has(staleName)) {
480
- await rest.request(`${base}/labels/${encodeURIComponent(staleName)}`, { method: "DELETE" });
419
+ await client.labels.deleteLabel(repo, staleName);
481
420
  deleted++;
482
421
  }
483
422
  return `${created} created, ${updated} updated, ${deleted} deleted`;
484
423
  }
485
424
  //#endregion
486
425
  //#region src/capabilities/properties.ts
487
- /**
488
- * Sync custom property values for a repo to the org-defined schema.
489
- *
490
- * Calls `PATCH /repos/{owner}/{name}/properties/values` with the full
491
- * map — GitHub replaces only the supplied keys, leaving others untouched.
492
- */
493
- async function syncProperties(rest, repo, values) {
494
- const { owner, name } = parseRepo(repo);
495
- const properties = Object.entries(values).map(([property_name, value]) => ({
496
- property_name,
497
- value
498
- }));
499
- await rest.request(`/repos/${owner}/${name}/properties/values`, {
500
- method: "PATCH",
501
- body: { properties }
502
- });
503
- return `${properties.length} properties set`;
426
+ async function syncProperties(client, repo, values) {
427
+ await client.properties.setProperties(repo, values);
428
+ return `${Object.keys(values).length} properties set`;
504
429
  }
505
430
  //#endregion
506
431
  //#region src/capabilities/topics.ts
507
- /**
508
- * Replace a repo's topic set with the supplied list.
509
- *
510
- * Calls `PUT /repos/{owner}/{name}/topics` — GitHub replaces the full
511
- * set atomically, so the config is always the source of truth.
512
- */
513
- async function syncTopics(rest, repo, topics) {
514
- const { owner, name } = parseRepo(repo);
515
- await rest.request(`/repos/${owner}/${name}/topics`, {
516
- method: "PUT",
517
- body: { names: topics }
518
- });
432
+ async function syncTopics(client, repo, topics) {
433
+ await client.topics.setTopics(repo, topics);
519
434
  return `${topics.length} topics set`;
520
435
  }
521
436
  //#endregion
522
437
  //#region src/capabilities/source.ts
523
- /**
524
- * `source` capability for GitHub.
525
- *
526
- * Ported from rando-id/rando.id `packages/cli/src/adapters/gh-rest.ts`
527
- * with two changes for v2:
528
- *
529
- * - Repo coords are bound at construction time (not passed per call)
530
- * - Workflow files are local-fs operations (not GH API) — they live
531
- * in the consumer's `.github/workflows/` directory
532
- */
533
438
  var GitHubSource = class {
534
- rest;
439
+ client;
535
440
  key = "source";
536
441
  providerName = "github";
537
442
  owner;
538
443
  name;
539
- repoPath;
444
+ repo;
540
445
  repoRoot;
541
446
  workflowDir;
542
- constructor(rest, opts) {
543
- this.rest = rest;
544
- const { owner, name } = parseRepo(opts.repo);
447
+ constructor(client, opts) {
448
+ this.client = client;
449
+ const [owner = "", name = ""] = opts.repo.split("/", 2);
545
450
  this.owner = owner;
546
451
  this.name = name;
547
- this.repoPath = `/repos/${owner}/${name}`;
452
+ this.repo = opts.repo;
548
453
  this.repoRoot = opts.repoRoot;
549
454
  this.workflowDir = join(opts.repoRoot, ".github", "workflows");
550
455
  }
551
456
  async whoami() {
552
- return { login: (await this.rest.request("/user")).login };
457
+ return { login: (await this.client.user.getCurrentUser()).login };
553
458
  }
554
459
  async getRepo() {
555
- const data = await this.rest.request(this.repoPath);
460
+ const data = await this.client.repos.getRepo(this.repo);
556
461
  return {
557
462
  owner: this.owner,
558
463
  name: this.name,
@@ -560,85 +465,40 @@ var GitHubSource = class {
560
465
  };
561
466
  }
562
467
  async listRulesets() {
563
- return this.rest.request(`${this.repoPath}/rulesets`);
468
+ return this.client.rulesets.listRulesets(this.repo);
564
469
  }
565
470
  async createRuleset(payload) {
566
- return this.rest.request(`${this.repoPath}/rulesets`, {
567
- method: "POST",
568
- body: payload
569
- });
471
+ return this.client.rulesets.createRuleset(this.repo, payload);
570
472
  }
571
473
  async updateRuleset(id, payload) {
572
- return this.rest.request(`${this.repoPath}/rulesets/${id}`, {
573
- method: "PUT",
574
- body: payload
575
- });
474
+ return this.client.rulesets.updateRuleset(this.repo, id, payload);
576
475
  }
577
476
  async updateRepoSettings(settings) {
578
- await this.rest.request(this.repoPath, {
579
- method: "PATCH",
580
- body: settings
581
- });
477
+ await this.client.repos.updateRepo(this.repo, settings);
582
478
  }
583
479
  async protectBranch(branch, payload) {
584
- await this.rest.request(`${this.repoPath}/branches/${encodeURIComponent(branch)}/protection`, {
585
- method: "PUT",
586
- body: payload
587
- });
480
+ await this.client.branches.protectBranch(this.repo, branch, payload);
588
481
  }
589
482
  async enableVulnerabilityAlerts() {
590
- await this.rest.request(`${this.repoPath}/vulnerability-alerts`, {
591
- method: "PUT",
592
- expectNoContent: true
593
- });
483
+ await this.client.security.enableVulnerabilityAlerts(this.repo);
594
484
  }
595
485
  async enableAutomatedSecurityFixes() {
596
- await this.rest.request(`${this.repoPath}/automated-security-fixes`, {
597
- method: "PUT",
598
- expectNoContent: true
599
- });
486
+ await this.client.security.enableAutomatedSecurityFixes(this.repo);
600
487
  }
601
488
  async enableSecretScanning() {
602
- await this.rest.request(this.repoPath, {
603
- method: "PATCH",
604
- body: { security_and_analysis: {
605
- secret_scanning: { status: "enabled" },
606
- secret_scanning_push_protection: { status: "enabled" },
607
- secret_scanning_validity_checks: { status: "enabled" },
608
- secret_scanning_non_provider_patterns: { status: "enabled" }
609
- } }
610
- });
489
+ await this.client.security.enableSecretScanning(this.repo);
611
490
  }
612
491
  async enableCodeScanning() {
613
- return `run ${(await this.rest.request(`${this.repoPath}/code-scanning/default-setup`, {
614
- method: "PATCH",
615
- body: {
616
- state: "configured",
617
- query_suite: "extended",
618
- threat_model: "remote_and_local"
619
- }
620
- })).run_id}`;
492
+ return `run ${(await this.client.security.enableCodeScanning(this.repo)).run_id}`;
621
493
  }
622
494
  async disableDefaultCodeScanning() {
623
- await this.rest.request(`${this.repoPath}/code-scanning/default-setup`, {
624
- method: "PATCH",
625
- body: { state: "not-configured" }
626
- });
495
+ await this.client.security.disableDefaultCodeScanning(this.repo);
627
496
  }
628
497
  async enableDependencyGraph() {
629
- await this.rest.request(this.repoPath, {
630
- method: "PATCH",
631
- body: { security_and_analysis: {
632
- dependency_graph: { status: "enabled" },
633
- dependency_graph_autosubmit_action: { status: "enabled" }
634
- } }
635
- });
498
+ await this.client.security.enableDependencyGraph(this.repo);
636
499
  }
637
500
  async enablePrivateVulnerabilityReporting() {
638
- await this.rest.request(`${this.repoPath}/private-vulnerability-reporting`, {
639
- method: "PUT",
640
- expectNoContent: true
641
- });
501
+ await this.client.security.enablePrivateVulnerabilityReporting(this.repo);
642
502
  }
643
503
  async listWorkflowFiles() {
644
504
  try {
@@ -674,13 +534,13 @@ var GitHubSource = class {
674
534
  await writeFile(full, contents, "utf8");
675
535
  }
676
536
  async syncLabels(canonical, stale) {
677
- return syncLabels(this.rest, `${this.owner}/${this.name}`, canonical, stale);
537
+ return syncLabels(this.client, this.repo, canonical, stale);
678
538
  }
679
539
  async syncProperties(values) {
680
- return syncProperties(this.rest, `${this.owner}/${this.name}`, values);
540
+ return syncProperties(this.client, this.repo, values);
681
541
  }
682
542
  async syncTopics(topics) {
683
- return syncTopics(this.rest, `${this.owner}/${this.name}`, topics);
543
+ return syncTopics(this.client, this.repo, topics);
684
544
  }
685
545
  };
686
546
  function isENOENT(err) {
@@ -698,20 +558,6 @@ async function ensureDir(path) {
698
558
  }
699
559
  }
700
560
  //#endregion
701
- //#region src/rest.ts
702
- function createGitHubRestClient(opts) {
703
- return createRestClient({
704
- baseUrl: opts.baseUrl ?? "https://api.github.com",
705
- token: opts.token,
706
- extraHeaders: {
707
- accept: "application/vnd.github+json",
708
- "x-github-api-version": "2022-11-28"
709
- },
710
- vendor: "GitHub",
711
- fetch: opts.fetch
712
- });
713
- }
714
- //#endregion
715
561
  //#region src/verify-token.ts
716
562
  /**
717
563
  * `verifyToken` — plugin-level export used by `holocron auth set` +
@@ -724,13 +570,13 @@ function createGitHubRestClient(opts) {
724
570
  * what we don't have yet at bootstrap time.
725
571
  */
726
572
  async function verifyToken(token, opts = {}) {
727
- const rest = createGitHubRestClient({
573
+ const client = createGitHubClient({
728
574
  token,
729
575
  baseUrl: opts.baseUrl,
730
576
  fetch: opts.fetch
731
577
  });
732
578
  try {
733
- const me = await rest.request("/user");
579
+ const me = await client.user.getCurrentUser();
734
580
  return {
735
581
  ok: true,
736
582
  subject: `user @ ${me.login ?? me.email ?? "unknown"}`
@@ -747,7 +593,7 @@ async function verifyToken(token, opts = {}) {
747
593
  function createContext(options) {
748
594
  return {
749
595
  options,
750
- rest: createGitHubRestClient({
596
+ client: createGitHubClient$1({
751
597
  token: resolveToken(options),
752
598
  baseUrl: options.baseUrl,
753
599
  fetch: options.fetch
@@ -757,24 +603,24 @@ function createContext(options) {
757
603
  };
758
604
  }
759
605
  function source(ctx) {
760
- return new GitHubSource(ctx.rest, {
606
+ return new GitHubSource(ctx.client, {
761
607
  repo: ctx.repo,
762
608
  repoRoot: ctx.repoRoot
763
609
  });
764
610
  }
765
611
  function secrets(ctx) {
766
- return new GitHubSecrets(ctx.rest, { repo: ctx.repo });
612
+ return new GitHubSecrets(ctx.client, { repo: ctx.repo });
767
613
  }
768
614
  function environments(ctx) {
769
- return new GitHubEnvironments(ctx.rest, { repo: ctx.repo });
615
+ return new GitHubEnvironments(ctx.client, { repo: ctx.repo });
770
616
  }
771
617
  function ci(ctx) {
772
- return new GitHubCi(ctx.rest, { repo: ctx.repo });
618
+ return new GitHubCi(ctx.client, { repo: ctx.repo });
773
619
  }
774
620
  function issues(ctx) {
775
621
  const opts = { repo: ctx.repo };
776
622
  if (ctx.options.labels !== void 0) opts.labels = ctx.options.labels;
777
- return new GitHubIssues(ctx.rest, opts);
623
+ return new GitHubIssues(ctx.client, opts);
778
624
  }
779
625
  function createPlugin(options) {
780
626
  const ctx = createContext(options);
@@ -797,4 +643,4 @@ function createPlugin(options) {
797
643
  */
798
644
  const AUTH_HINT = "generate a Personal Access Token at https://github.com/settings/tokens (scopes: repo, admin:repo_hook — plus admin:org for org-level ops), then run: holocron auth set github <PAT>";
799
645
  //#endregion
800
- export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createContext, createGitHubRestClient, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, syncLabels, verifyToken };
646
+ export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, syncLabels, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-github",
3
- "version": "2.0.0-alpha.45",
3
+ "version": "2.0.0-alpha.46",
4
4
  "description": "Holocron plugin for GitHub. Implements source, ci, secrets, environments, and issues capabilities against the GitHub REST API.",
5
5
  "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-github#readme",
6
6
  "bugs": "https://github.com/theholocron/holocron/issues",
@@ -21,12 +21,14 @@
21
21
  }
22
22
  },
23
23
  "peerDependencies": {
24
- "@theholocron/cli": "2.0.0-alpha.45"
24
+ "@theholocron/github-client": "^0.3.2",
25
+ "@theholocron/cli": "2.0.0-alpha.46"
25
26
  },
26
27
  "dependencies": {
27
28
  "libsodium-wrappers": "^0.7.15"
28
29
  },
29
30
  "devDependencies": {
31
+ "@theholocron/github-client": "^0.3.2",
30
32
  "@types/node": "^26",
31
33
  "@theholocron/eslint-config": "^5.2.0",
32
34
  "@theholocron/tsconfig": "^6.0.0",
@@ -41,7 +43,7 @@
41
43
  "tsx": "^4.22.4",
42
44
  "typescript": "^5.9.3",
43
45
  "vitest": "^4.1.10",
44
- "@theholocron/cli": "2.0.0-alpha.45"
46
+ "@theholocron/cli": "2.0.0-alpha.46"
45
47
  },
46
48
  "publishConfig": {
47
49
  "access": "public"