@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 +1 -1
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +108 -0
- package/package.json +5 -5
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 [
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
59
|
+
"@theholocron/github-client": "^1.51.0",
|
|
60
60
|
"@theholocron/misc-utils": "^1.16.0",
|
|
61
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
61
|
+
"@theholocron/cli": "5.0.0-alpha.210"
|
|
62
62
|
},
|
|
63
63
|
"engines": {
|
|
64
64
|
"node": ">=22"
|