@theholocron/holocron-plugin-github 5.0.0-alpha.27 → 5.0.0-alpha.270
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 +73 -70
- package/dist/index.mjs +396 -65
- package/package.json +23 -19
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,19 +1,18 @@
|
|
|
1
|
+
import { Auth, AuthError, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, PagesConfig, PluginToken, PullRequest, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, SourceBranch, SourceFile, SourceTeamMember, TeamEntry, TeamEntry as TeamEntry$1, TeamPermission, TrackerDoctorReport, TrackerUser, VerifyTokenFailure, VerifyTokenResult, VerifyTokenResult as VerifyTokenResult$1, VerifyTokenSuccess, createFeatureResolver } from "@theholocron/cli";
|
|
1
2
|
import { GitHubClient, GitHubClientOptions, createGitHubClient } from "@theholocron/github-client";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
declare const
|
|
5
|
-
declare const
|
|
6
|
-
declare const
|
|
7
|
-
declare const
|
|
8
|
-
declare const
|
|
9
|
-
declare const
|
|
10
|
-
declare const resolveOrgToken: (
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/capabilities/ci.d.ts
|
|
3
|
+
export declare const resolveReadToken: import("@theholocron/cli").FeatureResolver;
|
|
4
|
+
export declare const resolveIssuesToken: import("@theholocron/cli").FeatureResolver;
|
|
5
|
+
export declare const resolveSyncToken: import("@theholocron/cli").FeatureResolver;
|
|
6
|
+
export declare const resolveReleaseToken: import("@theholocron/cli").FeatureResolver;
|
|
7
|
+
export declare const resolveAdminToken: import("@theholocron/cli").FeatureResolver;
|
|
8
|
+
export declare const resolveSecretsToken: import("@theholocron/cli").FeatureResolver;
|
|
9
|
+
export declare const resolveDeployToken: import("@theholocron/cli").FeatureResolver;
|
|
10
|
+
export declare const resolveUserSyncToken: import("@theholocron/cli").FeatureResolver;
|
|
11
|
+
export declare const resolveOrgToken: import("@theholocron/cli").FeatureResolver;
|
|
13
12
|
interface CiOptions {
|
|
14
13
|
repo: string;
|
|
15
14
|
}
|
|
16
|
-
declare class GitHubCi implements Ci {
|
|
15
|
+
export declare class GitHubCi implements Ci {
|
|
17
16
|
private readonly client;
|
|
18
17
|
private readonly opts;
|
|
19
18
|
readonly key: "ci";
|
|
@@ -24,12 +23,10 @@ declare class GitHubCi implements Ci {
|
|
|
24
23
|
private mapRun;
|
|
25
24
|
private mapConclusion;
|
|
26
25
|
}
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/capabilities/environments.d.ts
|
|
29
26
|
interface EnvironmentsOptions {
|
|
30
27
|
repo: string;
|
|
31
28
|
}
|
|
32
|
-
declare class GitHubEnvironments implements Environments {
|
|
29
|
+
export declare class GitHubEnvironments implements Environments {
|
|
33
30
|
private readonly client;
|
|
34
31
|
private readonly opts;
|
|
35
32
|
readonly key: "environments";
|
|
@@ -40,8 +37,6 @@ declare class GitHubEnvironments implements Environments {
|
|
|
40
37
|
deleteEnvironment(name: string): Promise<void>;
|
|
41
38
|
private flattenReviewers;
|
|
42
39
|
}
|
|
43
|
-
//#endregion
|
|
44
|
-
//#region src/capabilities/issues.d.ts
|
|
45
40
|
interface IssuesOptions {
|
|
46
41
|
repo: string;
|
|
47
42
|
/**
|
|
@@ -55,7 +50,7 @@ interface IssuesOptions {
|
|
|
55
50
|
inReview: string;
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
|
-
declare class GitHubIssues implements Issues {
|
|
53
|
+
export declare class GitHubIssues implements Issues {
|
|
59
54
|
private readonly client;
|
|
60
55
|
readonly key: "issues";
|
|
61
56
|
readonly providerName = "github";
|
|
@@ -82,8 +77,6 @@ declare class GitHubIssues implements Issues {
|
|
|
82
77
|
private removeStatusLabels;
|
|
83
78
|
private mapIssue;
|
|
84
79
|
}
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/capabilities/labels.d.ts
|
|
87
80
|
interface CanonicalLabel {
|
|
88
81
|
readonly name: string;
|
|
89
82
|
readonly color: string;
|
|
@@ -97,34 +90,39 @@ interface CanonicalLabel {
|
|
|
97
90
|
* - DELETEs labels listed in `stale`.
|
|
98
91
|
* - Leaves unrecognised labels that aren't in `stale` untouched.
|
|
99
92
|
*/
|
|
100
|
-
declare function syncLabels(client: GitHubClient, repo: string, canonical: ReadonlyArray<CanonicalLabel>, stale: ReadonlyArray<string>): Promise<string>;
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region src/capabilities/secrets.d.ts
|
|
93
|
+
export declare function syncLabels(client: GitHubClient, repo: string, canonical: ReadonlyArray<CanonicalLabel>, stale: ReadonlyArray<string>): Promise<string>;
|
|
103
94
|
interface SecretsOptions {
|
|
104
95
|
repo: string;
|
|
105
96
|
}
|
|
106
|
-
declare class GitHubSecrets implements Secrets {
|
|
97
|
+
export declare class GitHubSecrets implements Secrets {
|
|
107
98
|
private readonly client;
|
|
108
99
|
private readonly opts;
|
|
100
|
+
private readonly orgClient?;
|
|
109
101
|
readonly key: "secrets";
|
|
110
102
|
readonly providerName = "github";
|
|
111
|
-
|
|
103
|
+
/**
|
|
104
|
+
* `client` handles repo and environment scopes; `orgClient`, when given, handles
|
|
105
|
+
* the organization scope. They differ when a dedicated org-only token exists:
|
|
106
|
+
* it can write org secrets but not repo ones, and `github.admin` is the reverse.
|
|
107
|
+
*/
|
|
108
|
+
constructor(client: GitHubClient, opts: SecretsOptions, orgClient?: GitHubClient | undefined);
|
|
109
|
+
private clientFor;
|
|
112
110
|
listSecrets(scope: SecretScope): Promise<string[]>;
|
|
113
111
|
setSecret(scope: SecretScope, name: string, value: string): Promise<void>;
|
|
114
112
|
deleteSecret(scope: SecretScope, name: string): Promise<void>;
|
|
115
113
|
}
|
|
116
|
-
//#endregion
|
|
117
|
-
//#region src/capabilities/source.d.ts
|
|
118
114
|
interface SourceOptions {
|
|
119
115
|
repo: string;
|
|
120
116
|
repoRoot: string;
|
|
121
117
|
/** Secondary client authenticated with HOLOCRON_ORG_TOKEN for org-scoped operations (teams, custom properties). */
|
|
122
118
|
orgClient?: GitHubClient;
|
|
119
|
+
/** Client for the files that link GitHub accounts to other services (HOLOCRON_USER_SYNC_TOKEN); falls back to the org client. */
|
|
120
|
+
userSyncClient?: GitHubClient;
|
|
123
121
|
/** Forwarded to a secondary client created for Pages calls (HOLOCRON_DEPLOY_TOKEN). */
|
|
124
122
|
baseUrl?: string;
|
|
125
123
|
fetch?: typeof fetch;
|
|
126
124
|
}
|
|
127
|
-
declare class GitHubSource implements Source {
|
|
125
|
+
export declare class GitHubSource implements Source {
|
|
128
126
|
private readonly client;
|
|
129
127
|
readonly key: "source";
|
|
130
128
|
readonly providerName = "github";
|
|
@@ -134,6 +132,7 @@ declare class GitHubSource implements Source {
|
|
|
134
132
|
private readonly repoRoot;
|
|
135
133
|
private readonly workflowDir;
|
|
136
134
|
private readonly orgClient?;
|
|
135
|
+
private readonly userSyncClient?;
|
|
137
136
|
private readonly baseUrl?;
|
|
138
137
|
private readonly fetchImpl?;
|
|
139
138
|
constructor(client: GitHubClient, opts: SourceOptions);
|
|
@@ -161,56 +160,56 @@ declare class GitHubSource implements Source {
|
|
|
161
160
|
syncLabels(canonical: ReadonlyArray<LabelDef>, stale: ReadonlyArray<string>): Promise<string>;
|
|
162
161
|
syncProperties(values: Record<string, string | string[]>): Promise<string>;
|
|
163
162
|
syncTeams(teams: TeamEntry$1[]): Promise<string>;
|
|
163
|
+
teamMembers(teamSlug: string, org?: string): Promise<SourceTeamMember[]>;
|
|
164
|
+
readFile(path: string, repo?: string): Promise<SourceFile | null>;
|
|
165
|
+
writeFile(path: string, content: string, opts: {
|
|
166
|
+
message: string;
|
|
167
|
+
version?: string;
|
|
168
|
+
repo?: string;
|
|
169
|
+
}): Promise<void>;
|
|
164
170
|
syncTopics(topics: string[]): Promise<string>;
|
|
171
|
+
syncVariables(variables: Record<string, string>, opts?: {
|
|
172
|
+
prune?: boolean;
|
|
173
|
+
dryRun?: boolean;
|
|
174
|
+
}): Promise<string>;
|
|
175
|
+
syncOrgVariables(org: string, variables: Record<string, {
|
|
176
|
+
value: string;
|
|
177
|
+
visibility: "all" | "private";
|
|
178
|
+
}>, opts?: {
|
|
179
|
+
prune?: boolean;
|
|
180
|
+
dryRun?: boolean;
|
|
181
|
+
}): Promise<string>;
|
|
182
|
+
listVariables(): Promise<Record<string, string>>;
|
|
183
|
+
listOrgVariables(org: string): Promise<Record<string, {
|
|
184
|
+
value: string;
|
|
185
|
+
visibility: "all" | "private" | "selected";
|
|
186
|
+
}>>;
|
|
165
187
|
syncDescription(description: string): Promise<string>;
|
|
166
188
|
syncHomepage(homepage: string): Promise<string>;
|
|
167
189
|
getPullRequest(number: number, repo?: string): Promise<PullRequest>;
|
|
190
|
+
listPullRequests(state: "open" | "closed", repo?: string): Promise<PullRequest[]>;
|
|
191
|
+
listBranches(repo?: string): Promise<SourceBranch[]>;
|
|
192
|
+
deleteBranch(name: string, repo?: string): Promise<void>;
|
|
193
|
+
listPullRequestCommits(number: number, repo?: string): Promise<string[]>;
|
|
168
194
|
configurePages(config: PagesConfig, token: string): Promise<void>;
|
|
169
195
|
}
|
|
170
|
-
//#endregion
|
|
171
|
-
//#region src/capabilities/teams.d.ts
|
|
172
196
|
interface NormalizedTeamEntry {
|
|
173
197
|
slug: string;
|
|
174
198
|
permission: TeamPermission;
|
|
175
199
|
}
|
|
176
|
-
declare function normalizeTeamEntry(entry: TeamEntry): NormalizedTeamEntry;
|
|
177
|
-
declare function syncTeams(client: GitHubClient, repo: string, teams: TeamEntry[]): Promise<string>;
|
|
178
|
-
//#endregion
|
|
179
|
-
//#region src/sodium.d.ts
|
|
200
|
+
export declare function normalizeTeamEntry(entry: TeamEntry): NormalizedTeamEntry;
|
|
201
|
+
export declare function syncTeams(client: GitHubClient, repo: string, teams: TeamEntry[]): Promise<string>;
|
|
180
202
|
/**
|
|
181
203
|
* Encrypt `value` for the given GitHub-provided base64 public key.
|
|
182
204
|
* Returns the base64 ciphertext to send as `encrypted_value`.
|
|
183
205
|
*/
|
|
184
|
-
declare function encryptSecret(publicKeyBase64: string, value: string): Promise<string>;
|
|
185
|
-
//#endregion
|
|
186
|
-
//#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
|
+
export declare function encryptSecret(publicKeyBase64: string, value: string): Promise<string>;
|
|
206
207
|
interface VerifyTokenOptions {
|
|
207
208
|
baseUrl?: string;
|
|
208
209
|
fetch?: typeof fetch;
|
|
209
210
|
}
|
|
210
|
-
declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult>;
|
|
211
|
-
|
|
212
|
-
//#region src/index.d.ts
|
|
213
|
-
interface GitHubPluginOptions extends ResolveTokenInput {
|
|
211
|
+
export declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult$1>;
|
|
212
|
+
export interface GitHubPluginOptions extends ResolveTokenInput {
|
|
214
213
|
/** "owner/name" — e.g., "theholocron/holocron". Required. */
|
|
215
214
|
repo: string;
|
|
216
215
|
/** Absolute path to the working repo root. Used by `source`'s
|
|
@@ -226,19 +225,22 @@ interface GitHubPluginOptions extends ResolveTokenInput {
|
|
|
226
225
|
/** Override `fetch` for tests. Defaults to global `fetch`. */
|
|
227
226
|
fetch?: typeof fetch;
|
|
228
227
|
}
|
|
229
|
-
interface PluginContext {
|
|
228
|
+
export interface PluginContext {
|
|
230
229
|
options: GitHubPluginOptions;
|
|
231
230
|
client: GitHubClient;
|
|
232
231
|
repo: string;
|
|
233
232
|
repoRoot: string;
|
|
234
233
|
}
|
|
235
|
-
declare function source(ctx: PluginContext, orgClient?: GitHubClient): Source;
|
|
236
|
-
declare function secrets(ctx: PluginContext): Secrets;
|
|
237
|
-
declare function environments(ctx: PluginContext): Environments;
|
|
238
|
-
declare function ci(ctx: PluginContext): Ci;
|
|
239
|
-
declare function issues(ctx: PluginContext): Issues;
|
|
240
|
-
declare function createPlugin(options: GitHubPluginOptions): {
|
|
234
|
+
export declare function source(ctx: PluginContext, orgClient?: GitHubClient, userSyncClient?: GitHubClient): Source;
|
|
235
|
+
export declare function secrets(ctx: PluginContext, orgClient?: GitHubClient): Secrets;
|
|
236
|
+
export declare function environments(ctx: PluginContext): Environments;
|
|
237
|
+
export declare function ci(ctx: PluginContext): Ci;
|
|
238
|
+
export declare function issues(ctx: PluginContext): Issues;
|
|
239
|
+
export declare function createPlugin(options: GitHubPluginOptions): {
|
|
241
240
|
name: string;
|
|
241
|
+
probes: {
|
|
242
|
+
connection: import("@theholocron/cli").Probe;
|
|
243
|
+
};
|
|
242
244
|
capabilities: {
|
|
243
245
|
source: () => Source;
|
|
244
246
|
ci: () => Ci;
|
|
@@ -251,6 +253,7 @@ declare function createPlugin(options: GitHubPluginOptions): {
|
|
|
251
253
|
* One-line hint printed by `holocron auth set github` when no token
|
|
252
254
|
* is supplied or the supplied token is rejected.
|
|
253
255
|
*/
|
|
254
|
-
declare const AUTH_HINT: string;
|
|
255
|
-
|
|
256
|
-
export
|
|
256
|
+
export declare const AUTH_HINT: string;
|
|
257
|
+
/** Every token this plugin takes, for `holocron doctor tokens`. */
|
|
258
|
+
export declare const tokens: PluginToken[];
|
|
259
|
+
export { type Auth, AuthError, type CanonicalLabel, type GitHubClientOptions, type NormalizedTeamEntry, type ResolveTokenInput, type TeamEntry, type TeamPermission, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createFeatureResolver, createGitHubClient };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { AuthError, MissingTokenError, ProviderApiError, connectionProbe, createFeatureResolver } from "@theholocron/cli";
|
|
2
3
|
import { createGitHubClient, createGitHubClient as createGitHubClient$1 } from "@theholocron/github-client";
|
|
3
|
-
import { AuthError, 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
|
-
|
|
6
|
+
import { findRootSync } from "@theholocron/fs-utils";
|
|
7
|
+
import { ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
|
|
8
|
+
import { errorMessage } from "@theholocron/misc-utils";
|
|
9
|
+
// src/auth.ts
|
|
7
10
|
const resolveReadToken = createFeatureResolver({
|
|
8
11
|
envName: "HOLOCRON_READ_TOKEN",
|
|
9
12
|
keyringKey: "github.read"
|
|
@@ -24,16 +27,23 @@ const resolveAdminToken = createFeatureResolver({
|
|
|
24
27
|
envName: "HOLOCRON_ADMIN_TOKEN",
|
|
25
28
|
keyringKey: "github.admin"
|
|
26
29
|
});
|
|
30
|
+
const resolveSecretsToken = createFeatureResolver({
|
|
31
|
+
envName: "HOLOCRON_SECRETS_TOKEN",
|
|
32
|
+
keyringKey: "github.secrets"
|
|
33
|
+
});
|
|
27
34
|
const resolveDeployToken = createFeatureResolver({
|
|
28
35
|
envName: "HOLOCRON_DEPLOY_TOKEN",
|
|
29
36
|
keyringKey: "github.deploy"
|
|
30
37
|
});
|
|
38
|
+
const resolveUserSyncToken = createFeatureResolver({
|
|
39
|
+
envName: "HOLOCRON_USER_SYNC_TOKEN",
|
|
40
|
+
keyringKey: "github.user-sync"
|
|
41
|
+
});
|
|
31
42
|
const resolveOrgToken = createFeatureResolver({
|
|
32
43
|
envName: "HOLOCRON_ORG_TOKEN",
|
|
33
44
|
keyringKey: "github.org"
|
|
34
45
|
});
|
|
35
|
-
|
|
36
|
-
//#region src/capabilities/ci.ts
|
|
46
|
+
// src/capabilities/ci.ts
|
|
37
47
|
var GitHubCi = class {
|
|
38
48
|
client;
|
|
39
49
|
opts;
|
|
@@ -77,8 +87,7 @@ var GitHubCi = class {
|
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
};
|
|
80
|
-
|
|
81
|
-
//#region src/capabilities/environments.ts
|
|
90
|
+
// src/capabilities/environments.ts
|
|
82
91
|
var GitHubEnvironments = class {
|
|
83
92
|
client;
|
|
84
93
|
opts;
|
|
@@ -119,8 +128,7 @@ var GitHubEnvironments = class {
|
|
|
119
128
|
}));
|
|
120
129
|
}
|
|
121
130
|
};
|
|
122
|
-
|
|
123
|
-
//#region src/capabilities/issues.ts
|
|
131
|
+
// src/capabilities/issues.ts
|
|
124
132
|
var GitHubIssues = class {
|
|
125
133
|
client;
|
|
126
134
|
key = "issues";
|
|
@@ -337,8 +345,7 @@ function parseIssueNumber(key, owner, repoName) {
|
|
|
337
345
|
if (!/^\d+$/.test(num)) throw new Error(`Invalid GitHub issue key "${key}" — expected #N or N.`);
|
|
338
346
|
return parseInt(num, 10);
|
|
339
347
|
}
|
|
340
|
-
|
|
341
|
-
//#region src/sodium.ts
|
|
348
|
+
// src/sodium.ts
|
|
342
349
|
/**
|
|
343
350
|
* libsodium sealed-box encryption for GitHub Actions secrets.
|
|
344
351
|
*
|
|
@@ -369,32 +376,42 @@ async function encryptSecret(publicKeyBase64, value) {
|
|
|
369
376
|
const ciphertext = sodium.crypto_box_seal(plaintext, publicKey);
|
|
370
377
|
return sodium.to_base64(ciphertext, sodium.base64_variants.ORIGINAL);
|
|
371
378
|
}
|
|
372
|
-
|
|
373
|
-
//#region src/capabilities/secrets.ts
|
|
379
|
+
// src/capabilities/secrets.ts
|
|
374
380
|
var GitHubSecrets = class {
|
|
375
381
|
client;
|
|
376
382
|
opts;
|
|
383
|
+
orgClient;
|
|
377
384
|
key = "secrets";
|
|
378
385
|
providerName = "github";
|
|
379
|
-
|
|
386
|
+
/**
|
|
387
|
+
* `client` handles repo and environment scopes; `orgClient`, when given, handles
|
|
388
|
+
* the organization scope. They differ when a dedicated org-only token exists:
|
|
389
|
+
* it can write org secrets but not repo ones, and `github.admin` is the reverse.
|
|
390
|
+
*/
|
|
391
|
+
constructor(client, opts, orgClient) {
|
|
380
392
|
this.client = client;
|
|
381
393
|
this.opts = opts;
|
|
394
|
+
this.orgClient = orgClient;
|
|
395
|
+
}
|
|
396
|
+
clientFor(scope) {
|
|
397
|
+
return scope.kind === "organization" ? this.orgClient ?? this.client : this.client;
|
|
382
398
|
}
|
|
383
399
|
async listSecrets(scope) {
|
|
384
|
-
return this.
|
|
400
|
+
return this.clientFor(scope).secrets.listSecrets(this.opts.repo, toGHScope(scope));
|
|
385
401
|
}
|
|
386
402
|
async setSecret(scope, name, value) {
|
|
387
403
|
const ghScope = toGHScope(scope);
|
|
388
|
-
const
|
|
404
|
+
const client = this.clientFor(scope);
|
|
405
|
+
const pk = await client.secrets.getPublicKey(this.opts.repo, ghScope);
|
|
389
406
|
const body = {
|
|
390
407
|
encrypted_value: await encryptSecret(pk.key, value),
|
|
391
408
|
key_id: pk.key_id
|
|
392
409
|
};
|
|
393
410
|
if (scope.kind === "organization") body.visibility = "all";
|
|
394
|
-
await
|
|
411
|
+
await client.secrets.putSecret(this.opts.repo, ghScope, name, body);
|
|
395
412
|
}
|
|
396
413
|
async deleteSecret(scope, name) {
|
|
397
|
-
await this.
|
|
414
|
+
await this.clientFor(scope).secrets.deleteSecret(this.opts.repo, toGHScope(scope), name);
|
|
398
415
|
}
|
|
399
416
|
};
|
|
400
417
|
function toGHScope(scope) {
|
|
@@ -404,8 +421,46 @@ function toGHScope(scope) {
|
|
|
404
421
|
};
|
|
405
422
|
return scope;
|
|
406
423
|
}
|
|
407
|
-
|
|
408
|
-
|
|
424
|
+
// src/capabilities/files.ts
|
|
425
|
+
/** Reads one file; `null` when it does not exist. `version` is the blob sha `writeFile` needs to replace exactly this one. */
|
|
426
|
+
/** A fine-grained token only reaches the repositories and permissions it was given; say which one is missing. */
|
|
427
|
+
function noAccess(repo, action, cause) {
|
|
428
|
+
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 });
|
|
429
|
+
}
|
|
430
|
+
async function readRepoFile(client, repo, path) {
|
|
431
|
+
try {
|
|
432
|
+
const file = await client.git.getContents(repo, path);
|
|
433
|
+
if (file.encoding !== "base64") throw new Error(`${repo}/${path}: unexpected encoding \`${file.encoding}\``);
|
|
434
|
+
return {
|
|
435
|
+
content: Buffer.from(file.content, "base64").toString("utf-8"),
|
|
436
|
+
version: file.sha
|
|
437
|
+
};
|
|
438
|
+
} catch (err) {
|
|
439
|
+
if (err instanceof ProviderApiError && err.status === 403) throw noAccess(repo, "read", err);
|
|
440
|
+
if (!(err instanceof ProviderApiError && err.status === 404)) throw err;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
await client.repos.getRepo(repo);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
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 });
|
|
446
|
+
throw err;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
/** Creates or replaces one file in a commit; GitHub refuses (409) a `version` that has since changed. */
|
|
451
|
+
async function writeRepoFile(client, repo, path, content, opts) {
|
|
452
|
+
try {
|
|
453
|
+
await client.git.putContents(repo, path, {
|
|
454
|
+
message: opts.message,
|
|
455
|
+
content,
|
|
456
|
+
...opts.version ? { sha: opts.version } : {}
|
|
457
|
+
});
|
|
458
|
+
} catch (err) {
|
|
459
|
+
if (err instanceof ProviderApiError && err.status === 403) throw noAccess(repo, "write", err);
|
|
460
|
+
throw err;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// src/capabilities/labels.ts
|
|
409
464
|
/**
|
|
410
465
|
* Idempotently synchronise a repo's labels to the canonical set.
|
|
411
466
|
*
|
|
@@ -443,14 +498,74 @@ async function syncLabels(client, repo, canonical, stale) {
|
|
|
443
498
|
}
|
|
444
499
|
return `${created} created, ${updated} updated, ${deleted} deleted`;
|
|
445
500
|
}
|
|
446
|
-
|
|
447
|
-
|
|
501
|
+
// src/capabilities/rejected-properties.ts
|
|
502
|
+
/** The values `repo setup` sends for `true_false` properties; anything else is rejected. */
|
|
503
|
+
const BOOLEANS = /* @__PURE__ */ new Set(["true", "false"]);
|
|
504
|
+
/**
|
|
505
|
+
* What is wrong with `values` when compared with the organization's property definitions, one sentence per
|
|
506
|
+
* problem, in the order the properties are given: a property the organization does not define, a value a
|
|
507
|
+
* `single_select` or `multi_select` property does not allow, or a `true_false` property that is not `true` or
|
|
508
|
+
* `false`. A `string` or `url` property has nothing to check here. Empty when every value fits its definition.
|
|
509
|
+
*/
|
|
510
|
+
function rejectedProperties(values, schema) {
|
|
511
|
+
const byName = new Map(schema.map((property) => [property.property_name, property]));
|
|
512
|
+
const problems = [];
|
|
513
|
+
for (const [name, value] of Object.entries(values)) {
|
|
514
|
+
const definition = byName.get(name);
|
|
515
|
+
if (!definition) {
|
|
516
|
+
problems.push(`${name} is not a custom property of the organization`);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const sent = Array.isArray(value) ? value : [value];
|
|
520
|
+
if (definition.value_type === "true_false") {
|
|
521
|
+
for (const item of sent.filter((item) => !BOOLEANS.has(item))) problems.push(`${name} is "${item}", but it takes only true or false`);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
const allowed = definition.allowed_values ?? [];
|
|
525
|
+
if (definition.value_type !== "single_select" && definition.value_type !== "multi_select") continue;
|
|
526
|
+
for (const item of sent.filter((item) => !allowed.includes(item))) problems.push(`${name} has "${item}", which is not one of its allowed values (${allowed.join(", ")})`);
|
|
527
|
+
}
|
|
528
|
+
return problems;
|
|
529
|
+
}
|
|
530
|
+
// src/capabilities/properties.ts
|
|
531
|
+
/** GitHub's own message from a 422 body (`{"message": ..., "errors": [...]}`), or the start of the body when it is not JSON. */
|
|
532
|
+
function githubMessage(details) {
|
|
533
|
+
const raw = String(details ?? "").trim();
|
|
534
|
+
try {
|
|
535
|
+
const body = JSON.parse(raw);
|
|
536
|
+
const errors = Array.isArray(body.errors) ? body.errors.map((e) => JSON.stringify(e)).join(", ") : "";
|
|
537
|
+
const message = typeof body.message === "string" ? body.message : raw;
|
|
538
|
+
return errors ? `${message}: ${errors}` : message;
|
|
539
|
+
} catch {
|
|
540
|
+
return raw.slice(0, 300);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/** The organization's property definitions, or `undefined` when they cannot be read (permission, an older client). */
|
|
544
|
+
async function orgSchema(client, repo) {
|
|
545
|
+
try {
|
|
546
|
+
return await client.properties.orgSchema(repo.split("/")[0]);
|
|
547
|
+
} catch {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Sets the repo's custom properties. When GitHub answers 422 the error says which value it refused: the values
|
|
553
|
+
* are compared with the organization's property definitions (read-only), and GitHub's own message is added.
|
|
554
|
+
* One refused value fails the whole request, so none of the properties is set.
|
|
555
|
+
*/
|
|
448
556
|
async function syncProperties(client, repo, values) {
|
|
449
|
-
|
|
557
|
+
try {
|
|
558
|
+
await client.properties.setProperties(repo, values);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
if (!(err instanceof ProviderApiError$1) || err.status !== 422) throw err;
|
|
561
|
+
const schema = await orgSchema(client, repo);
|
|
562
|
+
const problems = schema ? rejectedProperties(values, schema) : [];
|
|
563
|
+
const said = githubMessage(err.details);
|
|
564
|
+
throw new Error(`GitHub refused the custom properties (422), so none was set: ${problems.length > 0 ? problems.join("; ") : "no value differs from the organization's definitions, or they could not be read"}. GitHub said: ${said}`, { cause: err });
|
|
565
|
+
}
|
|
450
566
|
return `${Object.keys(values).length} properties set`;
|
|
451
567
|
}
|
|
452
|
-
|
|
453
|
-
//#region src/capabilities/teams.ts
|
|
568
|
+
// src/capabilities/teams.ts
|
|
454
569
|
function normalizeTeamEntry(entry) {
|
|
455
570
|
if (typeof entry === "string") return {
|
|
456
571
|
slug: entry,
|
|
@@ -470,14 +585,123 @@ async function syncTeams(client, repo, teams) {
|
|
|
470
585
|
}
|
|
471
586
|
return `${succeeded} team${succeeded === 1 ? "" : "s"} synced`;
|
|
472
587
|
}
|
|
473
|
-
|
|
474
|
-
//#region src/capabilities/topics.ts
|
|
588
|
+
// src/capabilities/topics.ts
|
|
475
589
|
async function syncTopics(client, repo, topics) {
|
|
476
590
|
await client.topics.setTopics(repo, topics);
|
|
477
591
|
return `${topics.length} topics set`;
|
|
478
592
|
}
|
|
479
|
-
|
|
480
|
-
|
|
593
|
+
// src/capabilities/variables.ts
|
|
594
|
+
/** GitHub's rule for a variable name: letters, digits and underscores, not starting with a digit or `GITHUB_`. */
|
|
595
|
+
const NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
596
|
+
/** Longest name GitHub accepts. */
|
|
597
|
+
const MAX_NAME = 256;
|
|
598
|
+
function assertNames(names) {
|
|
599
|
+
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_`);
|
|
600
|
+
}
|
|
601
|
+
/** GitHub treats variable names as case-insensitive, so every comparison goes through this. */
|
|
602
|
+
const key = (name) => name.toUpperCase();
|
|
603
|
+
/**
|
|
604
|
+
* Bring one scope's variables in line with the declaration: create what is missing, update what differs, leave what
|
|
605
|
+
* already matches. Variables that exist but are not declared are left alone, or deleted with `prune`, except an
|
|
606
|
+
* organization variable limited to selected repositories, which this client cannot manage and so is never deleted.
|
|
607
|
+
* Returns a one-line summary, naming the ones left so drift is visible. In a dry run it only describes the changes.
|
|
608
|
+
*/
|
|
609
|
+
async function reconcile(client, repo, scope, wanted, opts) {
|
|
610
|
+
assertNames(wanted.map((w) => w.name));
|
|
611
|
+
const existing = new Map((await client.variables.listVariables(repo, scope)).map((v) => [key(v.name), v]));
|
|
612
|
+
const counts = {
|
|
613
|
+
created: 0,
|
|
614
|
+
updated: 0,
|
|
615
|
+
unchanged: 0,
|
|
616
|
+
deleted: 0
|
|
617
|
+
};
|
|
618
|
+
for (const want of wanted) {
|
|
619
|
+
const have = existing.get(key(want.name));
|
|
620
|
+
const body = {
|
|
621
|
+
name: want.name,
|
|
622
|
+
value: want.value
|
|
623
|
+
};
|
|
624
|
+
if (want.visibility) body["visibility"] = want.visibility;
|
|
625
|
+
if (!have) {
|
|
626
|
+
if (opts.dryRun) {
|
|
627
|
+
counts.created++;
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
await client.variables.createVariable(repo, scope, body);
|
|
632
|
+
counts.created++;
|
|
633
|
+
continue;
|
|
634
|
+
} catch (err) {
|
|
635
|
+
if (!(err instanceof ProviderApiError && (err.status === 409 || err.status === 422))) throw err;
|
|
636
|
+
}
|
|
637
|
+
} else if (have.value === want.value && (!want.visibility || have.visibility === want.visibility)) {
|
|
638
|
+
counts.unchanged++;
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (!opts.dryRun) await client.variables.updateVariable(repo, scope, have?.name ?? want.name, body);
|
|
642
|
+
counts.updated++;
|
|
643
|
+
}
|
|
644
|
+
const declared = new Set(wanted.map((w) => key(w.name)));
|
|
645
|
+
const undeclared = [...existing.entries()].filter(([k]) => !declared.has(k)).map(([, v]) => v);
|
|
646
|
+
const unmanaged = undeclared.filter((v) => v.visibility === "selected");
|
|
647
|
+
if (opts.prune) for (const v of undeclared.filter((u) => u.visibility !== "selected")) {
|
|
648
|
+
if (!opts.dryRun) await client.variables.deleteVariable(repo, scope, v.name);
|
|
649
|
+
counts.deleted++;
|
|
650
|
+
}
|
|
651
|
+
const parts = opts.dryRun ? [
|
|
652
|
+
`would create ${counts.created}`,
|
|
653
|
+
`would update ${counts.updated}`,
|
|
654
|
+
`${counts.unchanged} unchanged`
|
|
655
|
+
] : [
|
|
656
|
+
`${counts.created} created`,
|
|
657
|
+
`${counts.updated} updated`,
|
|
658
|
+
`${counts.unchanged} unchanged`
|
|
659
|
+
];
|
|
660
|
+
if (counts.deleted > 0) parts.push(`${counts.deleted} ${opts.dryRun ? "would be deleted" : "deleted"}`);
|
|
661
|
+
const left = opts.prune ? unmanaged : undeclared;
|
|
662
|
+
if (left.length > 0) {
|
|
663
|
+
const why = opts.prune ? "limited to selected repositories, not managed" : "not declared, left alone";
|
|
664
|
+
parts.push(`${left.length} ${why}: ${left.map((v) => v.name).join(", ")}`);
|
|
665
|
+
}
|
|
666
|
+
return parts.join(", ");
|
|
667
|
+
}
|
|
668
|
+
/** Reconcile a repository's Actions variables. */
|
|
669
|
+
function syncVariables(client, repo, variables, opts = {}) {
|
|
670
|
+
return reconcile(client, repo, { kind: "repo" }, Object.entries(variables).map(([name, value]) => ({
|
|
671
|
+
name,
|
|
672
|
+
value
|
|
673
|
+
})), {
|
|
674
|
+
prune: opts.prune === true,
|
|
675
|
+
dryRun: opts.dryRun === true
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
/** Reconcile an organization's Actions variables. `repo` is only the coordinate the client is bound to. */
|
|
679
|
+
function syncOrgVariables(client, repo, org, variables, opts = {}) {
|
|
680
|
+
const wanted = Object.entries(variables).map(([name, v]) => ({
|
|
681
|
+
name,
|
|
682
|
+
value: v.value,
|
|
683
|
+
visibility: v.visibility
|
|
684
|
+
}));
|
|
685
|
+
return reconcile(client, repo, {
|
|
686
|
+
kind: "organization",
|
|
687
|
+
org
|
|
688
|
+
}, wanted, {
|
|
689
|
+
prune: opts.prune === true,
|
|
690
|
+
dryRun: opts.dryRun === true
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
// src/capabilities/source.ts
|
|
694
|
+
function toPullRequest(raw) {
|
|
695
|
+
return {
|
|
696
|
+
number: raw.number,
|
|
697
|
+
title: raw.title,
|
|
698
|
+
state: raw.state,
|
|
699
|
+
merged: raw.merged_at !== null,
|
|
700
|
+
branch: raw.head.ref,
|
|
701
|
+
headSha: raw.head.sha,
|
|
702
|
+
url: raw.html_url
|
|
703
|
+
};
|
|
704
|
+
}
|
|
481
705
|
var GitHubSource = class {
|
|
482
706
|
client;
|
|
483
707
|
key = "source";
|
|
@@ -488,6 +712,7 @@ var GitHubSource = class {
|
|
|
488
712
|
repoRoot;
|
|
489
713
|
workflowDir;
|
|
490
714
|
orgClient;
|
|
715
|
+
userSyncClient;
|
|
491
716
|
baseUrl;
|
|
492
717
|
fetchImpl;
|
|
493
718
|
constructor(client, opts) {
|
|
@@ -496,9 +721,13 @@ var GitHubSource = class {
|
|
|
496
721
|
this.owner = owner;
|
|
497
722
|
this.name = name;
|
|
498
723
|
this.repo = opts.repo;
|
|
499
|
-
this.repoRoot = opts.repoRoot
|
|
500
|
-
|
|
724
|
+
this.repoRoot = findRootSync(opts.repoRoot, ".git", {
|
|
725
|
+
fallback: "start",
|
|
726
|
+
realpath: true
|
|
727
|
+
});
|
|
728
|
+
this.workflowDir = join(this.repoRoot, ".github", "workflows");
|
|
501
729
|
this.orgClient = opts.orgClient;
|
|
730
|
+
this.userSyncClient = opts.userSyncClient;
|
|
502
731
|
this.baseUrl = opts.baseUrl;
|
|
503
732
|
this.fetchImpl = opts.fetch;
|
|
504
733
|
}
|
|
@@ -591,9 +820,43 @@ var GitHubSource = class {
|
|
|
591
820
|
async syncTeams(teams) {
|
|
592
821
|
return syncTeams(this.orgClient ?? this.client, this.repo, teams);
|
|
593
822
|
}
|
|
823
|
+
async teamMembers(teamSlug, org = this.owner) {
|
|
824
|
+
return (await (this.orgClient ?? this.client).teams.members(org, teamSlug)).map((m) => ({
|
|
825
|
+
id: String(m.id),
|
|
826
|
+
login: m.login
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
async readFile(path, repo = this.repo) {
|
|
830
|
+
return readRepoFile(this.userSyncClient ?? this.orgClient ?? this.client, repo, path);
|
|
831
|
+
}
|
|
832
|
+
async writeFile(path, content, opts) {
|
|
833
|
+
return writeRepoFile(this.userSyncClient ?? this.orgClient ?? this.client, opts.repo ?? this.repo, path, content, opts);
|
|
834
|
+
}
|
|
594
835
|
async syncTopics(topics) {
|
|
595
836
|
return syncTopics(this.client, this.repo, topics);
|
|
596
837
|
}
|
|
838
|
+
async syncVariables(variables, opts) {
|
|
839
|
+
return syncVariables(this.client, this.repo, variables, opts);
|
|
840
|
+
}
|
|
841
|
+
async syncOrgVariables(org, variables, opts) {
|
|
842
|
+
if (!this.orgClient) throw new Error("organization variables need the org token: set HOLOCRON_ORG_TOKEN or run `holocron auth set github.org <token>`");
|
|
843
|
+
return syncOrgVariables(this.orgClient, this.repo, org, variables, opts);
|
|
844
|
+
}
|
|
845
|
+
async listVariables() {
|
|
846
|
+
const found = await this.client.variables.listVariables(this.repo, { kind: "repo" });
|
|
847
|
+
return Object.fromEntries(found.map((v) => [v.name, v.value]));
|
|
848
|
+
}
|
|
849
|
+
async listOrgVariables(org) {
|
|
850
|
+
if (!this.orgClient) throw new MissingTokenError("organization variables need the org token: set HOLOCRON_ORG_TOKEN or run `holocron auth set github.org <token>`");
|
|
851
|
+
const found = await this.orgClient.variables.listVariables(this.repo, {
|
|
852
|
+
kind: "organization",
|
|
853
|
+
org
|
|
854
|
+
});
|
|
855
|
+
return Object.fromEntries(found.map((v) => [v.name, {
|
|
856
|
+
value: v.value,
|
|
857
|
+
visibility: v.visibility ?? "all"
|
|
858
|
+
}]));
|
|
859
|
+
}
|
|
597
860
|
async syncDescription(description) {
|
|
598
861
|
await this.client.repos.updateRepo(this.repo, { description });
|
|
599
862
|
return "description updated";
|
|
@@ -603,15 +866,27 @@ var GitHubSource = class {
|
|
|
603
866
|
return "homepage updated";
|
|
604
867
|
}
|
|
605
868
|
async getPullRequest(number, repo) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
869
|
+
return toPullRequest(await this.client.pulls.getPullRequest(repo ?? this.repo, number));
|
|
870
|
+
}
|
|
871
|
+
async listPullRequests(state, repo) {
|
|
872
|
+
return (await this.client.pulls.listPullRequests(repo ?? this.repo, state)).map(toPullRequest);
|
|
873
|
+
}
|
|
874
|
+
async listBranches(repo) {
|
|
875
|
+
return (await this.client.branches.listBranches(repo ?? this.repo)).map((b) => ({
|
|
876
|
+
name: b.name,
|
|
877
|
+
sha: b.commit.sha
|
|
878
|
+
}));
|
|
879
|
+
}
|
|
880
|
+
async deleteBranch(name, repo) {
|
|
881
|
+
try {
|
|
882
|
+
await this.client.branches.removeBranch(repo ?? this.repo, name);
|
|
883
|
+
} catch (err) {
|
|
884
|
+
if (err instanceof ProviderApiError && err.status === 422 && String(err.details).includes("does not exist")) return;
|
|
885
|
+
throw err;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
async listPullRequestCommits(number, repo) {
|
|
889
|
+
return (await this.client.pulls.listCommits(repo ?? this.repo, number)).map((commit) => commit.sha);
|
|
615
890
|
}
|
|
616
891
|
async configurePages(config, token) {
|
|
617
892
|
const pagesClient = createGitHubClient$1({
|
|
@@ -650,18 +925,7 @@ async function ensureDir(path) {
|
|
|
650
925
|
throw err;
|
|
651
926
|
}
|
|
652
927
|
}
|
|
653
|
-
|
|
654
|
-
//#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
|
-
*/
|
|
928
|
+
// src/verify-token.ts
|
|
665
929
|
async function verifyToken(token, opts = {}) {
|
|
666
930
|
const client = createGitHubClient({
|
|
667
931
|
token,
|
|
@@ -669,31 +933,32 @@ async function verifyToken(token, opts = {}) {
|
|
|
669
933
|
fetch: opts.fetch
|
|
670
934
|
});
|
|
671
935
|
try {
|
|
672
|
-
const me = await client.user.
|
|
936
|
+
const { user: me, expiresAt } = await client.user.getCurrentUserWithExpiry();
|
|
673
937
|
return {
|
|
674
938
|
ok: true,
|
|
675
|
-
subject: `user @ ${me.login ?? me.email ?? "unknown"}
|
|
939
|
+
subject: `user @ ${me.login ?? me.email ?? "unknown"}`,
|
|
940
|
+
...expiresAt ? { expiresAt } : {}
|
|
676
941
|
};
|
|
677
942
|
} catch (err) {
|
|
678
943
|
return {
|
|
679
944
|
ok: false,
|
|
680
|
-
message:
|
|
945
|
+
message: errorMessage(err)
|
|
681
946
|
};
|
|
682
947
|
}
|
|
683
948
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
function source(ctx, orgClient) {
|
|
949
|
+
// src/index.ts
|
|
950
|
+
function source(ctx, orgClient, userSyncClient) {
|
|
687
951
|
return new GitHubSource(ctx.client, {
|
|
688
952
|
repo: ctx.repo,
|
|
689
953
|
repoRoot: ctx.repoRoot,
|
|
690
954
|
orgClient,
|
|
955
|
+
userSyncClient,
|
|
691
956
|
baseUrl: ctx.options.baseUrl,
|
|
692
957
|
fetch: ctx.options.fetch
|
|
693
958
|
});
|
|
694
959
|
}
|
|
695
|
-
function secrets(ctx) {
|
|
696
|
-
return new GitHubSecrets(ctx.client, { repo: ctx.repo });
|
|
960
|
+
function secrets(ctx, orgClient) {
|
|
961
|
+
return new GitHubSecrets(ctx.client, { repo: ctx.repo }, orgClient);
|
|
697
962
|
}
|
|
698
963
|
function environments(ctx) {
|
|
699
964
|
return new GitHubEnvironments(ctx.client, { repo: ctx.repo });
|
|
@@ -708,10 +973,11 @@ function issues(ctx) {
|
|
|
708
973
|
}
|
|
709
974
|
function createPlugin(options) {
|
|
710
975
|
function makeCtx(resolver) {
|
|
976
|
+
const token = resolver(options);
|
|
711
977
|
return {
|
|
712
978
|
options,
|
|
713
979
|
client: createGitHubClient$1({
|
|
714
|
-
token
|
|
980
|
+
token,
|
|
715
981
|
baseUrl: options.baseUrl,
|
|
716
982
|
fetch: options.fetch
|
|
717
983
|
}),
|
|
@@ -719,10 +985,45 @@ function createPlugin(options) {
|
|
|
719
985
|
repoRoot: options.repoRoot ?? process.cwd()
|
|
720
986
|
};
|
|
721
987
|
}
|
|
988
|
+
/** A token, or undefined when that feature token is simply not configured. */
|
|
989
|
+
function tryResolve(resolver) {
|
|
990
|
+
try {
|
|
991
|
+
return resolver(options);
|
|
992
|
+
} catch (err) {
|
|
993
|
+
if (err instanceof AuthError) return void 0;
|
|
994
|
+
throw err;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Organization secrets need an org-resource-owner token (`github.secrets`);
|
|
999
|
+
* repo and environment secrets need `github.admin`. Each scope prefers its own
|
|
1000
|
+
* token and falls back to the other, so a setup with only one of them keeps
|
|
1001
|
+
* working, and an explicit `--token` serves both. With neither configured this
|
|
1002
|
+
* fails exactly as the admin-token capabilities do.
|
|
1003
|
+
*/
|
|
1004
|
+
function makeSecrets() {
|
|
1005
|
+
const secretsToken = tryResolve(resolveSecretsToken);
|
|
1006
|
+
const adminToken = tryResolve(resolveAdminToken);
|
|
1007
|
+
if (!secretsToken && !adminToken) resolveAdminToken(options);
|
|
1008
|
+
const clientFor = (token) => createGitHubClient$1({
|
|
1009
|
+
token,
|
|
1010
|
+
baseUrl: options.baseUrl,
|
|
1011
|
+
fetch: options.fetch
|
|
1012
|
+
});
|
|
1013
|
+
const repoClient = clientFor(adminToken ?? secretsToken);
|
|
1014
|
+
const orgClient = clientFor(secretsToken ?? adminToken);
|
|
1015
|
+
return secrets({
|
|
1016
|
+
options,
|
|
1017
|
+
client: repoClient,
|
|
1018
|
+
repo: options.repo,
|
|
1019
|
+
repoRoot: options.repoRoot ?? process.cwd()
|
|
1020
|
+
}, orgClient);
|
|
1021
|
+
}
|
|
722
1022
|
function makeOrgClient() {
|
|
723
1023
|
try {
|
|
1024
|
+
const token = resolveOrgToken(options);
|
|
724
1025
|
return createGitHubClient$1({
|
|
725
|
-
token
|
|
1026
|
+
token,
|
|
726
1027
|
baseUrl: options.baseUrl,
|
|
727
1028
|
fetch: options.fetch
|
|
728
1029
|
});
|
|
@@ -730,12 +1031,31 @@ function createPlugin(options) {
|
|
|
730
1031
|
return;
|
|
731
1032
|
}
|
|
732
1033
|
}
|
|
1034
|
+
/** The token for the user-sync files; undefined when it is not configured, and the org token is used instead. */
|
|
1035
|
+
function makeUserSyncClient() {
|
|
1036
|
+
const token = tryResolve(resolveUserSyncToken);
|
|
1037
|
+
return token ? createGitHubClient$1({
|
|
1038
|
+
token,
|
|
1039
|
+
baseUrl: options.baseUrl,
|
|
1040
|
+
fetch: options.fetch
|
|
1041
|
+
}) : void 0;
|
|
1042
|
+
}
|
|
733
1043
|
return {
|
|
734
1044
|
name: "@theholocron/holocron-plugin-github",
|
|
1045
|
+
probes: { connection: connectionProbe({
|
|
1046
|
+
provider: "GitHub",
|
|
1047
|
+
url: options.baseUrl ?? "https://api.github.com",
|
|
1048
|
+
fetch: options.fetch,
|
|
1049
|
+
token: () => resolveReadToken(options),
|
|
1050
|
+
verify: (token) => verifyToken(token, {
|
|
1051
|
+
baseUrl: options.baseUrl,
|
|
1052
|
+
fetch: options.fetch
|
|
1053
|
+
})
|
|
1054
|
+
}) },
|
|
735
1055
|
capabilities: {
|
|
736
|
-
source: () => source(makeCtx(resolveAdminToken), makeOrgClient()),
|
|
1056
|
+
source: () => source(makeCtx(resolveAdminToken), makeOrgClient(), makeUserSyncClient()),
|
|
737
1057
|
ci: () => ci(makeCtx(resolveReadToken)),
|
|
738
|
-
secrets: () =>
|
|
1058
|
+
secrets: () => makeSecrets(),
|
|
739
1059
|
environments: () => environments(makeCtx(resolveAdminToken)),
|
|
740
1060
|
issues: () => issues(makeCtx(resolveIssuesToken))
|
|
741
1061
|
}
|
|
@@ -746,5 +1066,16 @@ function createPlugin(options) {
|
|
|
746
1066
|
* is supplied or the supplied token is rejected.
|
|
747
1067
|
*/
|
|
748
1068
|
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
|
-
|
|
750
|
-
|
|
1069
|
+
/** Every token this plugin takes, for `holocron doctor tokens`. */
|
|
1070
|
+
const tokens = [
|
|
1071
|
+
resolveReadToken,
|
|
1072
|
+
resolveIssuesToken,
|
|
1073
|
+
resolveSyncToken,
|
|
1074
|
+
resolveReleaseToken,
|
|
1075
|
+
resolveAdminToken,
|
|
1076
|
+
resolveSecretsToken,
|
|
1077
|
+
resolveDeployToken,
|
|
1078
|
+
resolveUserSyncToken,
|
|
1079
|
+
resolveOrgToken
|
|
1080
|
+
].map((resolver) => resolver.token);
|
|
1081
|
+
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, tokens, 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.270",
|
|
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://
|
|
12
|
+
"homepage": "https://docs.theholocron.dev/holocron/plugins/github/",
|
|
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,27 +34,31 @@
|
|
|
34
34
|
"libsodium-wrappers": "^0.8.4"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@theholocron/eslint-config": "^8.0.0",
|
|
38
|
-
"@theholocron/github-client": "^1.23.0",
|
|
39
|
-
"@theholocron/http-client": "^1.23.0",
|
|
40
|
-
"@theholocron/tsconfig": "^8.0.0",
|
|
41
|
-
"@theholocron/tsdown-config": "^8.0.0",
|
|
42
|
-
"@theholocron/vitest-config": "^8.0.0",
|
|
43
37
|
"@types/node": "^26",
|
|
44
|
-
"@vitest/coverage-v8": "^
|
|
38
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
45
39
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
46
40
|
"eslint": "^10.8.1",
|
|
47
41
|
"eslint-plugin-n": "^18.3.0",
|
|
48
42
|
"globals": "^17.11.0",
|
|
49
|
-
"tsdown": "^0.
|
|
50
|
-
"tsx": "4.23.
|
|
43
|
+
"tsdown": "^0.23.3",
|
|
44
|
+
"tsx": "4.23.15",
|
|
51
45
|
"typescript": "^5.9.3",
|
|
52
|
-
"vitest": "^
|
|
53
|
-
"@theholocron/
|
|
46
|
+
"vitest": "^5.0.5",
|
|
47
|
+
"@theholocron/eslint-config": "^8.9.0-alpha.3",
|
|
48
|
+
"@theholocron/fs-utils": "^1.16.1-alpha.1",
|
|
49
|
+
"@theholocron/cli": "5.0.0-alpha.270",
|
|
50
|
+
"@theholocron/github-client": "^1.58.0-alpha.2",
|
|
51
|
+
"@theholocron/http-client": "^1.58.0-alpha.2",
|
|
52
|
+
"@theholocron/misc-utils": "^1.16.1-alpha.1",
|
|
53
|
+
"@theholocron/tsconfig": "^8.9.0-alpha.3",
|
|
54
|
+
"@theholocron/tsdown-config": "^8.9.0-alpha.3",
|
|
55
|
+
"@theholocron/vitest-config": "^8.9.0-alpha.3"
|
|
54
56
|
},
|
|
55
57
|
"peerDependencies": {
|
|
56
|
-
"@theholocron/
|
|
57
|
-
"@theholocron/
|
|
58
|
+
"@theholocron/fs-utils": "^1.16.0",
|
|
59
|
+
"@theholocron/github-client": "^1.58.0",
|
|
60
|
+
"@theholocron/misc-utils": "^1.16.0",
|
|
61
|
+
"@theholocron/cli": "5.0.0-alpha.270"
|
|
58
62
|
},
|
|
59
63
|
"engines": {
|
|
60
64
|
"node": ">=22"
|
|
@@ -64,10 +68,10 @@
|
|
|
64
68
|
},
|
|
65
69
|
"scripts": {
|
|
66
70
|
"delivery.build": "tsdown",
|
|
67
|
-
"sourceQuality.staticAnalysis": "
|
|
71
|
+
"sourceQuality.staticAnalysis": "eslint .",
|
|
68
72
|
"test:watch": "vitest",
|
|
69
73
|
"validate": "tsx scripts/validate.mjs",
|
|
70
|
-
"verification.typeSafety": "
|
|
71
|
-
"verification.unitTests": "
|
|
74
|
+
"verification.typeSafety": "tsc --noEmit",
|
|
75
|
+
"verification.unitTests": "vitest run"
|
|
72
76
|
}
|
|
73
77
|
}
|