@theholocron/holocron-plugin-github 2.0.0-alpha.7 → 2.0.0-alpha.71
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 +11 -7
- package/dist/index.d.mts +51 -86
- package/dist/index.mjs +209 -289
- package/package.json +17 -11
package/README.md
CHANGED
|
@@ -13,8 +13,10 @@ against the GitHub REST API:
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
+
<!-- prettier-ignore -->
|
|
16
17
|
```bash
|
|
17
18
|
pnpm add -D @theholocron/holocron-plugin-github@alpha
|
|
19
|
+
|
|
18
20
|
```
|
|
19
21
|
|
|
20
22
|
## Auth
|
|
@@ -33,17 +35,19 @@ toggles, etc.) — silent fallback would surface as mysterious 403s.
|
|
|
33
35
|
|
|
34
36
|
## Config
|
|
35
37
|
|
|
38
|
+
<!-- prettier-ignore -->
|
|
36
39
|
```jsonc
|
|
37
40
|
// holocron.config.json
|
|
38
41
|
{
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
"providers": {
|
|
43
|
+
"source": "github",
|
|
44
|
+
"ci": "github",
|
|
45
|
+
"secrets": "github",
|
|
46
|
+
"environments": "github",
|
|
47
|
+
"issues": ["github", { "labels": { "inProgress": "status:in-progress", "inReview": "status:in-review" } }],
|
|
48
|
+
},
|
|
46
49
|
}
|
|
50
|
+
|
|
47
51
|
```
|
|
48
52
|
|
|
49
53
|
## Status
|
package/dist/index.d.mts
CHANGED
|
@@ -1,62 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GitHubClient, createGitHubClient } from "@theholocron/github-client";
|
|
2
|
+
import { Auth, AuthError, Ci, CiRun, CiRunFilter, Environment, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, RepoRef, RepoSettings, ResolveTokenInput, Ruleset, SecretScope, Secrets, Source, TeamEntry, TeamEntry as TeamEntry$1, TeamPermission, TrackerDoctorReport, TrackerUser } from "@theholocron/cli";
|
|
2
3
|
|
|
3
4
|
//#region src/auth.d.ts
|
|
4
|
-
|
|
5
|
-
* Token resolution for the GitHub plugin. See README §Auth.
|
|
6
|
-
*
|
|
7
|
-
* Resolution order (matches the standard 4-step precedence set by
|
|
8
|
-
* `.notes/tech-auth-bootstrap.spec.md`):
|
|
9
|
-
* 1. explicit `token` argument (from `--token` flag)
|
|
10
|
-
* 2. HOLOCRON_GH_TOKEN env var (preferred over GITHUB_TOKEN — clearer intent)
|
|
11
|
-
* 3. GITHUB_TOKEN env var (auto-injected in GH Actions)
|
|
12
|
-
* 4. keyring (com.theholocron.cli / "github")
|
|
13
|
-
* 5. AuthError naming all four options + the bootstrap hint
|
|
14
|
-
*
|
|
15
|
-
* No `gh auth token` fallback by design — it usually has narrower
|
|
16
|
-
* scopes than admin commands need.
|
|
17
|
-
*/
|
|
18
|
-
declare class AuthError extends Error {
|
|
19
|
-
name: string;
|
|
20
|
-
}
|
|
21
|
-
interface ResolveTokenInput {
|
|
22
|
-
/** From `--token` CLI flag. */
|
|
23
|
-
cliToken?: string;
|
|
24
|
-
/** Env vars; passed in for testability. Defaults to `process.env`. */
|
|
25
|
-
env?: NodeJS.ProcessEnv;
|
|
26
|
-
/** Keyring lookup fn; passed in for testability. Defaults to `getToken(provider)`. */
|
|
27
|
-
keyring?: (provider: string) => string | null;
|
|
28
|
-
}
|
|
29
|
-
declare function resolveToken(input?: ResolveTokenInput): string;
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/rest.d.ts
|
|
32
|
-
/**
|
|
33
|
-
* Thin REST wrapper around api.github.com.
|
|
34
|
-
*
|
|
35
|
-
* Adapted from rando-id/rando.id `packages/cli/src/adapters/gh-rest.ts`
|
|
36
|
-
* `request<T>` method into a standalone class so each capability can
|
|
37
|
-
* hold a reference and share the same auth/error handling.
|
|
38
|
-
*
|
|
39
|
-
* Errors throw `ProviderApiError` from `@theholocron/cli`, which the
|
|
40
|
-
* orchestrator catches to soft-skip rather than abort the whole pipeline.
|
|
41
|
-
*/
|
|
42
|
-
interface RestClientOptions {
|
|
43
|
-
token: string;
|
|
44
|
-
fetch?: typeof fetch;
|
|
45
|
-
baseUrl?: string;
|
|
46
|
-
}
|
|
47
|
-
interface RequestOptions {
|
|
48
|
-
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
|
49
|
-
body?: unknown;
|
|
50
|
-
/** Skip JSON parse when true (204 No Content endpoints). */
|
|
51
|
-
expectNoContent?: boolean;
|
|
52
|
-
}
|
|
53
|
-
declare class GitHubRestClient {
|
|
54
|
-
private readonly token;
|
|
55
|
-
private readonly fetchImpl;
|
|
56
|
-
readonly baseUrl: string;
|
|
57
|
-
constructor(opts: RestClientOptions);
|
|
58
|
-
request<T>(path: string, opts?: RequestOptions): Promise<T>;
|
|
59
|
-
}
|
|
5
|
+
declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
|
|
60
6
|
//#endregion
|
|
61
7
|
//#region src/sodium.d.ts
|
|
62
8
|
/**
|
|
@@ -68,19 +14,18 @@ declare function encryptSecret(publicKeyBase64: string, value: string): Promise<
|
|
|
68
14
|
//#region src/capabilities/source.d.ts
|
|
69
15
|
interface SourceOptions {
|
|
70
16
|
repo: string;
|
|
71
|
-
/** Absolute path to the repo root. Workflow file ops are scoped here. */
|
|
72
17
|
repoRoot: string;
|
|
73
18
|
}
|
|
74
19
|
declare class GitHubSource implements Source {
|
|
75
|
-
private readonly
|
|
20
|
+
private readonly client;
|
|
76
21
|
readonly key: "source";
|
|
77
22
|
readonly providerName = "github";
|
|
78
23
|
private readonly owner;
|
|
79
24
|
private readonly name;
|
|
80
|
-
private readonly
|
|
25
|
+
private readonly repo;
|
|
81
26
|
private readonly repoRoot;
|
|
82
27
|
private readonly workflowDir;
|
|
83
|
-
constructor(
|
|
28
|
+
constructor(client: GitHubClient, opts: SourceOptions);
|
|
84
29
|
whoami(): Promise<{
|
|
85
30
|
login: string;
|
|
86
31
|
}>;
|
|
@@ -94,6 +39,7 @@ declare class GitHubSource implements Source {
|
|
|
94
39
|
enableAutomatedSecurityFixes(): Promise<void>;
|
|
95
40
|
enableSecretScanning(): Promise<void>;
|
|
96
41
|
enableCodeScanning(): Promise<string>;
|
|
42
|
+
disableDefaultCodeScanning(): Promise<void>;
|
|
97
43
|
enableDependencyGraph(): Promise<void>;
|
|
98
44
|
enablePrivateVulnerabilityReporting(): Promise<void>;
|
|
99
45
|
listWorkflowFiles(): Promise<string[]>;
|
|
@@ -101,6 +47,11 @@ declare class GitHubSource implements Source {
|
|
|
101
47
|
writeWorkflowFile(name: string, contents: string): Promise<void>;
|
|
102
48
|
removeWorkflowFile(name: string): Promise<void>;
|
|
103
49
|
writeRepoFile(path: string, contents: string): Promise<void>;
|
|
50
|
+
syncLabels(canonical: ReadonlyArray<LabelDef>, stale: ReadonlyArray<string>): Promise<string>;
|
|
51
|
+
syncProperties(values: Record<string, string>): Promise<string>;
|
|
52
|
+
syncTeams(teams: TeamEntry$1[]): Promise<string>;
|
|
53
|
+
syncTopics(topics: string[]): Promise<string>;
|
|
54
|
+
syncDescription(description: string): Promise<string>;
|
|
104
55
|
}
|
|
105
56
|
//#endregion
|
|
106
57
|
//#region src/capabilities/secrets.d.ts
|
|
@@ -108,16 +59,14 @@ interface SecretsOptions {
|
|
|
108
59
|
repo: string;
|
|
109
60
|
}
|
|
110
61
|
declare class GitHubSecrets implements Secrets {
|
|
111
|
-
private readonly
|
|
62
|
+
private readonly client;
|
|
63
|
+
private readonly opts;
|
|
112
64
|
readonly key: "secrets";
|
|
113
65
|
readonly providerName = "github";
|
|
114
|
-
|
|
115
|
-
constructor(rest: GitHubRestClient, opts: SecretsOptions);
|
|
66
|
+
constructor(client: GitHubClient, opts: SecretsOptions);
|
|
116
67
|
listSecrets(scope: SecretScope): Promise<string[]>;
|
|
117
68
|
setSecret(scope: SecretScope, name: string, value: string): Promise<void>;
|
|
118
69
|
deleteSecret(scope: SecretScope, name: string): Promise<void>;
|
|
119
|
-
/** Endpoint root for the given scope. */
|
|
120
|
-
private scopeBase;
|
|
121
70
|
}
|
|
122
71
|
//#endregion
|
|
123
72
|
//#region src/capabilities/environments.d.ts
|
|
@@ -125,11 +74,11 @@ interface EnvironmentsOptions {
|
|
|
125
74
|
repo: string;
|
|
126
75
|
}
|
|
127
76
|
declare class GitHubEnvironments implements Environments {
|
|
128
|
-
private readonly
|
|
77
|
+
private readonly client;
|
|
78
|
+
private readonly opts;
|
|
129
79
|
readonly key: "environments";
|
|
130
80
|
readonly providerName = "github";
|
|
131
|
-
|
|
132
|
-
constructor(rest: GitHubRestClient, opts: EnvironmentsOptions);
|
|
81
|
+
constructor(client: GitHubClient, opts: EnvironmentsOptions);
|
|
133
82
|
listEnvironments(): Promise<Environment[]>;
|
|
134
83
|
upsertEnvironment(env: Environment): Promise<void>;
|
|
135
84
|
deleteEnvironment(name: string): Promise<void>;
|
|
@@ -141,19 +90,13 @@ interface CiOptions {
|
|
|
141
90
|
repo: string;
|
|
142
91
|
}
|
|
143
92
|
declare class GitHubCi implements Ci {
|
|
144
|
-
private readonly
|
|
93
|
+
private readonly client;
|
|
94
|
+
private readonly opts;
|
|
145
95
|
readonly key: "ci";
|
|
146
96
|
readonly providerName = "github";
|
|
147
|
-
|
|
148
|
-
constructor(rest: GitHubRestClient, opts: CiOptions);
|
|
97
|
+
constructor(client: GitHubClient, opts: CiOptions);
|
|
149
98
|
listRuns(filter?: CiRunFilter): Promise<CiRun[]>;
|
|
150
99
|
getRun(id: string | number): Promise<CiRun>;
|
|
151
|
-
/**
|
|
152
|
-
* GitHub reports run state as `status` plus `conclusion` (the latter
|
|
153
|
-
* is set once `status === 'completed'`). Holocron's `CiRunStatus`
|
|
154
|
-
* collapses these onto one axis — when complete, the conclusion is
|
|
155
|
-
* the meaningful value.
|
|
156
|
-
*/
|
|
157
100
|
private mapRun;
|
|
158
101
|
private mapConclusion;
|
|
159
102
|
}
|
|
@@ -165,9 +108,7 @@ interface IssuesOptions {
|
|
|
165
108
|
* Lifecycle slot → status label name. Optional at construction; if
|
|
166
109
|
* omitted, capability methods that need labels (`transition`,
|
|
167
110
|
* `doctor`) degrade gracefully — `transition` throws a clear error,
|
|
168
|
-
* `doctor` reports the two label-backed slots as unresolved.
|
|
169
|
-
* lets the plugin load in configs where the operator hasn't picked
|
|
170
|
-
* labels yet without failing the whole runtime.
|
|
111
|
+
* `doctor` reports the two label-backed slots as unresolved.
|
|
171
112
|
*/
|
|
172
113
|
labels?: {
|
|
173
114
|
inProgress: string;
|
|
@@ -175,14 +116,14 @@ interface IssuesOptions {
|
|
|
175
116
|
};
|
|
176
117
|
}
|
|
177
118
|
declare class GitHubIssues implements Issues {
|
|
178
|
-
private readonly
|
|
119
|
+
private readonly client;
|
|
179
120
|
readonly key: "issues";
|
|
180
121
|
readonly providerName = "github";
|
|
181
122
|
private readonly owner;
|
|
182
123
|
private readonly repoName;
|
|
183
|
-
private readonly
|
|
124
|
+
private readonly repo;
|
|
184
125
|
private readonly labels;
|
|
185
|
-
constructor(
|
|
126
|
+
constructor(client: GitHubClient, opts: IssuesOptions);
|
|
186
127
|
getMyself(): Promise<TrackerUser>;
|
|
187
128
|
search(filter: IssueSearchFilter): Promise<Issue[]>;
|
|
188
129
|
get(key: string): Promise<Issue>;
|
|
@@ -202,6 +143,30 @@ declare class GitHubIssues implements Issues {
|
|
|
202
143
|
private mapIssue;
|
|
203
144
|
}
|
|
204
145
|
//#endregion
|
|
146
|
+
//#region src/capabilities/labels.d.ts
|
|
147
|
+
interface CanonicalLabel {
|
|
148
|
+
readonly name: string;
|
|
149
|
+
readonly color: string;
|
|
150
|
+
readonly description: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Idempotently synchronise a repo's labels to the canonical set.
|
|
154
|
+
*
|
|
155
|
+
* - Creates labels that are missing.
|
|
156
|
+
* - PATCHes labels whose color or description has drifted.
|
|
157
|
+
* - DELETEs labels listed in `stale`.
|
|
158
|
+
* - Leaves unrecognised labels that aren't in `stale` untouched.
|
|
159
|
+
*/
|
|
160
|
+
declare function syncLabels(client: GitHubClient, repo: string, canonical: ReadonlyArray<CanonicalLabel>, stale: ReadonlyArray<string>): Promise<string>;
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/capabilities/teams.d.ts
|
|
163
|
+
interface NormalizedTeamEntry {
|
|
164
|
+
slug: string;
|
|
165
|
+
permission: TeamPermission;
|
|
166
|
+
}
|
|
167
|
+
declare function normalizeTeamEntry(entry: TeamEntry): NormalizedTeamEntry;
|
|
168
|
+
declare function syncTeams(client: GitHubClient, repo: string, teams: TeamEntry[]): Promise<string>;
|
|
169
|
+
//#endregion
|
|
205
170
|
//#region src/verify-token.d.ts
|
|
206
171
|
/**
|
|
207
172
|
* `verifyToken` — plugin-level export used by `holocron auth set` +
|
|
@@ -247,7 +212,7 @@ interface GitHubPluginOptions extends ResolveTokenInput {
|
|
|
247
212
|
}
|
|
248
213
|
interface PluginContext {
|
|
249
214
|
options: GitHubPluginOptions;
|
|
250
|
-
|
|
215
|
+
client: GitHubClient;
|
|
251
216
|
repo: string;
|
|
252
217
|
repoRoot: string;
|
|
253
218
|
}
|
|
@@ -275,4 +240,4 @@ declare function createPlugin(options: GitHubPluginOptions): {
|
|
|
275
240
|
*/
|
|
276
241
|
declare const AUTH_HINT: string;
|
|
277
242
|
//#endregion
|
|
278
|
-
export { AUTH_HINT, type Auth, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions,
|
|
243
|
+
export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, type NormalizedTeamEntry, PluginContext, type ResolveTokenInput, type TeamEntry, type TeamPermission, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveToken, secrets, source, syncLabels, syncTeams, verifyToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,77 +1,37 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import {
|
|
2
|
+
import { createGitHubClient, createGitHubClient as createGitHubClient$1 } from "@theholocron/github-client";
|
|
3
|
+
import { AuthError, createResolveToken } from "@theholocron/cli";
|
|
3
4
|
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
//#region src/auth.ts
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* 2. HOLOCRON_GH_TOKEN env var (preferred over GITHUB_TOKEN — clearer intent)
|
|
13
|
-
* 3. GITHUB_TOKEN env var (auto-injected in GH Actions)
|
|
14
|
-
* 4. keyring (com.theholocron.cli / "github")
|
|
15
|
-
* 5. AuthError naming all four options + the bootstrap hint
|
|
16
|
-
*
|
|
17
|
-
* No `gh auth token` fallback by design — it usually has narrower
|
|
18
|
-
* scopes than admin commands need.
|
|
19
|
-
*/
|
|
20
|
-
var AuthError = class extends Error {
|
|
21
|
-
name = "AuthError";
|
|
22
|
-
};
|
|
23
|
-
function resolveToken(input = {}) {
|
|
24
|
-
const env = input.env ?? process.env;
|
|
25
|
-
const keyring = input.keyring ?? getToken;
|
|
26
|
-
const token = input.cliToken || env.HOLOCRON_GH_TOKEN || env.GITHUB_TOKEN || keyring("github");
|
|
27
|
-
if (!token) throw new AuthError("no GitHub token found. Pass --token <PAT>, set HOLOCRON_GH_TOKEN / GITHUB_TOKEN, or run: holocron auth set github <PAT>");
|
|
28
|
-
return token;
|
|
29
|
-
}
|
|
30
|
-
//#endregion
|
|
31
|
-
//#region src/repo.ts
|
|
32
|
-
var RepoError = class extends Error {
|
|
33
|
-
name = "RepoError";
|
|
34
|
-
};
|
|
35
|
-
function parseRepo(repo) {
|
|
36
|
-
if (!repo) throw new RepoError("repo is required; pass { repo: \"owner/name\" } in plugin options");
|
|
37
|
-
const [owner, name] = repo.split("/");
|
|
38
|
-
if (!owner || !name) throw new RepoError(`invalid repo "${repo}" — expected "owner/name"`);
|
|
39
|
-
return {
|
|
40
|
-
owner,
|
|
41
|
-
name
|
|
42
|
-
};
|
|
43
|
-
}
|
|
7
|
+
const resolveToken = createResolveToken({
|
|
8
|
+
envName: "HOLOCRON_GH_TOKEN",
|
|
9
|
+
vendorEnvName: "GITHUB_TOKEN",
|
|
10
|
+
keyringService: "github",
|
|
11
|
+
errorMessage: "no GitHub token found. Pass --token <PAT>, set HOLOCRON_GH_TOKEN / GITHUB_TOKEN, or run: holocron auth set github <PAT>"
|
|
12
|
+
});
|
|
44
13
|
//#endregion
|
|
45
14
|
//#region src/capabilities/ci.ts
|
|
46
15
|
var GitHubCi = class {
|
|
47
|
-
|
|
16
|
+
client;
|
|
17
|
+
opts;
|
|
48
18
|
key = "ci";
|
|
49
19
|
providerName = "github";
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
this.
|
|
53
|
-
const { owner, name } = parseRepo(opts.repo);
|
|
54
|
-
this.base = `/repos/${owner}/${name}/actions/runs`;
|
|
20
|
+
constructor(client, opts) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.opts = opts;
|
|
55
23
|
}
|
|
56
24
|
async listRuns(filter) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const path = qs ? `${this.base}?${qs}` : this.base;
|
|
63
|
-
return (await this.rest.request(path)).workflow_runs.map((r) => this.mapRun(r));
|
|
25
|
+
return (await this.client.workflows.listRuns(this.opts.repo, {
|
|
26
|
+
branch: filter?.branch,
|
|
27
|
+
limit: filter?.limit,
|
|
28
|
+
status: filter?.status
|
|
29
|
+
})).map((r) => this.mapRun(r));
|
|
64
30
|
}
|
|
65
31
|
async getRun(id) {
|
|
66
|
-
const raw = await this.
|
|
32
|
+
const raw = await this.client.workflows.getRun(this.opts.repo, id);
|
|
67
33
|
return this.mapRun(raw);
|
|
68
34
|
}
|
|
69
|
-
/**
|
|
70
|
-
* GitHub reports run state as `status` plus `conclusion` (the latter
|
|
71
|
-
* is set once `status === 'completed'`). Holocron's `CiRunStatus`
|
|
72
|
-
* collapses these onto one axis — when complete, the conclusion is
|
|
73
|
-
* the meaningful value.
|
|
74
|
-
*/
|
|
75
35
|
mapRun(raw) {
|
|
76
36
|
const status = raw.status === "completed" && raw.conclusion ? this.mapConclusion(raw.conclusion) : raw.status;
|
|
77
37
|
return {
|
|
@@ -98,17 +58,16 @@ var GitHubCi = class {
|
|
|
98
58
|
//#endregion
|
|
99
59
|
//#region src/capabilities/environments.ts
|
|
100
60
|
var GitHubEnvironments = class {
|
|
101
|
-
|
|
61
|
+
client;
|
|
62
|
+
opts;
|
|
102
63
|
key = "environments";
|
|
103
64
|
providerName = "github";
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
const { owner, name } = parseRepo(opts.repo);
|
|
108
|
-
this.base = `/repos/${owner}/${name}/environments`;
|
|
65
|
+
constructor(client, opts) {
|
|
66
|
+
this.client = client;
|
|
67
|
+
this.opts = opts;
|
|
109
68
|
}
|
|
110
69
|
async listEnvironments() {
|
|
111
|
-
return (await this.
|
|
70
|
+
return (await this.client.environments.listEnvironments(this.opts.repo)).map((e) => ({
|
|
112
71
|
name: e.name,
|
|
113
72
|
waitTimer: e.wait_timer,
|
|
114
73
|
preventSelfReview: e.prevent_self_review,
|
|
@@ -123,16 +82,10 @@ var GitHubEnvironments = class {
|
|
|
123
82
|
type: r.type,
|
|
124
83
|
id: r.id
|
|
125
84
|
}));
|
|
126
|
-
await this.
|
|
127
|
-
method: "PUT",
|
|
128
|
-
body
|
|
129
|
-
});
|
|
85
|
+
await this.client.environments.upsertEnvironment(this.opts.repo, env.name, body);
|
|
130
86
|
}
|
|
131
87
|
async deleteEnvironment(name) {
|
|
132
|
-
await this.
|
|
133
|
-
method: "DELETE",
|
|
134
|
-
expectNoContent: true
|
|
135
|
-
});
|
|
88
|
+
await this.client.environments.deleteEnvironment(this.opts.repo, name);
|
|
136
89
|
}
|
|
137
90
|
flattenReviewers(rules) {
|
|
138
91
|
if (!rules) return void 0;
|
|
@@ -147,41 +100,51 @@ var GitHubEnvironments = class {
|
|
|
147
100
|
//#endregion
|
|
148
101
|
//#region src/capabilities/issues.ts
|
|
149
102
|
var GitHubIssues = class {
|
|
150
|
-
|
|
103
|
+
client;
|
|
151
104
|
key = "issues";
|
|
152
105
|
providerName = "github";
|
|
153
106
|
owner;
|
|
154
107
|
repoName;
|
|
155
|
-
|
|
108
|
+
repo;
|
|
156
109
|
labels;
|
|
157
|
-
constructor(
|
|
158
|
-
this.
|
|
159
|
-
const
|
|
110
|
+
constructor(client, opts) {
|
|
111
|
+
this.client = client;
|
|
112
|
+
const [owner = "", repoName = ""] = opts.repo.split("/", 2);
|
|
160
113
|
this.owner = owner;
|
|
161
|
-
this.repoName =
|
|
162
|
-
this.
|
|
114
|
+
this.repoName = repoName;
|
|
115
|
+
this.repo = opts.repo;
|
|
163
116
|
this.labels = opts.labels;
|
|
164
117
|
}
|
|
165
118
|
async getMyself() {
|
|
166
|
-
|
|
119
|
+
const raw = await this.client.user.getCurrentUser();
|
|
120
|
+
return {
|
|
121
|
+
id: raw.login,
|
|
122
|
+
displayName: raw.name ?? raw.login,
|
|
123
|
+
...raw.email ? { emailAddress: raw.email } : {}
|
|
124
|
+
};
|
|
167
125
|
}
|
|
168
126
|
async search(filter) {
|
|
169
|
-
const params =
|
|
127
|
+
const params = {
|
|
170
128
|
state: filter.openOnly ? "open" : "all",
|
|
171
129
|
sort: "updated",
|
|
172
130
|
direction: "desc",
|
|
173
|
-
per_page:
|
|
131
|
+
per_page: filter.limit ?? 50,
|
|
174
132
|
filter: "all"
|
|
175
|
-
}
|
|
176
|
-
if (filter.assignee === "currentUser")
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
133
|
+
};
|
|
134
|
+
if (filter.assignee === "currentUser") params.assignee = (await this.getMyself()).id;
|
|
135
|
+
else if (filter.assignee) params.assignee = filter.assignee;
|
|
136
|
+
return (await this.client.issues.listIssues(this.repo, {
|
|
137
|
+
state: params.state,
|
|
138
|
+
sort: params.sort,
|
|
139
|
+
direction: params.direction,
|
|
140
|
+
per_page: Number(params.per_page),
|
|
141
|
+
filter: params.filter,
|
|
142
|
+
assignee: params.assignee
|
|
143
|
+
})).filter((i) => !i.pull_request).map((i) => this.mapIssue(i));
|
|
181
144
|
}
|
|
182
145
|
async get(key) {
|
|
183
146
|
const number = parseIssueNumber(key, this.owner, this.repoName);
|
|
184
|
-
const raw = await this.
|
|
147
|
+
const raw = await this.client.issues.getIssue(this.repo, number);
|
|
185
148
|
return this.mapIssue(raw);
|
|
186
149
|
}
|
|
187
150
|
async create(input) {
|
|
@@ -189,14 +152,11 @@ var GitHubIssues = class {
|
|
|
189
152
|
if (input.body) body.body = input.body;
|
|
190
153
|
if (input.labels?.length) body.labels = input.labels;
|
|
191
154
|
if (input.milestone) body.milestone = await this.resolveMilestone(input.milestone);
|
|
192
|
-
return { key: `#${(await this.
|
|
193
|
-
method: "POST",
|
|
194
|
-
body
|
|
195
|
-
})).number}` };
|
|
155
|
+
return { key: `#${(await this.client.issues.createIssue(this.repo, body)).number}` };
|
|
196
156
|
}
|
|
197
157
|
async transition(key, slot) {
|
|
198
158
|
const number = parseIssueNumber(key, this.owner, this.repoName);
|
|
199
|
-
const issue = await this.
|
|
159
|
+
const issue = await this.client.issues.getIssue(this.repo, number);
|
|
200
160
|
const currentStatusLabels = issue.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
|
|
201
161
|
if (slot === "done") {
|
|
202
162
|
if (issue.state === "closed") return {
|
|
@@ -205,12 +165,9 @@ var GitHubIssues = class {
|
|
|
205
165
|
via: "already closed"
|
|
206
166
|
};
|
|
207
167
|
await this.removeStatusLabels(number, currentStatusLabels);
|
|
208
|
-
await this.
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
state: "closed",
|
|
212
|
-
state_reason: "completed"
|
|
213
|
-
}
|
|
168
|
+
await this.client.issues.updateIssue(this.repo, number, {
|
|
169
|
+
state: "closed",
|
|
170
|
+
state_reason: "completed"
|
|
214
171
|
});
|
|
215
172
|
return {
|
|
216
173
|
transitioned: true,
|
|
@@ -227,16 +184,10 @@ var GitHubIssues = class {
|
|
|
227
184
|
status: `open + ${targetLabel}`,
|
|
228
185
|
via: "already there"
|
|
229
186
|
};
|
|
230
|
-
if (!alreadyOpen) await this.
|
|
231
|
-
method: "PATCH",
|
|
232
|
-
body: { state: "open" }
|
|
233
|
-
});
|
|
187
|
+
if (!alreadyOpen) await this.client.issues.updateIssue(this.repo, number, { state: "open" });
|
|
234
188
|
const toRemove = currentStatusLabels.filter((n) => n !== targetLabel);
|
|
235
189
|
if (toRemove.length) await this.removeStatusLabels(number, toRemove);
|
|
236
|
-
if (!alreadyLabeled) await this.
|
|
237
|
-
method: "POST",
|
|
238
|
-
body: { labels: [targetLabel] }
|
|
239
|
-
});
|
|
190
|
+
if (!alreadyLabeled) await this.client.issues.addLabels(this.repo, number, [targetLabel]);
|
|
240
191
|
return {
|
|
241
192
|
transitioned: true,
|
|
242
193
|
status: `open + ${targetLabel}`,
|
|
@@ -245,15 +196,12 @@ var GitHubIssues = class {
|
|
|
245
196
|
}
|
|
246
197
|
async comment(key, body) {
|
|
247
198
|
const number = parseIssueNumber(key, this.owner, this.repoName);
|
|
248
|
-
await this.
|
|
249
|
-
method: "POST",
|
|
250
|
-
body: { body }
|
|
251
|
-
});
|
|
199
|
+
await this.client.issues.createComment(this.repo, number, body);
|
|
252
200
|
}
|
|
253
201
|
async doctor() {
|
|
254
202
|
const me = await this.getMyself();
|
|
255
|
-
const repo = await this.
|
|
256
|
-
const allLabels = await this.
|
|
203
|
+
const repo = await this.client.repos.getRepo(this.repo);
|
|
204
|
+
const allLabels = await this.client.labels.listLabels(this.repo);
|
|
257
205
|
const labelNames = new Set(allLabels.map((l) => l.name));
|
|
258
206
|
const statuses = [
|
|
259
207
|
{
|
|
@@ -320,12 +268,12 @@ var GitHubIssues = class {
|
|
|
320
268
|
}
|
|
321
269
|
async resolveMilestone(ref) {
|
|
322
270
|
if (/^\d+$/.test(ref)) return parseInt(ref, 10);
|
|
323
|
-
const match = (await this.
|
|
271
|
+
const match = (await this.client.issues.listMilestones(this.repo, { state: "all" })).find((m) => m.title.toLowerCase() === ref.toLowerCase());
|
|
324
272
|
if (!match) throw new Error(`Milestone "${ref}" not found in ${this.owner}/${this.repoName}. Use the numeric id or an exact title.`);
|
|
325
273
|
return match.number;
|
|
326
274
|
}
|
|
327
275
|
async removeStatusLabels(number, labels) {
|
|
328
|
-
for (const label of labels) await this.
|
|
276
|
+
for (const label of labels) await this.client.issues.removeLabel(this.repo, number, label);
|
|
329
277
|
}
|
|
330
278
|
mapIssue(raw) {
|
|
331
279
|
const statusLabels = raw.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
|
|
@@ -346,17 +294,16 @@ var GitHubIssues = class {
|
|
|
346
294
|
...raw.body ? { body: raw.body } : {},
|
|
347
295
|
status: statusName,
|
|
348
296
|
statusCategory: category,
|
|
349
|
-
assignee: raw.assignee ?
|
|
297
|
+
assignee: raw.assignee ? {
|
|
298
|
+
id: raw.assignee.login,
|
|
299
|
+
displayName: raw.assignee.name ?? raw.assignee.login,
|
|
300
|
+
...raw.assignee.email ? { emailAddress: raw.assignee.email } : {}
|
|
301
|
+
} : null,
|
|
350
302
|
updated: raw.updated_at,
|
|
351
303
|
url: raw.html_url
|
|
352
304
|
};
|
|
353
305
|
}
|
|
354
306
|
};
|
|
355
|
-
/**
|
|
356
|
-
* Accept "#42", "42", or "owner/repo#42" and return the numeric id.
|
|
357
|
-
* Cross-repo refs are allowed in input but the adapter is bound to one
|
|
358
|
-
* repo — we error if the owner/repo doesn't match.
|
|
359
|
-
*/
|
|
360
307
|
function parseIssueNumber(key, owner, repoName) {
|
|
361
308
|
const trimmed = key.trim();
|
|
362
309
|
const crossMatch = trimmed.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
@@ -368,13 +315,6 @@ function parseIssueNumber(key, owner, repoName) {
|
|
|
368
315
|
if (!/^\d+$/.test(num)) throw new Error(`Invalid GitHub issue key "${key}" — expected #N or N.`);
|
|
369
316
|
return parseInt(num, 10);
|
|
370
317
|
}
|
|
371
|
-
function mapUser(raw) {
|
|
372
|
-
return {
|
|
373
|
-
id: raw.login,
|
|
374
|
-
displayName: raw.name ?? raw.login,
|
|
375
|
-
...raw.email ? { emailAddress: raw.email } : {}
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
318
|
//#endregion
|
|
379
319
|
//#region src/sodium.ts
|
|
380
320
|
/**
|
|
@@ -410,84 +350,135 @@ async function encryptSecret(publicKeyBase64, value) {
|
|
|
410
350
|
//#endregion
|
|
411
351
|
//#region src/capabilities/secrets.ts
|
|
412
352
|
var GitHubSecrets = class {
|
|
413
|
-
|
|
353
|
+
client;
|
|
354
|
+
opts;
|
|
414
355
|
key = "secrets";
|
|
415
356
|
providerName = "github";
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
this.
|
|
419
|
-
const { owner, name } = parseRepo(opts.repo);
|
|
420
|
-
this.repoBase = `/repos/${owner}/${name}`;
|
|
357
|
+
constructor(client, opts) {
|
|
358
|
+
this.client = client;
|
|
359
|
+
this.opts = opts;
|
|
421
360
|
}
|
|
422
361
|
async listSecrets(scope) {
|
|
423
|
-
|
|
424
|
-
return (await this.rest.request(`${base}/secrets`)).secrets.map((s) => s.name);
|
|
362
|
+
return this.client.secrets.listSecrets(this.opts.repo, toGHScope(scope));
|
|
425
363
|
}
|
|
426
364
|
async setSecret(scope, name, value) {
|
|
427
|
-
const
|
|
428
|
-
const pk = await this.
|
|
365
|
+
const ghScope = toGHScope(scope);
|
|
366
|
+
const pk = await this.client.secrets.getPublicKey(this.opts.repo, ghScope);
|
|
429
367
|
const body = {
|
|
430
368
|
encrypted_value: await encryptSecret(pk.key, value),
|
|
431
369
|
key_id: pk.key_id
|
|
432
370
|
};
|
|
433
371
|
if (scope.kind === "organization") body.visibility = "all";
|
|
434
|
-
await this.
|
|
435
|
-
method: "PUT",
|
|
436
|
-
body,
|
|
437
|
-
expectNoContent: true
|
|
438
|
-
});
|
|
372
|
+
await this.client.secrets.putSecret(this.opts.repo, ghScope, name, body);
|
|
439
373
|
}
|
|
440
374
|
async deleteSecret(scope, name) {
|
|
441
|
-
|
|
442
|
-
await this.rest.request(`${base}/secrets/${name}`, {
|
|
443
|
-
method: "DELETE",
|
|
444
|
-
expectNoContent: true
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
/** Endpoint root for the given scope. */
|
|
448
|
-
scopeBase(scope) {
|
|
449
|
-
switch (scope.kind) {
|
|
450
|
-
case "repo": return `${this.repoBase}/actions`;
|
|
451
|
-
case "environment": return `${this.repoBase}/environments/${scope.name}`;
|
|
452
|
-
case "organization": return `/orgs/${scope.name}/actions`;
|
|
453
|
-
}
|
|
375
|
+
await this.client.secrets.deleteSecret(this.opts.repo, toGHScope(scope), name);
|
|
454
376
|
}
|
|
455
377
|
};
|
|
378
|
+
function toGHScope(scope) {
|
|
379
|
+
if (scope.kind === "organization") return {
|
|
380
|
+
kind: "organization",
|
|
381
|
+
org: scope.name
|
|
382
|
+
};
|
|
383
|
+
return scope;
|
|
384
|
+
}
|
|
456
385
|
//#endregion
|
|
457
|
-
//#region src/capabilities/
|
|
386
|
+
//#region src/capabilities/labels.ts
|
|
458
387
|
/**
|
|
459
|
-
*
|
|
388
|
+
* Idempotently synchronise a repo's labels to the canonical set.
|
|
460
389
|
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* - Workflow files are local-fs operations (not GH API) — they live
|
|
466
|
-
* in the consumer's `.github/workflows/` directory
|
|
390
|
+
* - Creates labels that are missing.
|
|
391
|
+
* - PATCHes labels whose color or description has drifted.
|
|
392
|
+
* - DELETEs labels listed in `stale`.
|
|
393
|
+
* - Leaves unrecognised labels that aren't in `stale` untouched.
|
|
467
394
|
*/
|
|
395
|
+
async function syncLabels(client, repo, canonical, stale) {
|
|
396
|
+
const existing = await client.labels.listLabels(repo);
|
|
397
|
+
const existingMap = new Map(existing.map((l) => [l.name.toLowerCase(), l]));
|
|
398
|
+
let created = 0;
|
|
399
|
+
let updated = 0;
|
|
400
|
+
let deleted = 0;
|
|
401
|
+
for (const label of canonical) {
|
|
402
|
+
const current = existingMap.get(label.name);
|
|
403
|
+
if (!current) {
|
|
404
|
+
await client.labels.createLabel(repo, {
|
|
405
|
+
name: label.name,
|
|
406
|
+
color: label.color,
|
|
407
|
+
description: label.description
|
|
408
|
+
});
|
|
409
|
+
created++;
|
|
410
|
+
} else if (current.color !== label.color || (current.description ?? "") !== label.description) {
|
|
411
|
+
await client.labels.updateLabel(repo, label.name, {
|
|
412
|
+
color: label.color,
|
|
413
|
+
description: label.description
|
|
414
|
+
});
|
|
415
|
+
updated++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
for (const staleName of stale) if (existingMap.has(staleName)) {
|
|
419
|
+
await client.labels.deleteLabel(repo, staleName);
|
|
420
|
+
deleted++;
|
|
421
|
+
}
|
|
422
|
+
return `${created} created, ${updated} updated, ${deleted} deleted`;
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region src/capabilities/properties.ts
|
|
426
|
+
async function syncProperties(client, repo, values) {
|
|
427
|
+
await client.properties.setProperties(repo, values);
|
|
428
|
+
return `${Object.keys(values).length} properties set`;
|
|
429
|
+
}
|
|
430
|
+
//#endregion
|
|
431
|
+
//#region src/capabilities/teams.ts
|
|
432
|
+
function normalizeTeamEntry(entry) {
|
|
433
|
+
if (typeof entry === "string") return {
|
|
434
|
+
slug: entry,
|
|
435
|
+
permission: "push"
|
|
436
|
+
};
|
|
437
|
+
return entry;
|
|
438
|
+
}
|
|
439
|
+
async function syncTeams(client, repo, teams) {
|
|
440
|
+
const [org = "", name = ""] = repo.split("/", 2);
|
|
441
|
+
const normalized = teams.map(normalizeTeamEntry);
|
|
442
|
+
const results = await Promise.allSettled(normalized.map(({ slug, permission }) => client.teams.addRepo(org, slug, org, name, permission)));
|
|
443
|
+
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
|
444
|
+
const failedSlugs = normalized.filter((_, i) => results[i]?.status === "rejected").map(({ slug }) => slug);
|
|
445
|
+
if (failedSlugs.length > 0) {
|
|
446
|
+
if (succeeded === 0) throw new Error(`all teams failed: ${failedSlugs.join(", ")}`);
|
|
447
|
+
return `${succeeded} synced, failed: ${failedSlugs.join(", ")}`;
|
|
448
|
+
}
|
|
449
|
+
return `${succeeded} team${succeeded === 1 ? "" : "s"} synced`;
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/capabilities/topics.ts
|
|
453
|
+
async function syncTopics(client, repo, topics) {
|
|
454
|
+
await client.topics.setTopics(repo, topics);
|
|
455
|
+
return `${topics.length} topics set`;
|
|
456
|
+
}
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region src/capabilities/source.ts
|
|
468
459
|
var GitHubSource = class {
|
|
469
|
-
|
|
460
|
+
client;
|
|
470
461
|
key = "source";
|
|
471
462
|
providerName = "github";
|
|
472
463
|
owner;
|
|
473
464
|
name;
|
|
474
|
-
|
|
465
|
+
repo;
|
|
475
466
|
repoRoot;
|
|
476
467
|
workflowDir;
|
|
477
|
-
constructor(
|
|
478
|
-
this.
|
|
479
|
-
const
|
|
468
|
+
constructor(client, opts) {
|
|
469
|
+
this.client = client;
|
|
470
|
+
const [owner = "", name = ""] = opts.repo.split("/", 2);
|
|
480
471
|
this.owner = owner;
|
|
481
472
|
this.name = name;
|
|
482
|
-
this.
|
|
473
|
+
this.repo = opts.repo;
|
|
483
474
|
this.repoRoot = opts.repoRoot;
|
|
484
475
|
this.workflowDir = join(opts.repoRoot, ".github", "workflows");
|
|
485
476
|
}
|
|
486
477
|
async whoami() {
|
|
487
|
-
return { login: (await this.
|
|
478
|
+
return { login: (await this.client.user.getCurrentUser()).login };
|
|
488
479
|
}
|
|
489
480
|
async getRepo() {
|
|
490
|
-
const data = await this.
|
|
481
|
+
const data = await this.client.repos.getRepo(this.repo);
|
|
491
482
|
return {
|
|
492
483
|
owner: this.owner,
|
|
493
484
|
name: this.name,
|
|
@@ -495,79 +486,40 @@ var GitHubSource = class {
|
|
|
495
486
|
};
|
|
496
487
|
}
|
|
497
488
|
async listRulesets() {
|
|
498
|
-
return this.
|
|
489
|
+
return this.client.rulesets.listRulesets(this.repo);
|
|
499
490
|
}
|
|
500
491
|
async createRuleset(payload) {
|
|
501
|
-
return this.
|
|
502
|
-
method: "POST",
|
|
503
|
-
body: payload
|
|
504
|
-
});
|
|
492
|
+
return this.client.rulesets.createRuleset(this.repo, payload);
|
|
505
493
|
}
|
|
506
494
|
async updateRuleset(id, payload) {
|
|
507
|
-
return this.
|
|
508
|
-
method: "PUT",
|
|
509
|
-
body: payload
|
|
510
|
-
});
|
|
495
|
+
return this.client.rulesets.updateRuleset(this.repo, id, payload);
|
|
511
496
|
}
|
|
512
497
|
async updateRepoSettings(settings) {
|
|
513
|
-
await this.
|
|
514
|
-
method: "PATCH",
|
|
515
|
-
body: settings
|
|
516
|
-
});
|
|
498
|
+
await this.client.repos.updateRepo(this.repo, settings);
|
|
517
499
|
}
|
|
518
500
|
async protectBranch(branch, payload) {
|
|
519
|
-
await this.
|
|
520
|
-
method: "PUT",
|
|
521
|
-
body: payload
|
|
522
|
-
});
|
|
501
|
+
await this.client.branches.protectBranch(this.repo, branch, payload);
|
|
523
502
|
}
|
|
524
503
|
async enableVulnerabilityAlerts() {
|
|
525
|
-
await this.
|
|
526
|
-
method: "PUT",
|
|
527
|
-
expectNoContent: true
|
|
528
|
-
});
|
|
504
|
+
await this.client.security.enableVulnerabilityAlerts(this.repo);
|
|
529
505
|
}
|
|
530
506
|
async enableAutomatedSecurityFixes() {
|
|
531
|
-
await this.
|
|
532
|
-
method: "PUT",
|
|
533
|
-
expectNoContent: true
|
|
534
|
-
});
|
|
507
|
+
await this.client.security.enableAutomatedSecurityFixes(this.repo);
|
|
535
508
|
}
|
|
536
509
|
async enableSecretScanning() {
|
|
537
|
-
await this.
|
|
538
|
-
method: "PATCH",
|
|
539
|
-
body: { security_and_analysis: {
|
|
540
|
-
secret_scanning: { status: "enabled" },
|
|
541
|
-
secret_scanning_push_protection: { status: "enabled" },
|
|
542
|
-
secret_scanning_validity_checks: { status: "enabled" },
|
|
543
|
-
secret_scanning_non_provider_patterns: { status: "enabled" }
|
|
544
|
-
} }
|
|
545
|
-
});
|
|
510
|
+
await this.client.security.enableSecretScanning(this.repo);
|
|
546
511
|
}
|
|
547
512
|
async enableCodeScanning() {
|
|
548
|
-
return `run ${(await this.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
query_suite: "extended",
|
|
553
|
-
threat_model: "remote_and_local"
|
|
554
|
-
}
|
|
555
|
-
})).run_id}`;
|
|
513
|
+
return `run ${(await this.client.security.enableCodeScanning(this.repo)).run_id}`;
|
|
514
|
+
}
|
|
515
|
+
async disableDefaultCodeScanning() {
|
|
516
|
+
await this.client.security.disableDefaultCodeScanning(this.repo);
|
|
556
517
|
}
|
|
557
518
|
async enableDependencyGraph() {
|
|
558
|
-
await this.
|
|
559
|
-
method: "PATCH",
|
|
560
|
-
body: { security_and_analysis: {
|
|
561
|
-
dependency_graph: { status: "enabled" },
|
|
562
|
-
dependency_graph_autosubmit_action: { status: "enabled" }
|
|
563
|
-
} }
|
|
564
|
-
});
|
|
519
|
+
await this.client.security.enableDependencyGraph(this.repo);
|
|
565
520
|
}
|
|
566
521
|
async enablePrivateVulnerabilityReporting() {
|
|
567
|
-
await this.
|
|
568
|
-
method: "PUT",
|
|
569
|
-
expectNoContent: true
|
|
570
|
-
});
|
|
522
|
+
await this.client.security.enablePrivateVulnerabilityReporting(this.repo);
|
|
571
523
|
}
|
|
572
524
|
async listWorkflowFiles() {
|
|
573
525
|
try {
|
|
@@ -602,6 +554,22 @@ var GitHubSource = class {
|
|
|
602
554
|
await ensureDir(dirname(full));
|
|
603
555
|
await writeFile(full, contents, "utf8");
|
|
604
556
|
}
|
|
557
|
+
async syncLabels(canonical, stale) {
|
|
558
|
+
return syncLabels(this.client, this.repo, canonical, stale);
|
|
559
|
+
}
|
|
560
|
+
async syncProperties(values) {
|
|
561
|
+
return syncProperties(this.client, this.repo, values);
|
|
562
|
+
}
|
|
563
|
+
async syncTeams(teams) {
|
|
564
|
+
return syncTeams(this.client, this.repo, teams);
|
|
565
|
+
}
|
|
566
|
+
async syncTopics(topics) {
|
|
567
|
+
return syncTopics(this.client, this.repo, topics);
|
|
568
|
+
}
|
|
569
|
+
async syncDescription(description) {
|
|
570
|
+
await this.client.repos.updateRepo(this.repo, { description });
|
|
571
|
+
return "description updated";
|
|
572
|
+
}
|
|
605
573
|
};
|
|
606
574
|
function isENOENT(err) {
|
|
607
575
|
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
@@ -618,55 +586,6 @@ async function ensureDir(path) {
|
|
|
618
586
|
}
|
|
619
587
|
}
|
|
620
588
|
//#endregion
|
|
621
|
-
//#region src/rest.ts
|
|
622
|
-
/**
|
|
623
|
-
* Thin REST wrapper around api.github.com.
|
|
624
|
-
*
|
|
625
|
-
* Adapted from rando-id/rando.id `packages/cli/src/adapters/gh-rest.ts`
|
|
626
|
-
* `request<T>` method into a standalone class so each capability can
|
|
627
|
-
* hold a reference and share the same auth/error handling.
|
|
628
|
-
*
|
|
629
|
-
* Errors throw `ProviderApiError` from `@theholocron/cli`, which the
|
|
630
|
-
* orchestrator catches to soft-skip rather than abort the whole pipeline.
|
|
631
|
-
*/
|
|
632
|
-
var GitHubRestClient = class {
|
|
633
|
-
token;
|
|
634
|
-
fetchImpl;
|
|
635
|
-
baseUrl;
|
|
636
|
-
constructor(opts) {
|
|
637
|
-
this.token = opts.token;
|
|
638
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
639
|
-
let url = opts.baseUrl ?? "https://api.github.com";
|
|
640
|
-
while (url.endsWith("/")) url = url.slice(0, -1);
|
|
641
|
-
this.baseUrl = url;
|
|
642
|
-
}
|
|
643
|
-
async request(path, opts = {}) {
|
|
644
|
-
const url = `${this.baseUrl}${path}`;
|
|
645
|
-
const headers = {
|
|
646
|
-
accept: "application/vnd.github+json",
|
|
647
|
-
authorization: `Bearer ${this.token}`,
|
|
648
|
-
"x-github-api-version": "2022-11-28"
|
|
649
|
-
};
|
|
650
|
-
const init = {
|
|
651
|
-
method: opts.method ?? "GET",
|
|
652
|
-
headers
|
|
653
|
-
};
|
|
654
|
-
if (opts.body !== void 0) {
|
|
655
|
-
headers["content-type"] = "application/json";
|
|
656
|
-
init.body = JSON.stringify(opts.body);
|
|
657
|
-
}
|
|
658
|
-
const res = await this.fetchImpl(url, init);
|
|
659
|
-
if (!res.ok) {
|
|
660
|
-
const body = await res.text().catch(() => "");
|
|
661
|
-
throw new ProviderApiError(`GitHub ${init.method} ${path} → ${res.status}`, res.status, body);
|
|
662
|
-
}
|
|
663
|
-
if (opts.expectNoContent || res.status === 204) return;
|
|
664
|
-
const text = await res.text();
|
|
665
|
-
if (!text) return void 0;
|
|
666
|
-
return JSON.parse(text);
|
|
667
|
-
}
|
|
668
|
-
};
|
|
669
|
-
//#endregion
|
|
670
589
|
//#region src/verify-token.ts
|
|
671
590
|
/**
|
|
672
591
|
* `verifyToken` — plugin-level export used by `holocron auth set` +
|
|
@@ -679,12 +598,13 @@ var GitHubRestClient = class {
|
|
|
679
598
|
* what we don't have yet at bootstrap time.
|
|
680
599
|
*/
|
|
681
600
|
async function verifyToken(token, opts = {}) {
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
601
|
+
const client = createGitHubClient({
|
|
602
|
+
token,
|
|
603
|
+
baseUrl: opts.baseUrl,
|
|
604
|
+
fetch: opts.fetch
|
|
605
|
+
});
|
|
686
606
|
try {
|
|
687
|
-
const me = await
|
|
607
|
+
const me = await client.user.getCurrentUser();
|
|
688
608
|
return {
|
|
689
609
|
ok: true,
|
|
690
610
|
subject: `user @ ${me.login ?? me.email ?? "unknown"}`
|
|
@@ -701,7 +621,7 @@ async function verifyToken(token, opts = {}) {
|
|
|
701
621
|
function createContext(options) {
|
|
702
622
|
return {
|
|
703
623
|
options,
|
|
704
|
-
|
|
624
|
+
client: createGitHubClient$1({
|
|
705
625
|
token: resolveToken(options),
|
|
706
626
|
baseUrl: options.baseUrl,
|
|
707
627
|
fetch: options.fetch
|
|
@@ -711,24 +631,24 @@ function createContext(options) {
|
|
|
711
631
|
};
|
|
712
632
|
}
|
|
713
633
|
function source(ctx) {
|
|
714
|
-
return new GitHubSource(ctx.
|
|
634
|
+
return new GitHubSource(ctx.client, {
|
|
715
635
|
repo: ctx.repo,
|
|
716
636
|
repoRoot: ctx.repoRoot
|
|
717
637
|
});
|
|
718
638
|
}
|
|
719
639
|
function secrets(ctx) {
|
|
720
|
-
return new GitHubSecrets(ctx.
|
|
640
|
+
return new GitHubSecrets(ctx.client, { repo: ctx.repo });
|
|
721
641
|
}
|
|
722
642
|
function environments(ctx) {
|
|
723
|
-
return new GitHubEnvironments(ctx.
|
|
643
|
+
return new GitHubEnvironments(ctx.client, { repo: ctx.repo });
|
|
724
644
|
}
|
|
725
645
|
function ci(ctx) {
|
|
726
|
-
return new GitHubCi(ctx.
|
|
646
|
+
return new GitHubCi(ctx.client, { repo: ctx.repo });
|
|
727
647
|
}
|
|
728
648
|
function issues(ctx) {
|
|
729
649
|
const opts = { repo: ctx.repo };
|
|
730
650
|
if (ctx.options.labels !== void 0) opts.labels = ctx.options.labels;
|
|
731
|
-
return new GitHubIssues(ctx.
|
|
651
|
+
return new GitHubIssues(ctx.client, opts);
|
|
732
652
|
}
|
|
733
653
|
function createPlugin(options) {
|
|
734
654
|
const ctx = createContext(options);
|
|
@@ -751,4 +671,4 @@ function createPlugin(options) {
|
|
|
751
671
|
*/
|
|
752
672
|
const AUTH_HINT = "generate a Personal Access Token at https://github.com/settings/tokens (scopes: repo, admin:repo_hook — plus admin:org for org-level ops), then run: holocron auth set github <PAT>";
|
|
753
673
|
//#endregion
|
|
754
|
-
export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues,
|
|
674
|
+
export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, normalizeTeamEntry, resolveToken, secrets, source, syncLabels, syncTeams, verifyToken };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-github",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.71",
|
|
4
4
|
"description": "Holocron plugin for GitHub. Implements source, ci, secrets, environments, and issues capabilities against the GitHub REST API.",
|
|
5
5
|
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-github#readme",
|
|
6
6
|
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
@@ -21,24 +21,30 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@theholocron/
|
|
24
|
+
"@theholocron/github-client": "^1.1.0",
|
|
25
|
+
"@theholocron/cli": "2.0.0-alpha.71"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"libsodium-wrappers": "^0.7.15"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
|
-
"@theholocron/eslint-config": "^
|
|
31
|
-
"@theholocron/
|
|
32
|
-
"@tsconfig
|
|
31
|
+
"@theholocron/eslint-config": "^7.3.0",
|
|
32
|
+
"@theholocron/github-client": "^1.1.0",
|
|
33
|
+
"@theholocron/tsconfig": "^7.3.0",
|
|
34
|
+
"@theholocron/tsdown-config": "^7.3.0",
|
|
35
|
+
"@theholocron/vitest-config": "^7.3.0",
|
|
33
36
|
"@types/libsodium-wrappers": "^0.7.14",
|
|
34
|
-
"@
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
37
|
+
"@types/node": "^26",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
39
|
+
"@vitest/eslint-plugin": "^1.6.23",
|
|
40
|
+
"eslint": "^10.7.0",
|
|
41
|
+
"eslint-plugin-n": "^18.2.2",
|
|
42
|
+
"globals": "^17.7.0",
|
|
39
43
|
"tsdown": "^0.22.3",
|
|
40
44
|
"tsx": "^4.22.4",
|
|
41
|
-
"
|
|
45
|
+
"typescript": "^5.9.3",
|
|
46
|
+
"vitest": "^4.1.10",
|
|
47
|
+
"@theholocron/cli": "2.0.0-alpha.71"
|
|
42
48
|
},
|
|
43
49
|
"publishConfig": {
|
|
44
50
|
"access": "public"
|