@theholocron/holocron-plugin-github 5.0.0-alpha.23 → 5.0.0-alpha.230

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/README.md CHANGED
@@ -45,7 +45,7 @@ holocron auth set github.release ghp_xxx # semantic-release
45
45
  holocron auth set github.admin ghp_xxx # setup, secrets, environments
46
46
  ```
47
47
 
48
- See [docs/tokens.md](../../docs/tokens.md) for full scope requirements and CI setup.
48
+ See the [token reference](https://docs.theholocron.dev/holocron/tokens/) for full scope requirements and CI setup.
49
49
 
50
50
  ## Config
51
51
 
package/dist/index.d.mts CHANGED
@@ -1,12 +1,14 @@
1
1
  import { GitHubClient, GitHubClientOptions, createGitHubClient } from "@theholocron/github-client";
2
- import { Auth, AuthError, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, PagesConfig, PullRequest, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, TeamEntry, TeamEntry as TeamEntry$1, TeamPermission, TrackerDoctorReport, TrackerUser, createFeatureResolver } from "@theholocron/cli";
2
+ import { Auth, AuthError, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, PagesConfig, PullRequest, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, SourceFile, SourceTeamMember, TeamEntry, TeamEntry as TeamEntry$1, TeamPermission, TrackerDoctorReport, TrackerUser, VerifyTokenFailure, VerifyTokenResult, VerifyTokenResult as VerifyTokenResult$1, VerifyTokenSuccess, createFeatureResolver } from "@theholocron/cli";
3
3
  //#region src/auth.d.ts
4
4
  declare const resolveReadToken: (input?: ResolveTokenInput) => string;
5
5
  declare const resolveIssuesToken: (input?: ResolveTokenInput) => string;
6
6
  declare const resolveSyncToken: (input?: ResolveTokenInput) => string;
7
7
  declare const resolveReleaseToken: (input?: ResolveTokenInput) => string;
8
8
  declare const resolveAdminToken: (input?: ResolveTokenInput) => string;
9
+ declare const resolveSecretsToken: (input?: ResolveTokenInput) => string;
9
10
  declare const resolveDeployToken: (input?: ResolveTokenInput) => string;
11
+ declare const resolveUserSyncToken: (input?: ResolveTokenInput) => string;
10
12
  declare const resolveOrgToken: (input?: ResolveTokenInput) => string;
11
13
  //#endregion
12
14
  //#region src/capabilities/ci.d.ts
@@ -106,9 +108,16 @@ interface SecretsOptions {
106
108
  declare class GitHubSecrets implements Secrets {
107
109
  private readonly client;
108
110
  private readonly opts;
111
+ private readonly orgClient?;
109
112
  readonly key: "secrets";
110
113
  readonly providerName = "github";
111
- constructor(client: GitHubClient, opts: SecretsOptions);
114
+ /**
115
+ * `client` handles repo and environment scopes; `orgClient`, when given, handles
116
+ * the organization scope. They differ when a dedicated org-only token exists:
117
+ * it can write org secrets but not repo ones, and `github.admin` is the reverse.
118
+ */
119
+ constructor(client: GitHubClient, opts: SecretsOptions, orgClient?: GitHubClient | undefined);
120
+ private clientFor;
112
121
  listSecrets(scope: SecretScope): Promise<string[]>;
113
122
  setSecret(scope: SecretScope, name: string, value: string): Promise<void>;
114
123
  deleteSecret(scope: SecretScope, name: string): Promise<void>;
@@ -120,6 +129,8 @@ interface SourceOptions {
120
129
  repoRoot: string;
121
130
  /** Secondary client authenticated with HOLOCRON_ORG_TOKEN for org-scoped operations (teams, custom properties). */
122
131
  orgClient?: GitHubClient;
132
+ /** Client for the files that link GitHub accounts to other services (HOLOCRON_USER_SYNC_TOKEN); falls back to the org client. */
133
+ userSyncClient?: GitHubClient;
123
134
  /** Forwarded to a secondary client created for Pages calls (HOLOCRON_DEPLOY_TOKEN). */
124
135
  baseUrl?: string;
125
136
  fetch?: typeof fetch;
@@ -134,6 +145,7 @@ declare class GitHubSource implements Source {
134
145
  private readonly repoRoot;
135
146
  private readonly workflowDir;
136
147
  private readonly orgClient?;
148
+ private readonly userSyncClient?;
137
149
  private readonly baseUrl?;
138
150
  private readonly fetchImpl?;
139
151
  constructor(client: GitHubClient, opts: SourceOptions);
@@ -161,7 +173,25 @@ declare class GitHubSource implements Source {
161
173
  syncLabels(canonical: ReadonlyArray<LabelDef>, stale: ReadonlyArray<string>): Promise<string>;
162
174
  syncProperties(values: Record<string, string | string[]>): Promise<string>;
163
175
  syncTeams(teams: TeamEntry$1[]): Promise<string>;
176
+ teamMembers(teamSlug: string, org?: string): Promise<SourceTeamMember[]>;
177
+ readFile(path: string, repo?: string): Promise<SourceFile | null>;
178
+ writeFile(path: string, content: string, opts: {
179
+ message: string;
180
+ version?: string;
181
+ repo?: string;
182
+ }): Promise<void>;
164
183
  syncTopics(topics: string[]): Promise<string>;
184
+ syncVariables(variables: Record<string, string>, opts?: {
185
+ prune?: boolean;
186
+ dryRun?: boolean;
187
+ }): Promise<string>;
188
+ syncOrgVariables(org: string, variables: Record<string, {
189
+ value: string;
190
+ visibility: "all" | "private";
191
+ }>, opts?: {
192
+ prune?: boolean;
193
+ dryRun?: boolean;
194
+ }): Promise<string>;
165
195
  syncDescription(description: string): Promise<string>;
166
196
  syncHomepage(homepage: string): Promise<string>;
167
197
  getPullRequest(number: number, repo?: string): Promise<PullRequest>;
@@ -184,30 +214,11 @@ declare function syncTeams(client: GitHubClient, repo: string, teams: TeamEntry[
184
214
  declare function encryptSecret(publicKeyBase64: string, value: string): Promise<string>;
185
215
  //#endregion
186
216
  //#region src/verify-token.d.ts
187
- /**
188
- * `verifyToken` — plugin-level export used by `holocron auth set` +
189
- * `holocron auth check`. Hits `GET /user` and translates the response
190
- * into the normalized `VerifyTokenResult` shape.
191
- *
192
- * Kept as a standalone function (not a capability method) so the auth
193
- * command can call it without initializing the full plugin — plugin
194
- * construction requires an already-resolved token, which is exactly
195
- * what we don't have yet at bootstrap time.
196
- */
197
- interface VerifyTokenSuccess {
198
- ok: true;
199
- subject: string;
200
- }
201
- interface VerifyTokenFailure {
202
- ok: false;
203
- message: string;
204
- }
205
- type VerifyTokenResult = VerifyTokenSuccess | VerifyTokenFailure;
206
217
  interface VerifyTokenOptions {
207
218
  baseUrl?: string;
208
219
  fetch?: typeof fetch;
209
220
  }
210
- declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult>;
221
+ declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult$1>;
211
222
  //#endregion
212
223
  //#region src/index.d.ts
213
224
  interface GitHubPluginOptions extends ResolveTokenInput {
@@ -232,8 +243,8 @@ interface PluginContext {
232
243
  repo: string;
233
244
  repoRoot: string;
234
245
  }
235
- declare function source(ctx: PluginContext, orgClient?: GitHubClient): Source;
236
- declare function secrets(ctx: PluginContext): Secrets;
246
+ declare function source(ctx: PluginContext, orgClient?: GitHubClient, userSyncClient?: GitHubClient): Source;
247
+ declare function secrets(ctx: PluginContext, orgClient?: GitHubClient): Secrets;
237
248
  declare function environments(ctx: PluginContext): Environments;
238
249
  declare function ci(ctx: PluginContext): Ci;
239
250
  declare function issues(ctx: PluginContext): Issues;
@@ -253,4 +264,4 @@ declare function createPlugin(options: GitHubPluginOptions): {
253
264
  */
254
265
  declare const AUTH_HINT: string;
255
266
  //#endregion
256
- export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, type GitHubClientOptions, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, type NormalizedTeamEntry, PluginContext, type ResolveTokenInput, type TeamEntry, type TeamPermission, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createFeatureResolver, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveAdminToken, resolveDeployToken, resolveIssuesToken, resolveOrgToken, resolveReadToken, resolveReleaseToken, resolveSyncToken, secrets, source, syncLabels, syncTeams, verifyToken };
267
+ export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, type GitHubClientOptions, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, type NormalizedTeamEntry, PluginContext, type ResolveTokenInput, type TeamEntry, type TeamPermission, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createFeatureResolver, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveAdminToken, resolveDeployToken, resolveIssuesToken, resolveOrgToken, resolveReadToken, resolveReleaseToken, resolveSecretsToken, resolveSyncToken, resolveUserSyncToken, secrets, source, syncLabels, syncTeams, verifyToken };
package/dist/index.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createGitHubClient, createGitHubClient as createGitHubClient$1 } from "@theholocron/github-client";
3
- import { AuthError, createFeatureResolver } from "@theholocron/cli";
3
+ import { AuthError, ProviderApiError, createFeatureResolver } from "@theholocron/cli";
4
4
  import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
5
5
  import { dirname, join } from "node:path";
6
+ import { findRootSync } from "@theholocron/fs-utils";
7
+ import { errorMessage } from "@theholocron/misc-utils";
6
8
  //#region src/auth.ts
7
9
  const resolveReadToken = createFeatureResolver({
8
10
  envName: "HOLOCRON_READ_TOKEN",
@@ -24,10 +26,18 @@ const resolveAdminToken = createFeatureResolver({
24
26
  envName: "HOLOCRON_ADMIN_TOKEN",
25
27
  keyringKey: "github.admin"
26
28
  });
29
+ const resolveSecretsToken = createFeatureResolver({
30
+ envName: "HOLOCRON_SECRETS_TOKEN",
31
+ keyringKey: "github.secrets"
32
+ });
27
33
  const resolveDeployToken = createFeatureResolver({
28
34
  envName: "HOLOCRON_DEPLOY_TOKEN",
29
35
  keyringKey: "github.deploy"
30
36
  });
37
+ const resolveUserSyncToken = createFeatureResolver({
38
+ envName: "HOLOCRON_USER_SYNC_TOKEN",
39
+ keyringKey: "github.user-sync"
40
+ });
31
41
  const resolveOrgToken = createFeatureResolver({
32
42
  envName: "HOLOCRON_ORG_TOKEN",
33
43
  keyringKey: "github.org"
@@ -374,27 +384,38 @@ async function encryptSecret(publicKeyBase64, value) {
374
384
  var GitHubSecrets = class {
375
385
  client;
376
386
  opts;
387
+ orgClient;
377
388
  key = "secrets";
378
389
  providerName = "github";
379
- constructor(client, opts) {
390
+ /**
391
+ * `client` handles repo and environment scopes; `orgClient`, when given, handles
392
+ * the organization scope. They differ when a dedicated org-only token exists:
393
+ * it can write org secrets but not repo ones, and `github.admin` is the reverse.
394
+ */
395
+ constructor(client, opts, orgClient) {
380
396
  this.client = client;
381
397
  this.opts = opts;
398
+ this.orgClient = orgClient;
399
+ }
400
+ clientFor(scope) {
401
+ return scope.kind === "organization" ? this.orgClient ?? this.client : this.client;
382
402
  }
383
403
  async listSecrets(scope) {
384
- return this.client.secrets.listSecrets(this.opts.repo, toGHScope(scope));
404
+ return this.clientFor(scope).secrets.listSecrets(this.opts.repo, toGHScope(scope));
385
405
  }
386
406
  async setSecret(scope, name, value) {
387
407
  const ghScope = toGHScope(scope);
388
- const pk = await this.client.secrets.getPublicKey(this.opts.repo, ghScope);
408
+ const client = this.clientFor(scope);
409
+ const pk = await client.secrets.getPublicKey(this.opts.repo, ghScope);
389
410
  const body = {
390
411
  encrypted_value: await encryptSecret(pk.key, value),
391
412
  key_id: pk.key_id
392
413
  };
393
414
  if (scope.kind === "organization") body.visibility = "all";
394
- await this.client.secrets.putSecret(this.opts.repo, ghScope, name, body);
415
+ await client.secrets.putSecret(this.opts.repo, ghScope, name, body);
395
416
  }
396
417
  async deleteSecret(scope, name) {
397
- await this.client.secrets.deleteSecret(this.opts.repo, toGHScope(scope), name);
418
+ await this.clientFor(scope).secrets.deleteSecret(this.opts.repo, toGHScope(scope), name);
398
419
  }
399
420
  };
400
421
  function toGHScope(scope) {
@@ -405,6 +426,46 @@ function toGHScope(scope) {
405
426
  return scope;
406
427
  }
407
428
  //#endregion
429
+ //#region src/capabilities/files.ts
430
+ /** Reads one file; `null` when it does not exist. `version` is the blob sha `writeFile` needs to replace exactly this one. */
431
+ /** A fine-grained token only reaches the repositories and permissions it was given; say which one is missing. */
432
+ function noAccess(repo, action, cause) {
433
+ return new Error(`the token cannot ${action} files in ${repo} (403): give the user-sync token (HOLOCRON_USER_SYNC_TOKEN, or holocron auth set github.user-sync) access to that repository with Contents: ${action === "read" ? "read" : "read and write"}`, { cause });
434
+ }
435
+ async function readRepoFile(client, repo, path) {
436
+ try {
437
+ const file = await client.git.getContents(repo, path);
438
+ if (file.encoding !== "base64") throw new Error(`${repo}/${path}: unexpected encoding \`${file.encoding}\``);
439
+ return {
440
+ content: Buffer.from(file.content, "base64").toString("utf-8"),
441
+ version: file.sha
442
+ };
443
+ } catch (err) {
444
+ if (err instanceof ProviderApiError && err.status === 403) throw noAccess(repo, "read", err);
445
+ if (!(err instanceof ProviderApiError && err.status === 404)) throw err;
446
+ }
447
+ try {
448
+ await client.repos.getRepo(repo);
449
+ } catch (err) {
450
+ if (err instanceof ProviderApiError && err.status === 404) throw new Error(`${repo} was not found: it does not exist, or the token cannot see it`, { cause: err });
451
+ throw err;
452
+ }
453
+ return null;
454
+ }
455
+ /** Creates or replaces one file in a commit; GitHub refuses (409) a `version` that has since changed. */
456
+ async function writeRepoFile(client, repo, path, content, opts) {
457
+ try {
458
+ await client.git.putContents(repo, path, {
459
+ message: opts.message,
460
+ content,
461
+ ...opts.version ? { sha: opts.version } : {}
462
+ });
463
+ } catch (err) {
464
+ if (err instanceof ProviderApiError && err.status === 403) throw noAccess(repo, "write", err);
465
+ throw err;
466
+ }
467
+ }
468
+ //#endregion
408
469
  //#region src/capabilities/labels.ts
409
470
  /**
410
471
  * Idempotently synchronise a repo's labels to the canonical set.
@@ -477,6 +538,107 @@ async function syncTopics(client, repo, topics) {
477
538
  return `${topics.length} topics set`;
478
539
  }
479
540
  //#endregion
541
+ //#region src/capabilities/variables.ts
542
+ /** GitHub's rule for a variable name: letters, digits and underscores, not starting with a digit or `GITHUB_`. */
543
+ const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
544
+ /** Longest name GitHub accepts. */
545
+ const MAX_NAME = 256;
546
+ function assertNames(names) {
547
+ for (const name of names) if (!NAME.test(name) || name.length > MAX_NAME || name.toUpperCase().startsWith("GITHUB_")) throw new Error(`invalid variable name ${JSON.stringify(name)}: use letters, digits and underscores (at most ${MAX_NAME}), not starting with a digit or GITHUB_`);
548
+ }
549
+ /** GitHub treats variable names as case-insensitive, so every comparison goes through this. */
550
+ const key = (name) => name.toUpperCase();
551
+ /**
552
+ * Bring one scope's variables in line with the declaration: create what is missing, update what differs, leave what
553
+ * already matches. Variables that exist but are not declared are left alone, or deleted with `prune`, except an
554
+ * organization variable limited to selected repositories, which this client cannot manage and so is never deleted.
555
+ * Returns a one-line summary, naming the ones left so drift is visible. In a dry run it only describes the changes.
556
+ */
557
+ async function reconcile(client, repo, scope, wanted, opts) {
558
+ assertNames(wanted.map((w) => w.name));
559
+ const existing = new Map((await client.variables.listVariables(repo, scope)).map((v) => [key(v.name), v]));
560
+ const counts = {
561
+ created: 0,
562
+ updated: 0,
563
+ unchanged: 0,
564
+ deleted: 0
565
+ };
566
+ for (const want of wanted) {
567
+ const have = existing.get(key(want.name));
568
+ const body = {
569
+ name: want.name,
570
+ value: want.value
571
+ };
572
+ if (want.visibility) body["visibility"] = want.visibility;
573
+ if (!have) {
574
+ if (opts.dryRun) {
575
+ counts.created++;
576
+ continue;
577
+ }
578
+ try {
579
+ await client.variables.createVariable(repo, scope, body);
580
+ counts.created++;
581
+ continue;
582
+ } catch (err) {
583
+ if (!(err instanceof ProviderApiError && (err.status === 409 || err.status === 422))) throw err;
584
+ }
585
+ } else if (have.value === want.value && (!want.visibility || have.visibility === want.visibility)) {
586
+ counts.unchanged++;
587
+ continue;
588
+ }
589
+ if (!opts.dryRun) await client.variables.updateVariable(repo, scope, have?.name ?? want.name, body);
590
+ counts.updated++;
591
+ }
592
+ const declared = new Set(wanted.map((w) => key(w.name)));
593
+ const undeclared = [...existing.entries()].filter(([k]) => !declared.has(k)).map(([, v]) => v);
594
+ const unmanaged = undeclared.filter((v) => v.visibility === "selected");
595
+ if (opts.prune) for (const v of undeclared.filter((u) => u.visibility !== "selected")) {
596
+ if (!opts.dryRun) await client.variables.deleteVariable(repo, scope, v.name);
597
+ counts.deleted++;
598
+ }
599
+ const parts = opts.dryRun ? [
600
+ `would create ${counts.created}`,
601
+ `would update ${counts.updated}`,
602
+ `${counts.unchanged} unchanged`
603
+ ] : [
604
+ `${counts.created} created`,
605
+ `${counts.updated} updated`,
606
+ `${counts.unchanged} unchanged`
607
+ ];
608
+ if (counts.deleted > 0) parts.push(`${counts.deleted} ${opts.dryRun ? "would be deleted" : "deleted"}`);
609
+ const left = opts.prune ? unmanaged : undeclared;
610
+ if (left.length > 0) {
611
+ const why = opts.prune ? "limited to selected repositories, not managed" : "not declared, left alone";
612
+ parts.push(`${left.length} ${why}: ${left.map((v) => v.name).join(", ")}`);
613
+ }
614
+ return parts.join(", ");
615
+ }
616
+ /** Reconcile a repository's Actions variables. */
617
+ function syncVariables(client, repo, variables, opts = {}) {
618
+ return reconcile(client, repo, { kind: "repo" }, Object.entries(variables).map(([name, value]) => ({
619
+ name,
620
+ value
621
+ })), {
622
+ prune: opts.prune === true,
623
+ dryRun: opts.dryRun === true
624
+ });
625
+ }
626
+ /** Reconcile an organization's Actions variables. `repo` is only the coordinate the client is bound to. */
627
+ function syncOrgVariables(client, repo, org, variables, opts = {}) {
628
+ const wanted = Object.entries(variables).map(([name, v]) => ({
629
+ name,
630
+ value: v.value,
631
+ visibility: v.visibility
632
+ }));
633
+ return reconcile(client, repo, {
634
+ kind: "organization",
635
+ org
636
+ }, wanted, {
637
+ prune: opts.prune === true,
638
+ dryRun: opts.dryRun === true
639
+ });
640
+ }
641
+ //#endregion
480
642
  //#region src/capabilities/source.ts
481
643
  var GitHubSource = class {
482
644
  client;
@@ -488,6 +650,7 @@ var GitHubSource = class {
488
650
  repoRoot;
489
651
  workflowDir;
490
652
  orgClient;
653
+ userSyncClient;
491
654
  baseUrl;
492
655
  fetchImpl;
493
656
  constructor(client, opts) {
@@ -496,9 +659,13 @@ var GitHubSource = class {
496
659
  this.owner = owner;
497
660
  this.name = name;
498
661
  this.repo = opts.repo;
499
- this.repoRoot = opts.repoRoot;
500
- this.workflowDir = join(opts.repoRoot, ".github", "workflows");
662
+ this.repoRoot = findRootSync(opts.repoRoot, ".git", {
663
+ fallback: "start",
664
+ realpath: true
665
+ });
666
+ this.workflowDir = join(this.repoRoot, ".github", "workflows");
501
667
  this.orgClient = opts.orgClient;
668
+ this.userSyncClient = opts.userSyncClient;
502
669
  this.baseUrl = opts.baseUrl;
503
670
  this.fetchImpl = opts.fetch;
504
671
  }
@@ -591,9 +758,28 @@ var GitHubSource = class {
591
758
  async syncTeams(teams) {
592
759
  return syncTeams(this.orgClient ?? this.client, this.repo, teams);
593
760
  }
761
+ async teamMembers(teamSlug, org = this.owner) {
762
+ return (await (this.orgClient ?? this.client).teams.members(org, teamSlug)).map((m) => ({
763
+ id: String(m.id),
764
+ login: m.login
765
+ }));
766
+ }
767
+ async readFile(path, repo = this.repo) {
768
+ return readRepoFile(this.userSyncClient ?? this.orgClient ?? this.client, repo, path);
769
+ }
770
+ async writeFile(path, content, opts) {
771
+ return writeRepoFile(this.userSyncClient ?? this.orgClient ?? this.client, opts.repo ?? this.repo, path, content, opts);
772
+ }
594
773
  async syncTopics(topics) {
595
774
  return syncTopics(this.client, this.repo, topics);
596
775
  }
776
+ async syncVariables(variables, opts) {
777
+ return syncVariables(this.client, this.repo, variables, opts);
778
+ }
779
+ async syncOrgVariables(org, variables, opts) {
780
+ if (!this.orgClient) throw new Error("organization variables need the org token: set HOLOCRON_ORG_TOKEN or run `holocron auth set github.org <token>`");
781
+ return syncOrgVariables(this.orgClient, this.repo, org, variables, opts);
782
+ }
597
783
  async syncDescription(description) {
598
784
  await this.client.repos.updateRepo(this.repo, { description });
599
785
  return "description updated";
@@ -652,16 +838,6 @@ async function ensureDir(path) {
652
838
  }
653
839
  //#endregion
654
840
  //#region src/verify-token.ts
655
- /**
656
- * `verifyToken` — plugin-level export used by `holocron auth set` +
657
- * `holocron auth check`. Hits `GET /user` and translates the response
658
- * into the normalized `VerifyTokenResult` shape.
659
- *
660
- * Kept as a standalone function (not a capability method) so the auth
661
- * command can call it without initializing the full plugin — plugin
662
- * construction requires an already-resolved token, which is exactly
663
- * what we don't have yet at bootstrap time.
664
- */
665
841
  async function verifyToken(token, opts = {}) {
666
842
  const client = createGitHubClient({
667
843
  token,
@@ -677,23 +853,24 @@ async function verifyToken(token, opts = {}) {
677
853
  } catch (err) {
678
854
  return {
679
855
  ok: false,
680
- message: err instanceof Error ? err.message : String(err)
856
+ message: errorMessage(err)
681
857
  };
682
858
  }
683
859
  }
684
860
  //#endregion
685
861
  //#region src/index.ts
686
- function source(ctx, orgClient) {
862
+ function source(ctx, orgClient, userSyncClient) {
687
863
  return new GitHubSource(ctx.client, {
688
864
  repo: ctx.repo,
689
865
  repoRoot: ctx.repoRoot,
690
866
  orgClient,
867
+ userSyncClient,
691
868
  baseUrl: ctx.options.baseUrl,
692
869
  fetch: ctx.options.fetch
693
870
  });
694
871
  }
695
- function secrets(ctx) {
696
- return new GitHubSecrets(ctx.client, { repo: ctx.repo });
872
+ function secrets(ctx, orgClient) {
873
+ return new GitHubSecrets(ctx.client, { repo: ctx.repo }, orgClient);
697
874
  }
698
875
  function environments(ctx) {
699
876
  return new GitHubEnvironments(ctx.client, { repo: ctx.repo });
@@ -708,10 +885,11 @@ function issues(ctx) {
708
885
  }
709
886
  function createPlugin(options) {
710
887
  function makeCtx(resolver) {
888
+ const token = resolver(options);
711
889
  return {
712
890
  options,
713
891
  client: createGitHubClient$1({
714
- token: resolver(options),
892
+ token,
715
893
  baseUrl: options.baseUrl,
716
894
  fetch: options.fetch
717
895
  }),
@@ -719,10 +897,45 @@ function createPlugin(options) {
719
897
  repoRoot: options.repoRoot ?? process.cwd()
720
898
  };
721
899
  }
900
+ /** A token, or undefined when that feature token is simply not configured. */
901
+ function tryResolve(resolver) {
902
+ try {
903
+ return resolver(options);
904
+ } catch (err) {
905
+ if (err instanceof AuthError) return void 0;
906
+ throw err;
907
+ }
908
+ }
909
+ /**
910
+ * Organization secrets need an org-resource-owner token (`github.secrets`);
911
+ * repo and environment secrets need `github.admin`. Each scope prefers its own
912
+ * token and falls back to the other, so a setup with only one of them keeps
913
+ * working, and an explicit `--token` serves both. With neither configured this
914
+ * fails exactly as the admin-token capabilities do.
915
+ */
916
+ function makeSecrets() {
917
+ const secretsToken = tryResolve(resolveSecretsToken);
918
+ const adminToken = tryResolve(resolveAdminToken);
919
+ if (!secretsToken && !adminToken) resolveAdminToken(options);
920
+ const clientFor = (token) => createGitHubClient$1({
921
+ token,
922
+ baseUrl: options.baseUrl,
923
+ fetch: options.fetch
924
+ });
925
+ const repoClient = clientFor(adminToken ?? secretsToken);
926
+ const orgClient = clientFor(secretsToken ?? adminToken);
927
+ return secrets({
928
+ options,
929
+ client: repoClient,
930
+ repo: options.repo,
931
+ repoRoot: options.repoRoot ?? process.cwd()
932
+ }, orgClient);
933
+ }
722
934
  function makeOrgClient() {
723
935
  try {
936
+ const token = resolveOrgToken(options);
724
937
  return createGitHubClient$1({
725
- token: resolveOrgToken(options),
938
+ token,
726
939
  baseUrl: options.baseUrl,
727
940
  fetch: options.fetch
728
941
  });
@@ -730,12 +943,21 @@ function createPlugin(options) {
730
943
  return;
731
944
  }
732
945
  }
946
+ /** The token for the user-sync files; undefined when it is not configured, and the org token is used instead. */
947
+ function makeUserSyncClient() {
948
+ const token = tryResolve(resolveUserSyncToken);
949
+ return token ? createGitHubClient$1({
950
+ token,
951
+ baseUrl: options.baseUrl,
952
+ fetch: options.fetch
953
+ }) : void 0;
954
+ }
733
955
  return {
734
956
  name: "@theholocron/holocron-plugin-github",
735
957
  capabilities: {
736
- source: () => source(makeCtx(resolveAdminToken), makeOrgClient()),
958
+ source: () => source(makeCtx(resolveAdminToken), makeOrgClient(), makeUserSyncClient()),
737
959
  ci: () => ci(makeCtx(resolveReadToken)),
738
- secrets: () => secrets(makeCtx(resolveAdminToken)),
960
+ secrets: () => makeSecrets(),
739
961
  environments: () => environments(makeCtx(resolveAdminToken)),
740
962
  issues: () => issues(makeCtx(resolveIssuesToken))
741
963
  }
@@ -747,4 +969,4 @@ function createPlugin(options) {
747
969
  */
748
970
  const AUTH_HINT = "generate fine-grained Personal Access Tokens at https://github.com/settings/tokens (one per feature — see docs/tokens.md for required scopes per operation), then run: holocron auth set github.<feature> <PAT>";
749
971
  //#endregion
750
- export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createFeatureResolver, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveAdminToken, resolveDeployToken, resolveIssuesToken, resolveOrgToken, resolveReadToken, resolveReleaseToken, resolveSyncToken, secrets, source, syncLabels, syncTeams, verifyToken };
972
+ export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createFeatureResolver, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveAdminToken, resolveDeployToken, resolveIssuesToken, resolveOrgToken, resolveReadToken, resolveReleaseToken, resolveSecretsToken, resolveSyncToken, resolveUserSyncToken, secrets, source, syncLabels, syncTeams, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-github",
3
- "version": "5.0.0-alpha.23",
3
+ "version": "5.0.0-alpha.230",
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",
@@ -9,12 +9,12 @@
9
9
  "plugin",
10
10
  "source"
11
11
  ],
12
- "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-github#readme",
12
+ "homepage": "https://github.com/theholocron/holocron/tree/main/plugins/holocron-plugin-github#readme",
13
13
  "bugs": "https://github.com/theholocron/holocron/issues",
14
14
  "repository": {
15
15
  "type": "git",
16
16
  "url": "git+https://github.com/theholocron/holocron.git",
17
- "directory": "packages/holocron-plugin-github"
17
+ "directory": "plugins/holocron-plugin-github"
18
18
  },
19
19
  "license": "MIT",
20
20
  "author": "Newton Koumantzelis",
@@ -34,12 +34,14 @@
34
34
  "libsodium-wrappers": "^0.8.4"
35
35
  },
36
36
  "devDependencies": {
37
- "@theholocron/eslint-config": "^8.0.0",
38
- "@theholocron/github-client": "^1.22.0",
39
- "@theholocron/http-client": "^1.22.0",
40
- "@theholocron/tsconfig": "^8.0.0",
41
- "@theholocron/tsdown-config": "^8.0.0",
42
- "@theholocron/vitest-config": "^8.0.0",
37
+ "@theholocron/eslint-config": "^8.8.2",
38
+ "@theholocron/fs-utils": "^1.16.0",
39
+ "@theholocron/github-client": "^1.51.0",
40
+ "@theholocron/http-client": "^1.54.2",
41
+ "@theholocron/misc-utils": "^1.16.0",
42
+ "@theholocron/tsconfig": "^8.8.2",
43
+ "@theholocron/tsdown-config": "^8.8.2",
44
+ "@theholocron/vitest-config": "^8.8.2",
43
45
  "@types/node": "^26",
44
46
  "@vitest/coverage-v8": "^4.1.11",
45
47
  "@vitest/eslint-plugin": "^1.6.27",
@@ -47,14 +49,16 @@
47
49
  "eslint-plugin-n": "^18.3.0",
48
50
  "globals": "^17.11.0",
49
51
  "tsdown": "^0.22.14",
50
- "tsx": "4.23.12",
52
+ "tsx": "4.23.15",
51
53
  "typescript": "^5.9.3",
52
54
  "vitest": "^4.1.11",
53
- "@theholocron/cli": "5.0.0-alpha.23"
55
+ "@theholocron/cli": "5.0.0-alpha.230"
54
56
  },
55
57
  "peerDependencies": {
56
- "@theholocron/github-client": "^1.22.0",
57
- "@theholocron/cli": "5.0.0-alpha.23"
58
+ "@theholocron/fs-utils": "^1.16.0",
59
+ "@theholocron/github-client": "^1.51.0",
60
+ "@theholocron/misc-utils": "^1.16.0",
61
+ "@theholocron/cli": "5.0.0-alpha.230"
58
62
  },
59
63
  "engines": {
60
64
  "node": ">=22"