@theholocron/holocron-plugin-github 5.0.0-alpha.209 → 5.0.0-alpha.210

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
@@ -181,6 +181,17 @@ declare class GitHubSource implements Source {
181
181
  repo?: string;
182
182
  }): Promise<void>;
183
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>;
184
195
  syncDescription(description: string): Promise<string>;
185
196
  syncHomepage(homepage: string): Promise<string>;
186
197
  getPullRequest(number: number, repo?: string): Promise<PullRequest>;
package/dist/index.mjs CHANGED
@@ -538,6 +538,107 @@ async function syncTopics(client, repo, topics) {
538
538
  return `${topics.length} topics set`;
539
539
  }
540
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
541
642
  //#region src/capabilities/source.ts
542
643
  var GitHubSource = class {
543
644
  client;
@@ -672,6 +773,13 @@ var GitHubSource = class {
672
773
  async syncTopics(topics) {
673
774
  return syncTopics(this.client, this.repo, topics);
674
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
+ }
675
783
  async syncDescription(description) {
676
784
  await this.client.repos.updateRepo(this.repo, { description });
677
785
  return "description updated";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-github",
3
- "version": "5.0.0-alpha.209",
3
+ "version": "5.0.0-alpha.210",
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",
@@ -36,7 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@theholocron/eslint-config": "^8.8.2",
38
38
  "@theholocron/fs-utils": "^1.16.0",
39
- "@theholocron/github-client": "^1.47.1",
39
+ "@theholocron/github-client": "^1.51.0",
40
40
  "@theholocron/http-client": "^1.43.1",
41
41
  "@theholocron/misc-utils": "^1.16.0",
42
42
  "@theholocron/tsconfig": "^8.8.2",
@@ -52,13 +52,13 @@
52
52
  "tsx": "4.23.15",
53
53
  "typescript": "^5.9.3",
54
54
  "vitest": "^4.1.11",
55
- "@theholocron/cli": "5.0.0-alpha.209"
55
+ "@theholocron/cli": "5.0.0-alpha.210"
56
56
  },
57
57
  "peerDependencies": {
58
58
  "@theholocron/fs-utils": "^1.16.0",
59
- "@theholocron/github-client": "^1.47.1",
59
+ "@theholocron/github-client": "^1.51.0",
60
60
  "@theholocron/misc-utils": "^1.16.0",
61
- "@theholocron/cli": "5.0.0-alpha.209"
61
+ "@theholocron/cli": "5.0.0-alpha.210"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=22"