@theholocron/holocron-plugin-github 2.0.0-alpha.1 → 2.0.0-alpha.11

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
@@ -4,10 +4,13 @@ import { Auth, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSe
4
4
  /**
5
5
  * Token resolution for the GitHub plugin. See README §Auth.
6
6
  *
7
- * Resolution order:
7
+ * Resolution order (matches the standard 4-step precedence set by
8
+ * `.notes/tech-auth-bootstrap.spec.md`):
8
9
  * 1. explicit `token` argument (from `--token` flag)
9
10
  * 2. HOLOCRON_GH_TOKEN env var (preferred over GITHUB_TOKEN — clearer intent)
10
11
  * 3. GITHUB_TOKEN env var (auto-injected in GH Actions)
12
+ * 4. keyring (com.theholocron.cli / "github")
13
+ * 5. AuthError naming all four options + the bootstrap hint
11
14
  *
12
15
  * No `gh auth token` fallback by design — it usually has narrower
13
16
  * scopes than admin commands need.
@@ -20,6 +23,8 @@ interface ResolveTokenInput {
20
23
  cliToken?: string;
21
24
  /** Env vars; passed in for testability. Defaults to `process.env`. */
22
25
  env?: NodeJS.ProcessEnv;
26
+ /** Keyring lookup fn; passed in for testability. Defaults to `getToken(provider)`. */
27
+ keyring?: (provider: string) => string | null;
23
28
  }
24
29
  declare function resolveToken(input?: ResolveTokenInput): string;
25
30
  //#endregion
@@ -73,6 +78,7 @@ declare class GitHubSource implements Source {
73
78
  private readonly owner;
74
79
  private readonly name;
75
80
  private readonly repoPath;
81
+ private readonly repoRoot;
76
82
  private readonly workflowDir;
77
83
  constructor(rest: GitHubRestClient, opts: SourceOptions);
78
84
  whoami(): Promise<{
@@ -83,14 +89,18 @@ declare class GitHubSource implements Source {
83
89
  createRuleset(payload: Record<string, unknown>): Promise<Ruleset>;
84
90
  updateRuleset(id: number, payload: Record<string, unknown>): Promise<Ruleset>;
85
91
  updateRepoSettings(settings: RepoSettings): Promise<void>;
92
+ protectBranch(branch: string, payload: Record<string, unknown>): Promise<void>;
86
93
  enableVulnerabilityAlerts(): Promise<void>;
87
94
  enableAutomatedSecurityFixes(): Promise<void>;
88
95
  enableSecretScanning(): Promise<void>;
96
+ enableCodeScanning(): Promise<string>;
97
+ enableDependencyGraph(): Promise<void>;
89
98
  enablePrivateVulnerabilityReporting(): Promise<void>;
90
99
  listWorkflowFiles(): Promise<string[]>;
91
100
  readWorkflowFile(name: string): Promise<string | null>;
92
101
  writeWorkflowFile(name: string, contents: string): Promise<void>;
93
102
  removeWorkflowFile(name: string): Promise<void>;
103
+ writeRepoFile(path: string, contents: string): Promise<void>;
94
104
  }
95
105
  //#endregion
96
106
  //#region src/capabilities/secrets.d.ts
@@ -151,8 +161,15 @@ declare class GitHubCi implements Ci {
151
161
  //#region src/capabilities/issues.d.ts
152
162
  interface IssuesOptions {
153
163
  repo: string;
154
- /** Lifecycle slot → status label name. */
155
- labels: {
164
+ /**
165
+ * Lifecycle slot → status label name. Optional at construction; if
166
+ * omitted, capability methods that need labels (`transition`,
167
+ * `doctor`) degrade gracefully — `transition` throws a clear error,
168
+ * `doctor` reports the two label-backed slots as unresolved. This
169
+ * lets the plugin load in configs where the operator hasn't picked
170
+ * labels yet without failing the whole runtime.
171
+ */
172
+ labels?: {
156
173
  inProgress: string;
157
174
  inReview: string;
158
175
  };
@@ -185,6 +202,32 @@ declare class GitHubIssues implements Issues {
185
202
  private mapIssue;
186
203
  }
187
204
  //#endregion
205
+ //#region src/verify-token.d.ts
206
+ /**
207
+ * `verifyToken` — plugin-level export used by `holocron auth set` +
208
+ * `holocron auth check`. Hits `GET /user` and translates the response
209
+ * into the normalized `VerifyTokenResult` shape.
210
+ *
211
+ * Kept as a standalone function (not a capability method) so the auth
212
+ * command can call it without initializing the full plugin — plugin
213
+ * construction requires an already-resolved token, which is exactly
214
+ * what we don't have yet at bootstrap time.
215
+ */
216
+ interface VerifyTokenSuccess {
217
+ ok: true;
218
+ subject: string;
219
+ }
220
+ interface VerifyTokenFailure {
221
+ ok: false;
222
+ message: string;
223
+ }
224
+ type VerifyTokenResult = VerifyTokenSuccess | VerifyTokenFailure;
225
+ interface VerifyTokenOptions {
226
+ baseUrl?: string;
227
+ fetch?: typeof fetch;
228
+ }
229
+ declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult>;
230
+ //#endregion
188
231
  //#region src/index.d.ts
189
232
  interface GitHubPluginOptions extends ResolveTokenInput {
190
233
  /** "owner/name" — e.g., "theholocron/holocron". Required. */
@@ -224,5 +267,12 @@ declare function createPlugin(options: GitHubPluginOptions): {
224
267
  issues: () => Issues;
225
268
  };
226
269
  };
270
+ /**
271
+ * One-line hint printed by `holocron auth set github` when no token
272
+ * is supplied or the supplied token is rejected. Generate a PAT at
273
+ * https://github.com/settings/tokens with `repo` + `admin:repo_hook`
274
+ * scopes (add `admin:org` for org-level operations).
275
+ */
276
+ declare const AUTH_HINT: string;
227
277
  //#endregion
228
- export { type Auth, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubRestClient, GitHubSecrets, GitHubSource, PluginContext, ResolveTokenInput, ci, createContext, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source };
278
+ export { AUTH_HINT, type Auth, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubRestClient, GitHubSecrets, GitHubSource, PluginContext, ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, verifyToken };
package/dist/index.mjs CHANGED
@@ -1,15 +1,18 @@
1
1
  import { createRequire } from "node:module";
2
+ import { ProviderApiError, getToken } from "@theholocron/cli";
2
3
  import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { ProviderApiError } from "@theholocron/cli";
4
+ import { dirname, join } from "node:path";
5
5
  //#region src/auth.ts
6
6
  /**
7
7
  * Token resolution for the GitHub plugin. See README §Auth.
8
8
  *
9
- * Resolution order:
9
+ * Resolution order (matches the standard 4-step precedence set by
10
+ * `.notes/tech-auth-bootstrap.spec.md`):
10
11
  * 1. explicit `token` argument (from `--token` flag)
11
12
  * 2. HOLOCRON_GH_TOKEN env var (preferred over GITHUB_TOKEN — clearer intent)
12
13
  * 3. GITHUB_TOKEN env var (auto-injected in GH Actions)
14
+ * 4. keyring (com.theholocron.cli / "github")
15
+ * 5. AuthError naming all four options + the bootstrap hint
13
16
  *
14
17
  * No `gh auth token` fallback by design — it usually has narrower
15
18
  * scopes than admin commands need.
@@ -19,8 +22,9 @@ var AuthError = class extends Error {
19
22
  };
20
23
  function resolveToken(input = {}) {
21
24
  const env = input.env ?? process.env;
22
- const token = input.cliToken || env.HOLOCRON_GH_TOKEN || env.GITHUB_TOKEN;
23
- if (!token) throw new AuthError("no GitHub token found. Pass --token <PAT>, or set HOLOCRON_GH_TOKEN / GITHUB_TOKEN.");
25
+ const keyring = input.keyring ?? getToken;
26
+ const token = input.cliToken || env.HOLOCRON_GH_TOKEN || env.GITHUB_TOKEN || keyring("github");
27
+ if (!token) throw new AuthError("no GitHub token found. Pass --token <PAT>, set HOLOCRON_GH_TOKEN / GITHUB_TOKEN, or run: holocron auth set github <PAT>");
24
28
  return token;
25
29
  }
26
30
  //#endregion
@@ -214,6 +218,7 @@ var GitHubIssues = class {
214
218
  via: "closed (completed)"
215
219
  };
216
220
  }
221
+ if (!this.labels) throw new Error(`transition to \`${slot}\` requires \`labels\` in plugin options: { inProgress: '<label>', inReview: '<label>' }`);
217
222
  const targetLabel = slot === "inProgress" ? this.labels.inProgress : this.labels.inReview;
218
223
  const alreadyOpen = issue.state === "open";
219
224
  const alreadyLabeled = currentStatusLabels.includes(targetLabel);
@@ -255,20 +260,19 @@ var GitHubIssues = class {
255
260
  name: "open (no status label)",
256
261
  category: "open"
257
262
  },
258
- {
263
+ ...this.labels ? [{
259
264
  name: `open + ${this.labels.inProgress}`,
260
265
  category: "in-progress"
261
- },
262
- {
266
+ }, {
263
267
  name: `open + ${this.labels.inReview}`,
264
268
  category: "in-review"
265
- },
269
+ }] : [],
266
270
  {
267
271
  name: "closed",
268
272
  category: "done"
269
273
  }
270
274
  ];
271
- const lifecycle = [
275
+ const lifecycle = this.labels ? [
272
276
  {
273
277
  slot: "inProgress",
274
278
  value: this.labels.inProgress,
@@ -287,6 +291,25 @@ var GitHubIssues = class {
287
291
  resolved: true,
288
292
  note: "(intrinsic — GitHub close-with-reason)"
289
293
  }
294
+ ] : [
295
+ {
296
+ slot: "inProgress",
297
+ value: null,
298
+ resolved: false,
299
+ note: "no `labels` configured in plugin options — transitions to `inProgress` will error"
300
+ },
301
+ {
302
+ slot: "inReview",
303
+ value: null,
304
+ resolved: false,
305
+ note: "no `labels` configured in plugin options — transitions to `inReview` will error"
306
+ },
307
+ {
308
+ slot: "done",
309
+ value: "closed (state_reason=completed)",
310
+ resolved: true,
311
+ note: "(intrinsic — GitHub close-with-reason)"
312
+ }
290
313
  ];
291
314
  return {
292
315
  authedAs: `${me.displayName} (${me.emailAddress ?? me.id})`,
@@ -309,10 +332,10 @@ var GitHubIssues = class {
309
332
  let category = "open";
310
333
  let statusName = raw.state === "closed" ? "closed" : "open";
311
334
  if (raw.state === "closed") category = "done";
312
- else if (statusLabels.includes(this.labels.inProgress)) {
335
+ else if (this.labels && statusLabels.includes(this.labels.inProgress)) {
313
336
  category = "in-progress";
314
337
  statusName = `open + ${this.labels.inProgress}`;
315
- } else if (statusLabels.includes(this.labels.inReview)) {
338
+ } else if (this.labels && statusLabels.includes(this.labels.inReview)) {
316
339
  category = "in-review";
317
340
  statusName = `open + ${this.labels.inReview}`;
318
341
  }
@@ -449,6 +472,7 @@ var GitHubSource = class {
449
472
  owner;
450
473
  name;
451
474
  repoPath;
475
+ repoRoot;
452
476
  workflowDir;
453
477
  constructor(rest, opts) {
454
478
  this.rest = rest;
@@ -456,6 +480,7 @@ var GitHubSource = class {
456
480
  this.owner = owner;
457
481
  this.name = name;
458
482
  this.repoPath = `/repos/${owner}/${name}`;
483
+ this.repoRoot = opts.repoRoot;
459
484
  this.workflowDir = join(opts.repoRoot, ".github", "workflows");
460
485
  }
461
486
  async whoami() {
@@ -490,6 +515,12 @@ var GitHubSource = class {
490
515
  body: settings
491
516
  });
492
517
  }
518
+ async protectBranch(branch, payload) {
519
+ await this.rest.request(`${this.repoPath}/branches/${encodeURIComponent(branch)}/protection`, {
520
+ method: "PUT",
521
+ body: payload
522
+ });
523
+ }
493
524
  async enableVulnerabilityAlerts() {
494
525
  await this.rest.request(`${this.repoPath}/vulnerability-alerts`, {
495
526
  method: "PUT",
@@ -507,7 +538,28 @@ var GitHubSource = class {
507
538
  method: "PATCH",
508
539
  body: { security_and_analysis: {
509
540
  secret_scanning: { status: "enabled" },
510
- secret_scanning_push_protection: { status: "enabled" }
541
+ secret_scanning_push_protection: { status: "enabled" },
542
+ secret_scanning_validity_checks: { status: "enabled" },
543
+ secret_scanning_non_provider_patterns: { status: "enabled" }
544
+ } }
545
+ });
546
+ }
547
+ async enableCodeScanning() {
548
+ return `run ${(await this.rest.request(`${this.repoPath}/code-scanning/default-setup`, {
549
+ method: "PATCH",
550
+ body: {
551
+ state: "configured",
552
+ query_suite: "extended",
553
+ threat_model: "remote_and_local"
554
+ }
555
+ })).run_id}`;
556
+ }
557
+ async enableDependencyGraph() {
558
+ await this.rest.request(this.repoPath, {
559
+ method: "PATCH",
560
+ body: { security_and_analysis: {
561
+ dependency_graph: { status: "enabled" },
562
+ dependency_graph_autosubmit_action: { status: "enabled" }
511
563
  } }
512
564
  });
513
565
  }
@@ -545,6 +597,11 @@ var GitHubSource = class {
545
597
  throw err;
546
598
  }
547
599
  }
600
+ async writeRepoFile(path, contents) {
601
+ const full = join(this.repoRoot, path);
602
+ await ensureDir(dirname(full));
603
+ await writeFile(full, contents, "utf8");
604
+ }
548
605
  };
549
606
  function isENOENT(err) {
550
607
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
@@ -610,6 +667,36 @@ var GitHubRestClient = class {
610
667
  }
611
668
  };
612
669
  //#endregion
670
+ //#region src/verify-token.ts
671
+ /**
672
+ * `verifyToken` — plugin-level export used by `holocron auth set` +
673
+ * `holocron auth check`. Hits `GET /user` and translates the response
674
+ * into the normalized `VerifyTokenResult` shape.
675
+ *
676
+ * Kept as a standalone function (not a capability method) so the auth
677
+ * command can call it without initializing the full plugin — plugin
678
+ * construction requires an already-resolved token, which is exactly
679
+ * what we don't have yet at bootstrap time.
680
+ */
681
+ async function verifyToken(token, opts = {}) {
682
+ const restOpts = { token };
683
+ if (opts.baseUrl !== void 0) restOpts.baseUrl = opts.baseUrl;
684
+ if (opts.fetch !== void 0) restOpts.fetch = opts.fetch;
685
+ const rest = new GitHubRestClient(restOpts);
686
+ try {
687
+ const me = await rest.request("/user");
688
+ return {
689
+ ok: true,
690
+ subject: `user @ ${me.login ?? me.email ?? "unknown"}`
691
+ };
692
+ } catch (err) {
693
+ return {
694
+ ok: false,
695
+ message: err instanceof Error ? err.message : String(err)
696
+ };
697
+ }
698
+ }
699
+ //#endregion
613
700
  //#region src/index.ts
614
701
  function createContext(options) {
615
702
  return {
@@ -639,11 +726,9 @@ function ci(ctx) {
639
726
  return new GitHubCi(ctx.rest, { repo: ctx.repo });
640
727
  }
641
728
  function issues(ctx) {
642
- if (!ctx.options.labels) throw new Error("issues capability requires `labels` in plugin options: { inProgress, inReview }");
643
- return new GitHubIssues(ctx.rest, {
644
- repo: ctx.repo,
645
- labels: ctx.options.labels
646
- });
729
+ const opts = { repo: ctx.repo };
730
+ if (ctx.options.labels !== void 0) opts.labels = ctx.options.labels;
731
+ return new GitHubIssues(ctx.rest, opts);
647
732
  }
648
733
  function createPlugin(options) {
649
734
  const ctx = createContext(options);
@@ -658,5 +743,12 @@ function createPlugin(options) {
658
743
  }
659
744
  };
660
745
  }
746
+ /**
747
+ * One-line hint printed by `holocron auth set github` when no token
748
+ * is supplied or the supplied token is rejected. Generate a PAT at
749
+ * https://github.com/settings/tokens with `repo` + `admin:repo_hook`
750
+ * scopes (add `admin:org` for org-level operations).
751
+ */
752
+ 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>";
661
753
  //#endregion
662
- export { AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubRestClient, GitHubSecrets, GitHubSource, ci, createContext, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source };
754
+ export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubRestClient, GitHubSecrets, GitHubSource, ci, createContext, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-github",
3
- "version": "2.0.0-alpha.1",
3
+ "version": "2.0.0-alpha.11",
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,7 +21,7 @@
21
21
  }
22
22
  },
23
23
  "peerDependencies": {
24
- "@theholocron/cli": "2.0.0-alpha.1"
24
+ "@theholocron/cli": "2.0.0-alpha.11"
25
25
  },
26
26
  "dependencies": {
27
27
  "libsodium-wrappers": "^0.7.15"
@@ -37,7 +37,8 @@
37
37
  "typescript": "^5.9.3",
38
38
  "vitest": "^3.2.6",
39
39
  "tsdown": "^0.22.3",
40
- "@theholocron/cli": "2.0.0-alpha.1"
40
+ "tsx": "^4.22.4",
41
+ "@theholocron/cli": "2.0.0-alpha.11"
41
42
  },
42
43
  "publishConfig": {
43
44
  "access": "public"
@@ -52,7 +53,8 @@
52
53
  "typecheck": "tsc --noEmit",
53
54
  "test": "vitest run",
54
55
  "test:watch": "vitest",
55
- "test:coverage": "vitest run --coverage"
56
+ "test:coverage": "vitest run --coverage",
57
+ "validate": "tsx scripts/validate.mjs"
56
58
  },
57
59
  "types": "./dist/index.d.mts"
58
60
  }