@theholocron/holocron-plugin-github 5.0.0-alpha.22 → 5.0.0-alpha.220
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 +36 -25
- package/dist/index.mjs +249 -27
- package/package.json +17 -13
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
415
|
+
await client.secrets.putSecret(this.opts.repo, ghScope, name, body);
|
|
395
416
|
}
|
|
396
417
|
async deleteSecret(scope, name) {
|
|
397
|
-
await this.
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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: () =>
|
|
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.
|
|
3
|
+
"version": "5.0.0-alpha.220",
|
|
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/
|
|
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": "
|
|
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.
|
|
38
|
-
"@theholocron/
|
|
39
|
-
"@theholocron/
|
|
40
|
-
"@theholocron/
|
|
41
|
-
"@theholocron/
|
|
42
|
-
"@theholocron/
|
|
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.43.1",
|
|
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.
|
|
52
|
+
"tsx": "4.23.15",
|
|
51
53
|
"typescript": "^5.9.3",
|
|
52
54
|
"vitest": "^4.1.11",
|
|
53
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
55
|
+
"@theholocron/cli": "5.0.0-alpha.220"
|
|
54
56
|
},
|
|
55
57
|
"peerDependencies": {
|
|
56
|
-
"@theholocron/
|
|
57
|
-
"@theholocron/
|
|
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.220"
|
|
58
62
|
},
|
|
59
63
|
"engines": {
|
|
60
64
|
"node": ">=22"
|