@theholocron/holocron-plugin-github 2.0.0-alpha.45 → 2.0.0-alpha.47
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/dist/index.d.mts +21 -38
- package/dist/index.mjs +129 -283
- package/package.json +5 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
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, TrackerDoctorReport, TrackerUser } from "@theholocron/cli";
|
|
2
3
|
|
|
3
4
|
//#region src/auth.d.ts
|
|
4
5
|
declare const resolveToken: (input?: import("@theholocron/http-client").ResolveTokenInput) => string;
|
|
5
6
|
//#endregion
|
|
6
|
-
//#region src/rest.d.ts
|
|
7
|
-
declare function createGitHubRestClient(opts: {
|
|
8
|
-
token: string;
|
|
9
|
-
baseUrl?: string;
|
|
10
|
-
fetch?: typeof fetch;
|
|
11
|
-
}): RestClient$1;
|
|
12
|
-
//#endregion
|
|
13
7
|
//#region src/sodium.d.ts
|
|
14
8
|
/**
|
|
15
9
|
* Encrypt `value` for the given GitHub-provided base64 public key.
|
|
@@ -20,19 +14,18 @@ declare function encryptSecret(publicKeyBase64: string, value: string): Promise<
|
|
|
20
14
|
//#region src/capabilities/source.d.ts
|
|
21
15
|
interface SourceOptions {
|
|
22
16
|
repo: string;
|
|
23
|
-
/** Absolute path to the repo root. Workflow file ops are scoped here. */
|
|
24
17
|
repoRoot: string;
|
|
25
18
|
}
|
|
26
19
|
declare class GitHubSource implements Source {
|
|
27
|
-
private readonly
|
|
20
|
+
private readonly client;
|
|
28
21
|
readonly key: "source";
|
|
29
22
|
readonly providerName = "github";
|
|
30
23
|
private readonly owner;
|
|
31
24
|
private readonly name;
|
|
32
|
-
private readonly
|
|
25
|
+
private readonly repo;
|
|
33
26
|
private readonly repoRoot;
|
|
34
27
|
private readonly workflowDir;
|
|
35
|
-
constructor(
|
|
28
|
+
constructor(client: GitHubClient, opts: SourceOptions);
|
|
36
29
|
whoami(): Promise<{
|
|
37
30
|
login: string;
|
|
38
31
|
}>;
|
|
@@ -64,16 +57,14 @@ interface SecretsOptions {
|
|
|
64
57
|
repo: string;
|
|
65
58
|
}
|
|
66
59
|
declare class GitHubSecrets implements Secrets {
|
|
67
|
-
private readonly
|
|
60
|
+
private readonly client;
|
|
61
|
+
private readonly opts;
|
|
68
62
|
readonly key: "secrets";
|
|
69
63
|
readonly providerName = "github";
|
|
70
|
-
|
|
71
|
-
constructor(rest: RestClient, opts: SecretsOptions);
|
|
64
|
+
constructor(client: GitHubClient, opts: SecretsOptions);
|
|
72
65
|
listSecrets(scope: SecretScope): Promise<string[]>;
|
|
73
66
|
setSecret(scope: SecretScope, name: string, value: string): Promise<void>;
|
|
74
67
|
deleteSecret(scope: SecretScope, name: string): Promise<void>;
|
|
75
|
-
/** Endpoint root for the given scope. */
|
|
76
|
-
private scopeBase;
|
|
77
68
|
}
|
|
78
69
|
//#endregion
|
|
79
70
|
//#region src/capabilities/environments.d.ts
|
|
@@ -81,11 +72,11 @@ interface EnvironmentsOptions {
|
|
|
81
72
|
repo: string;
|
|
82
73
|
}
|
|
83
74
|
declare class GitHubEnvironments implements Environments {
|
|
84
|
-
private readonly
|
|
75
|
+
private readonly client;
|
|
76
|
+
private readonly opts;
|
|
85
77
|
readonly key: "environments";
|
|
86
78
|
readonly providerName = "github";
|
|
87
|
-
|
|
88
|
-
constructor(rest: RestClient, opts: EnvironmentsOptions);
|
|
79
|
+
constructor(client: GitHubClient, opts: EnvironmentsOptions);
|
|
89
80
|
listEnvironments(): Promise<Environment[]>;
|
|
90
81
|
upsertEnvironment(env: Environment): Promise<void>;
|
|
91
82
|
deleteEnvironment(name: string): Promise<void>;
|
|
@@ -97,19 +88,13 @@ interface CiOptions {
|
|
|
97
88
|
repo: string;
|
|
98
89
|
}
|
|
99
90
|
declare class GitHubCi implements Ci {
|
|
100
|
-
private readonly
|
|
91
|
+
private readonly client;
|
|
92
|
+
private readonly opts;
|
|
101
93
|
readonly key: "ci";
|
|
102
94
|
readonly providerName = "github";
|
|
103
|
-
|
|
104
|
-
constructor(rest: RestClient, opts: CiOptions);
|
|
95
|
+
constructor(client: GitHubClient, opts: CiOptions);
|
|
105
96
|
listRuns(filter?: CiRunFilter): Promise<CiRun[]>;
|
|
106
97
|
getRun(id: string | number): Promise<CiRun>;
|
|
107
|
-
/**
|
|
108
|
-
* GitHub reports run state as `status` plus `conclusion` (the latter
|
|
109
|
-
* is set once `status === 'completed'`). Holocron's `CiRunStatus`
|
|
110
|
-
* collapses these onto one axis — when complete, the conclusion is
|
|
111
|
-
* the meaningful value.
|
|
112
|
-
*/
|
|
113
98
|
private mapRun;
|
|
114
99
|
private mapConclusion;
|
|
115
100
|
}
|
|
@@ -121,9 +106,7 @@ interface IssuesOptions {
|
|
|
121
106
|
* Lifecycle slot → status label name. Optional at construction; if
|
|
122
107
|
* omitted, capability methods that need labels (`transition`,
|
|
123
108
|
* `doctor`) degrade gracefully — `transition` throws a clear error,
|
|
124
|
-
* `doctor` reports the two label-backed slots as unresolved.
|
|
125
|
-
* lets the plugin load in configs where the operator hasn't picked
|
|
126
|
-
* labels yet without failing the whole runtime.
|
|
109
|
+
* `doctor` reports the two label-backed slots as unresolved.
|
|
127
110
|
*/
|
|
128
111
|
labels?: {
|
|
129
112
|
inProgress: string;
|
|
@@ -131,14 +114,14 @@ interface IssuesOptions {
|
|
|
131
114
|
};
|
|
132
115
|
}
|
|
133
116
|
declare class GitHubIssues implements Issues {
|
|
134
|
-
private readonly
|
|
117
|
+
private readonly client;
|
|
135
118
|
readonly key: "issues";
|
|
136
119
|
readonly providerName = "github";
|
|
137
120
|
private readonly owner;
|
|
138
121
|
private readonly repoName;
|
|
139
|
-
private readonly
|
|
122
|
+
private readonly repo;
|
|
140
123
|
private readonly labels;
|
|
141
|
-
constructor(
|
|
124
|
+
constructor(client: GitHubClient, opts: IssuesOptions);
|
|
142
125
|
getMyself(): Promise<TrackerUser>;
|
|
143
126
|
search(filter: IssueSearchFilter): Promise<Issue[]>;
|
|
144
127
|
get(key: string): Promise<Issue>;
|
|
@@ -172,7 +155,7 @@ interface CanonicalLabel {
|
|
|
172
155
|
* - DELETEs labels listed in `stale`.
|
|
173
156
|
* - Leaves unrecognised labels that aren't in `stale` untouched.
|
|
174
157
|
*/
|
|
175
|
-
declare function syncLabels(
|
|
158
|
+
declare function syncLabels(client: GitHubClient, repo: string, canonical: ReadonlyArray<CanonicalLabel>, stale: ReadonlyArray<string>): Promise<string>;
|
|
176
159
|
//#endregion
|
|
177
160
|
//#region src/verify-token.d.ts
|
|
178
161
|
/**
|
|
@@ -219,7 +202,7 @@ interface GitHubPluginOptions extends ResolveTokenInput {
|
|
|
219
202
|
}
|
|
220
203
|
interface PluginContext {
|
|
221
204
|
options: GitHubPluginOptions;
|
|
222
|
-
|
|
205
|
+
client: GitHubClient;
|
|
223
206
|
repo: string;
|
|
224
207
|
repoRoot: string;
|
|
225
208
|
}
|
|
@@ -247,4 +230,4 @@ declare function createPlugin(options: GitHubPluginOptions): {
|
|
|
247
230
|
*/
|
|
248
231
|
declare const AUTH_HINT: string;
|
|
249
232
|
//#endregion
|
|
250
|
-
export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext,
|
|
233
|
+
export { AUTH_HINT, type Auth, AuthError, type CanonicalLabel, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubPluginOptions, GitHubSecrets, GitHubSource, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, syncLabels, verifyToken };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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
|
|
@@ -10,50 +11,27 @@ const resolveToken = createResolveToken({
|
|
|
10
11
|
errorMessage: "no GitHub token found. Pass --token <PAT>, set HOLOCRON_GH_TOKEN / GITHUB_TOKEN, or run: holocron auth set github <PAT>"
|
|
11
12
|
});
|
|
12
13
|
//#endregion
|
|
13
|
-
//#region src/repo.ts
|
|
14
|
-
var RepoError = class extends Error {
|
|
15
|
-
name = "RepoError";
|
|
16
|
-
};
|
|
17
|
-
function parseRepo(repo) {
|
|
18
|
-
if (!repo) throw new RepoError("repo is required; pass { repo: \"owner/name\" } in plugin options");
|
|
19
|
-
const [owner, name] = repo.split("/");
|
|
20
|
-
if (!owner || !name) throw new RepoError(`invalid repo "${repo}" — expected "owner/name"`);
|
|
21
|
-
return {
|
|
22
|
-
owner,
|
|
23
|
-
name
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
14
|
//#region src/capabilities/ci.ts
|
|
28
15
|
var GitHubCi = class {
|
|
29
|
-
|
|
16
|
+
client;
|
|
17
|
+
opts;
|
|
30
18
|
key = "ci";
|
|
31
19
|
providerName = "github";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.
|
|
35
|
-
const { owner, name } = parseRepo(opts.repo);
|
|
36
|
-
this.base = `/repos/${owner}/${name}/actions/runs`;
|
|
20
|
+
constructor(client, opts) {
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.opts = opts;
|
|
37
23
|
}
|
|
38
24
|
async listRuns(filter) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const path = qs ? `${this.base}?${qs}` : this.base;
|
|
45
|
-
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));
|
|
46
30
|
}
|
|
47
31
|
async getRun(id) {
|
|
48
|
-
const raw = await this.
|
|
32
|
+
const raw = await this.client.workflows.getRun(this.opts.repo, id);
|
|
49
33
|
return this.mapRun(raw);
|
|
50
34
|
}
|
|
51
|
-
/**
|
|
52
|
-
* GitHub reports run state as `status` plus `conclusion` (the latter
|
|
53
|
-
* is set once `status === 'completed'`). Holocron's `CiRunStatus`
|
|
54
|
-
* collapses these onto one axis — when complete, the conclusion is
|
|
55
|
-
* the meaningful value.
|
|
56
|
-
*/
|
|
57
35
|
mapRun(raw) {
|
|
58
36
|
const status = raw.status === "completed" && raw.conclusion ? this.mapConclusion(raw.conclusion) : raw.status;
|
|
59
37
|
return {
|
|
@@ -80,17 +58,16 @@ var GitHubCi = class {
|
|
|
80
58
|
//#endregion
|
|
81
59
|
//#region src/capabilities/environments.ts
|
|
82
60
|
var GitHubEnvironments = class {
|
|
83
|
-
|
|
61
|
+
client;
|
|
62
|
+
opts;
|
|
84
63
|
key = "environments";
|
|
85
64
|
providerName = "github";
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.
|
|
89
|
-
const { owner, name } = parseRepo(opts.repo);
|
|
90
|
-
this.base = `/repos/${owner}/${name}/environments`;
|
|
65
|
+
constructor(client, opts) {
|
|
66
|
+
this.client = client;
|
|
67
|
+
this.opts = opts;
|
|
91
68
|
}
|
|
92
69
|
async listEnvironments() {
|
|
93
|
-
return (await this.
|
|
70
|
+
return (await this.client.environments.listEnvironments(this.opts.repo)).map((e) => ({
|
|
94
71
|
name: e.name,
|
|
95
72
|
waitTimer: e.wait_timer,
|
|
96
73
|
preventSelfReview: e.prevent_self_review,
|
|
@@ -105,16 +82,10 @@ var GitHubEnvironments = class {
|
|
|
105
82
|
type: r.type,
|
|
106
83
|
id: r.id
|
|
107
84
|
}));
|
|
108
|
-
await this.
|
|
109
|
-
method: "PUT",
|
|
110
|
-
body
|
|
111
|
-
});
|
|
85
|
+
await this.client.environments.upsertEnvironment(this.opts.repo, env.name, body);
|
|
112
86
|
}
|
|
113
87
|
async deleteEnvironment(name) {
|
|
114
|
-
await this.
|
|
115
|
-
method: "DELETE",
|
|
116
|
-
expectNoContent: true
|
|
117
|
-
});
|
|
88
|
+
await this.client.environments.deleteEnvironment(this.opts.repo, name);
|
|
118
89
|
}
|
|
119
90
|
flattenReviewers(rules) {
|
|
120
91
|
if (!rules) return void 0;
|
|
@@ -129,41 +100,51 @@ var GitHubEnvironments = class {
|
|
|
129
100
|
//#endregion
|
|
130
101
|
//#region src/capabilities/issues.ts
|
|
131
102
|
var GitHubIssues = class {
|
|
132
|
-
|
|
103
|
+
client;
|
|
133
104
|
key = "issues";
|
|
134
105
|
providerName = "github";
|
|
135
106
|
owner;
|
|
136
107
|
repoName;
|
|
137
|
-
|
|
108
|
+
repo;
|
|
138
109
|
labels;
|
|
139
|
-
constructor(
|
|
140
|
-
this.
|
|
141
|
-
const
|
|
110
|
+
constructor(client, opts) {
|
|
111
|
+
this.client = client;
|
|
112
|
+
const [owner = "", repoName = ""] = opts.repo.split("/", 2);
|
|
142
113
|
this.owner = owner;
|
|
143
|
-
this.repoName =
|
|
144
|
-
this.
|
|
114
|
+
this.repoName = repoName;
|
|
115
|
+
this.repo = opts.repo;
|
|
145
116
|
this.labels = opts.labels;
|
|
146
117
|
}
|
|
147
118
|
async getMyself() {
|
|
148
|
-
|
|
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
|
+
};
|
|
149
125
|
}
|
|
150
126
|
async search(filter) {
|
|
151
|
-
const params =
|
|
127
|
+
const params = {
|
|
152
128
|
state: filter.openOnly ? "open" : "all",
|
|
153
129
|
sort: "updated",
|
|
154
130
|
direction: "desc",
|
|
155
|
-
per_page:
|
|
131
|
+
per_page: filter.limit ?? 50,
|
|
156
132
|
filter: "all"
|
|
157
|
-
}
|
|
158
|
-
if (filter.assignee === "currentUser")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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));
|
|
163
144
|
}
|
|
164
145
|
async get(key) {
|
|
165
146
|
const number = parseIssueNumber(key, this.owner, this.repoName);
|
|
166
|
-
const raw = await this.
|
|
147
|
+
const raw = await this.client.issues.getIssue(this.repo, number);
|
|
167
148
|
return this.mapIssue(raw);
|
|
168
149
|
}
|
|
169
150
|
async create(input) {
|
|
@@ -171,14 +152,11 @@ var GitHubIssues = class {
|
|
|
171
152
|
if (input.body) body.body = input.body;
|
|
172
153
|
if (input.labels?.length) body.labels = input.labels;
|
|
173
154
|
if (input.milestone) body.milestone = await this.resolveMilestone(input.milestone);
|
|
174
|
-
return { key: `#${(await this.
|
|
175
|
-
method: "POST",
|
|
176
|
-
body
|
|
177
|
-
})).number}` };
|
|
155
|
+
return { key: `#${(await this.client.issues.createIssue(this.repo, body)).number}` };
|
|
178
156
|
}
|
|
179
157
|
async transition(key, slot) {
|
|
180
158
|
const number = parseIssueNumber(key, this.owner, this.repoName);
|
|
181
|
-
const issue = await this.
|
|
159
|
+
const issue = await this.client.issues.getIssue(this.repo, number);
|
|
182
160
|
const currentStatusLabels = issue.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
|
|
183
161
|
if (slot === "done") {
|
|
184
162
|
if (issue.state === "closed") return {
|
|
@@ -187,12 +165,9 @@ var GitHubIssues = class {
|
|
|
187
165
|
via: "already closed"
|
|
188
166
|
};
|
|
189
167
|
await this.removeStatusLabels(number, currentStatusLabels);
|
|
190
|
-
await this.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
state: "closed",
|
|
194
|
-
state_reason: "completed"
|
|
195
|
-
}
|
|
168
|
+
await this.client.issues.updateIssue(this.repo, number, {
|
|
169
|
+
state: "closed",
|
|
170
|
+
state_reason: "completed"
|
|
196
171
|
});
|
|
197
172
|
return {
|
|
198
173
|
transitioned: true,
|
|
@@ -209,16 +184,10 @@ var GitHubIssues = class {
|
|
|
209
184
|
status: `open + ${targetLabel}`,
|
|
210
185
|
via: "already there"
|
|
211
186
|
};
|
|
212
|
-
if (!alreadyOpen) await this.
|
|
213
|
-
method: "PATCH",
|
|
214
|
-
body: { state: "open" }
|
|
215
|
-
});
|
|
187
|
+
if (!alreadyOpen) await this.client.issues.updateIssue(this.repo, number, { state: "open" });
|
|
216
188
|
const toRemove = currentStatusLabels.filter((n) => n !== targetLabel);
|
|
217
189
|
if (toRemove.length) await this.removeStatusLabels(number, toRemove);
|
|
218
|
-
if (!alreadyLabeled) await this.
|
|
219
|
-
method: "POST",
|
|
220
|
-
body: { labels: [targetLabel] }
|
|
221
|
-
});
|
|
190
|
+
if (!alreadyLabeled) await this.client.issues.addLabels(this.repo, number, [targetLabel]);
|
|
222
191
|
return {
|
|
223
192
|
transitioned: true,
|
|
224
193
|
status: `open + ${targetLabel}`,
|
|
@@ -227,15 +196,12 @@ var GitHubIssues = class {
|
|
|
227
196
|
}
|
|
228
197
|
async comment(key, body) {
|
|
229
198
|
const number = parseIssueNumber(key, this.owner, this.repoName);
|
|
230
|
-
await this.
|
|
231
|
-
method: "POST",
|
|
232
|
-
body: { body }
|
|
233
|
-
});
|
|
199
|
+
await this.client.issues.createComment(this.repo, number, body);
|
|
234
200
|
}
|
|
235
201
|
async doctor() {
|
|
236
202
|
const me = await this.getMyself();
|
|
237
|
-
const repo = await this.
|
|
238
|
-
const allLabels = await this.
|
|
203
|
+
const repo = await this.client.repos.getRepo(this.repo);
|
|
204
|
+
const allLabels = await this.client.labels.listLabels(this.repo);
|
|
239
205
|
const labelNames = new Set(allLabels.map((l) => l.name));
|
|
240
206
|
const statuses = [
|
|
241
207
|
{
|
|
@@ -302,12 +268,12 @@ var GitHubIssues = class {
|
|
|
302
268
|
}
|
|
303
269
|
async resolveMilestone(ref) {
|
|
304
270
|
if (/^\d+$/.test(ref)) return parseInt(ref, 10);
|
|
305
|
-
const match = (await this.
|
|
271
|
+
const match = (await this.client.issues.listMilestones(this.repo, { state: "all" })).find((m) => m.title.toLowerCase() === ref.toLowerCase());
|
|
306
272
|
if (!match) throw new Error(`Milestone "${ref}" not found in ${this.owner}/${this.repoName}. Use the numeric id or an exact title.`);
|
|
307
273
|
return match.number;
|
|
308
274
|
}
|
|
309
275
|
async removeStatusLabels(number, labels) {
|
|
310
|
-
for (const label of labels) await this.
|
|
276
|
+
for (const label of labels) await this.client.issues.removeLabel(this.repo, number, label);
|
|
311
277
|
}
|
|
312
278
|
mapIssue(raw) {
|
|
313
279
|
const statusLabels = raw.labels.map((l) => l.name).filter((n) => n.startsWith("status:"));
|
|
@@ -328,17 +294,16 @@ var GitHubIssues = class {
|
|
|
328
294
|
...raw.body ? { body: raw.body } : {},
|
|
329
295
|
status: statusName,
|
|
330
296
|
statusCategory: category,
|
|
331
|
-
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,
|
|
332
302
|
updated: raw.updated_at,
|
|
333
303
|
url: raw.html_url
|
|
334
304
|
};
|
|
335
305
|
}
|
|
336
306
|
};
|
|
337
|
-
/**
|
|
338
|
-
* Accept "#42", "42", or "owner/repo#42" and return the numeric id.
|
|
339
|
-
* Cross-repo refs are allowed in input but the adapter is bound to one
|
|
340
|
-
* repo — we error if the owner/repo doesn't match.
|
|
341
|
-
*/
|
|
342
307
|
function parseIssueNumber(key, owner, repoName) {
|
|
343
308
|
const trimmed = key.trim();
|
|
344
309
|
const crossMatch = trimmed.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
@@ -350,13 +315,6 @@ function parseIssueNumber(key, owner, repoName) {
|
|
|
350
315
|
if (!/^\d+$/.test(num)) throw new Error(`Invalid GitHub issue key "${key}" — expected #N or N.`);
|
|
351
316
|
return parseInt(num, 10);
|
|
352
317
|
}
|
|
353
|
-
function mapUser(raw) {
|
|
354
|
-
return {
|
|
355
|
-
id: raw.login,
|
|
356
|
-
displayName: raw.name ?? raw.login,
|
|
357
|
-
...raw.email ? { emailAddress: raw.email } : {}
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
318
|
//#endregion
|
|
361
319
|
//#region src/sodium.ts
|
|
362
320
|
/**
|
|
@@ -392,49 +350,38 @@ async function encryptSecret(publicKeyBase64, value) {
|
|
|
392
350
|
//#endregion
|
|
393
351
|
//#region src/capabilities/secrets.ts
|
|
394
352
|
var GitHubSecrets = class {
|
|
395
|
-
|
|
353
|
+
client;
|
|
354
|
+
opts;
|
|
396
355
|
key = "secrets";
|
|
397
356
|
providerName = "github";
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
this.
|
|
401
|
-
const { owner, name } = parseRepo(opts.repo);
|
|
402
|
-
this.repoBase = `/repos/${owner}/${name}`;
|
|
357
|
+
constructor(client, opts) {
|
|
358
|
+
this.client = client;
|
|
359
|
+
this.opts = opts;
|
|
403
360
|
}
|
|
404
361
|
async listSecrets(scope) {
|
|
405
|
-
|
|
406
|
-
return (await this.rest.request(`${base}/secrets`)).secrets.map((s) => s.name);
|
|
362
|
+
return this.client.secrets.listSecrets(this.opts.repo, toGHScope(scope));
|
|
407
363
|
}
|
|
408
364
|
async setSecret(scope, name, value) {
|
|
409
|
-
const
|
|
410
|
-
const pk = await this.
|
|
365
|
+
const ghScope = toGHScope(scope);
|
|
366
|
+
const pk = await this.client.secrets.getPublicKey(this.opts.repo, ghScope);
|
|
411
367
|
const body = {
|
|
412
368
|
encrypted_value: await encryptSecret(pk.key, value),
|
|
413
369
|
key_id: pk.key_id
|
|
414
370
|
};
|
|
415
371
|
if (scope.kind === "organization") body.visibility = "all";
|
|
416
|
-
await this.
|
|
417
|
-
method: "PUT",
|
|
418
|
-
body,
|
|
419
|
-
expectNoContent: true
|
|
420
|
-
});
|
|
372
|
+
await this.client.secrets.putSecret(this.opts.repo, ghScope, name, body);
|
|
421
373
|
}
|
|
422
374
|
async deleteSecret(scope, name) {
|
|
423
|
-
|
|
424
|
-
await this.rest.request(`${base}/secrets/${name}`, {
|
|
425
|
-
method: "DELETE",
|
|
426
|
-
expectNoContent: true
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
|
-
/** Endpoint root for the given scope. */
|
|
430
|
-
scopeBase(scope) {
|
|
431
|
-
switch (scope.kind) {
|
|
432
|
-
case "repo": return `${this.repoBase}/actions`;
|
|
433
|
-
case "environment": return `${this.repoBase}/environments/${scope.name}`;
|
|
434
|
-
case "organization": return `/orgs/${scope.name}/actions`;
|
|
435
|
-
}
|
|
375
|
+
await this.client.secrets.deleteSecret(this.opts.repo, toGHScope(scope), name);
|
|
436
376
|
}
|
|
437
377
|
};
|
|
378
|
+
function toGHScope(scope) {
|
|
379
|
+
if (scope.kind === "organization") return {
|
|
380
|
+
kind: "organization",
|
|
381
|
+
org: scope.name
|
|
382
|
+
};
|
|
383
|
+
return scope;
|
|
384
|
+
}
|
|
438
385
|
//#endregion
|
|
439
386
|
//#region src/capabilities/labels.ts
|
|
440
387
|
/**
|
|
@@ -445,10 +392,8 @@ var GitHubSecrets = class {
|
|
|
445
392
|
* - DELETEs labels listed in `stale`.
|
|
446
393
|
* - Leaves unrecognised labels that aren't in `stale` untouched.
|
|
447
394
|
*/
|
|
448
|
-
async function syncLabels(
|
|
449
|
-
const
|
|
450
|
-
const base = `/repos/${owner}/${name}`;
|
|
451
|
-
const existing = await rest.request(`${base}/labels?per_page=100`);
|
|
395
|
+
async function syncLabels(client, repo, canonical, stale) {
|
|
396
|
+
const existing = await client.labels.listLabels(repo);
|
|
452
397
|
const existingMap = new Map(existing.map((l) => [l.name.toLowerCase(), l]));
|
|
453
398
|
let created = 0;
|
|
454
399
|
let updated = 0;
|
|
@@ -456,103 +401,63 @@ async function syncLabels(rest, repo, canonical, stale) {
|
|
|
456
401
|
for (const label of canonical) {
|
|
457
402
|
const current = existingMap.get(label.name);
|
|
458
403
|
if (!current) {
|
|
459
|
-
await
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
color: label.color,
|
|
464
|
-
description: label.description
|
|
465
|
-
}
|
|
404
|
+
await client.labels.createLabel(repo, {
|
|
405
|
+
name: label.name,
|
|
406
|
+
color: label.color,
|
|
407
|
+
description: label.description
|
|
466
408
|
});
|
|
467
409
|
created++;
|
|
468
410
|
} else if (current.color !== label.color || (current.description ?? "") !== label.description) {
|
|
469
|
-
await
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
color: label.color,
|
|
473
|
-
description: label.description
|
|
474
|
-
}
|
|
411
|
+
await client.labels.updateLabel(repo, label.name, {
|
|
412
|
+
color: label.color,
|
|
413
|
+
description: label.description
|
|
475
414
|
});
|
|
476
415
|
updated++;
|
|
477
416
|
}
|
|
478
417
|
}
|
|
479
418
|
for (const staleName of stale) if (existingMap.has(staleName)) {
|
|
480
|
-
await
|
|
419
|
+
await client.labels.deleteLabel(repo, staleName);
|
|
481
420
|
deleted++;
|
|
482
421
|
}
|
|
483
422
|
return `${created} created, ${updated} updated, ${deleted} deleted`;
|
|
484
423
|
}
|
|
485
424
|
//#endregion
|
|
486
425
|
//#region src/capabilities/properties.ts
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
* Calls `PATCH /repos/{owner}/{name}/properties/values` with the full
|
|
491
|
-
* map — GitHub replaces only the supplied keys, leaving others untouched.
|
|
492
|
-
*/
|
|
493
|
-
async function syncProperties(rest, repo, values) {
|
|
494
|
-
const { owner, name } = parseRepo(repo);
|
|
495
|
-
const properties = Object.entries(values).map(([property_name, value]) => ({
|
|
496
|
-
property_name,
|
|
497
|
-
value
|
|
498
|
-
}));
|
|
499
|
-
await rest.request(`/repos/${owner}/${name}/properties/values`, {
|
|
500
|
-
method: "PATCH",
|
|
501
|
-
body: { properties }
|
|
502
|
-
});
|
|
503
|
-
return `${properties.length} properties set`;
|
|
426
|
+
async function syncProperties(client, repo, values) {
|
|
427
|
+
await client.properties.setProperties(repo, values);
|
|
428
|
+
return `${Object.keys(values).length} properties set`;
|
|
504
429
|
}
|
|
505
430
|
//#endregion
|
|
506
431
|
//#region src/capabilities/topics.ts
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
*
|
|
510
|
-
* Calls `PUT /repos/{owner}/{name}/topics` — GitHub replaces the full
|
|
511
|
-
* set atomically, so the config is always the source of truth.
|
|
512
|
-
*/
|
|
513
|
-
async function syncTopics(rest, repo, topics) {
|
|
514
|
-
const { owner, name } = parseRepo(repo);
|
|
515
|
-
await rest.request(`/repos/${owner}/${name}/topics`, {
|
|
516
|
-
method: "PUT",
|
|
517
|
-
body: { names: topics }
|
|
518
|
-
});
|
|
432
|
+
async function syncTopics(client, repo, topics) {
|
|
433
|
+
await client.topics.setTopics(repo, topics);
|
|
519
434
|
return `${topics.length} topics set`;
|
|
520
435
|
}
|
|
521
436
|
//#endregion
|
|
522
437
|
//#region src/capabilities/source.ts
|
|
523
|
-
/**
|
|
524
|
-
* `source` capability for GitHub.
|
|
525
|
-
*
|
|
526
|
-
* Ported from rando-id/rando.id `packages/cli/src/adapters/gh-rest.ts`
|
|
527
|
-
* with two changes for v2:
|
|
528
|
-
*
|
|
529
|
-
* - Repo coords are bound at construction time (not passed per call)
|
|
530
|
-
* - Workflow files are local-fs operations (not GH API) — they live
|
|
531
|
-
* in the consumer's `.github/workflows/` directory
|
|
532
|
-
*/
|
|
533
438
|
var GitHubSource = class {
|
|
534
|
-
|
|
439
|
+
client;
|
|
535
440
|
key = "source";
|
|
536
441
|
providerName = "github";
|
|
537
442
|
owner;
|
|
538
443
|
name;
|
|
539
|
-
|
|
444
|
+
repo;
|
|
540
445
|
repoRoot;
|
|
541
446
|
workflowDir;
|
|
542
|
-
constructor(
|
|
543
|
-
this.
|
|
544
|
-
const
|
|
447
|
+
constructor(client, opts) {
|
|
448
|
+
this.client = client;
|
|
449
|
+
const [owner = "", name = ""] = opts.repo.split("/", 2);
|
|
545
450
|
this.owner = owner;
|
|
546
451
|
this.name = name;
|
|
547
|
-
this.
|
|
452
|
+
this.repo = opts.repo;
|
|
548
453
|
this.repoRoot = opts.repoRoot;
|
|
549
454
|
this.workflowDir = join(opts.repoRoot, ".github", "workflows");
|
|
550
455
|
}
|
|
551
456
|
async whoami() {
|
|
552
|
-
return { login: (await this.
|
|
457
|
+
return { login: (await this.client.user.getCurrentUser()).login };
|
|
553
458
|
}
|
|
554
459
|
async getRepo() {
|
|
555
|
-
const data = await this.
|
|
460
|
+
const data = await this.client.repos.getRepo(this.repo);
|
|
556
461
|
return {
|
|
557
462
|
owner: this.owner,
|
|
558
463
|
name: this.name,
|
|
@@ -560,85 +465,40 @@ var GitHubSource = class {
|
|
|
560
465
|
};
|
|
561
466
|
}
|
|
562
467
|
async listRulesets() {
|
|
563
|
-
return this.
|
|
468
|
+
return this.client.rulesets.listRulesets(this.repo);
|
|
564
469
|
}
|
|
565
470
|
async createRuleset(payload) {
|
|
566
|
-
return this.
|
|
567
|
-
method: "POST",
|
|
568
|
-
body: payload
|
|
569
|
-
});
|
|
471
|
+
return this.client.rulesets.createRuleset(this.repo, payload);
|
|
570
472
|
}
|
|
571
473
|
async updateRuleset(id, payload) {
|
|
572
|
-
return this.
|
|
573
|
-
method: "PUT",
|
|
574
|
-
body: payload
|
|
575
|
-
});
|
|
474
|
+
return this.client.rulesets.updateRuleset(this.repo, id, payload);
|
|
576
475
|
}
|
|
577
476
|
async updateRepoSettings(settings) {
|
|
578
|
-
await this.
|
|
579
|
-
method: "PATCH",
|
|
580
|
-
body: settings
|
|
581
|
-
});
|
|
477
|
+
await this.client.repos.updateRepo(this.repo, settings);
|
|
582
478
|
}
|
|
583
479
|
async protectBranch(branch, payload) {
|
|
584
|
-
await this.
|
|
585
|
-
method: "PUT",
|
|
586
|
-
body: payload
|
|
587
|
-
});
|
|
480
|
+
await this.client.branches.protectBranch(this.repo, branch, payload);
|
|
588
481
|
}
|
|
589
482
|
async enableVulnerabilityAlerts() {
|
|
590
|
-
await this.
|
|
591
|
-
method: "PUT",
|
|
592
|
-
expectNoContent: true
|
|
593
|
-
});
|
|
483
|
+
await this.client.security.enableVulnerabilityAlerts(this.repo);
|
|
594
484
|
}
|
|
595
485
|
async enableAutomatedSecurityFixes() {
|
|
596
|
-
await this.
|
|
597
|
-
method: "PUT",
|
|
598
|
-
expectNoContent: true
|
|
599
|
-
});
|
|
486
|
+
await this.client.security.enableAutomatedSecurityFixes(this.repo);
|
|
600
487
|
}
|
|
601
488
|
async enableSecretScanning() {
|
|
602
|
-
await this.
|
|
603
|
-
method: "PATCH",
|
|
604
|
-
body: { security_and_analysis: {
|
|
605
|
-
secret_scanning: { status: "enabled" },
|
|
606
|
-
secret_scanning_push_protection: { status: "enabled" },
|
|
607
|
-
secret_scanning_validity_checks: { status: "enabled" },
|
|
608
|
-
secret_scanning_non_provider_patterns: { status: "enabled" }
|
|
609
|
-
} }
|
|
610
|
-
});
|
|
489
|
+
await this.client.security.enableSecretScanning(this.repo);
|
|
611
490
|
}
|
|
612
491
|
async enableCodeScanning() {
|
|
613
|
-
return `run ${(await this.
|
|
614
|
-
method: "PATCH",
|
|
615
|
-
body: {
|
|
616
|
-
state: "configured",
|
|
617
|
-
query_suite: "extended",
|
|
618
|
-
threat_model: "remote_and_local"
|
|
619
|
-
}
|
|
620
|
-
})).run_id}`;
|
|
492
|
+
return `run ${(await this.client.security.enableCodeScanning(this.repo)).run_id}`;
|
|
621
493
|
}
|
|
622
494
|
async disableDefaultCodeScanning() {
|
|
623
|
-
await this.
|
|
624
|
-
method: "PATCH",
|
|
625
|
-
body: { state: "not-configured" }
|
|
626
|
-
});
|
|
495
|
+
await this.client.security.disableDefaultCodeScanning(this.repo);
|
|
627
496
|
}
|
|
628
497
|
async enableDependencyGraph() {
|
|
629
|
-
await this.
|
|
630
|
-
method: "PATCH",
|
|
631
|
-
body: { security_and_analysis: {
|
|
632
|
-
dependency_graph: { status: "enabled" },
|
|
633
|
-
dependency_graph_autosubmit_action: { status: "enabled" }
|
|
634
|
-
} }
|
|
635
|
-
});
|
|
498
|
+
await this.client.security.enableDependencyGraph(this.repo);
|
|
636
499
|
}
|
|
637
500
|
async enablePrivateVulnerabilityReporting() {
|
|
638
|
-
await this.
|
|
639
|
-
method: "PUT",
|
|
640
|
-
expectNoContent: true
|
|
641
|
-
});
|
|
501
|
+
await this.client.security.enablePrivateVulnerabilityReporting(this.repo);
|
|
642
502
|
}
|
|
643
503
|
async listWorkflowFiles() {
|
|
644
504
|
try {
|
|
@@ -674,13 +534,13 @@ var GitHubSource = class {
|
|
|
674
534
|
await writeFile(full, contents, "utf8");
|
|
675
535
|
}
|
|
676
536
|
async syncLabels(canonical, stale) {
|
|
677
|
-
return syncLabels(this.
|
|
537
|
+
return syncLabels(this.client, this.repo, canonical, stale);
|
|
678
538
|
}
|
|
679
539
|
async syncProperties(values) {
|
|
680
|
-
return syncProperties(this.
|
|
540
|
+
return syncProperties(this.client, this.repo, values);
|
|
681
541
|
}
|
|
682
542
|
async syncTopics(topics) {
|
|
683
|
-
return syncTopics(this.
|
|
543
|
+
return syncTopics(this.client, this.repo, topics);
|
|
684
544
|
}
|
|
685
545
|
};
|
|
686
546
|
function isENOENT(err) {
|
|
@@ -698,20 +558,6 @@ async function ensureDir(path) {
|
|
|
698
558
|
}
|
|
699
559
|
}
|
|
700
560
|
//#endregion
|
|
701
|
-
//#region src/rest.ts
|
|
702
|
-
function createGitHubRestClient(opts) {
|
|
703
|
-
return createRestClient({
|
|
704
|
-
baseUrl: opts.baseUrl ?? "https://api.github.com",
|
|
705
|
-
token: opts.token,
|
|
706
|
-
extraHeaders: {
|
|
707
|
-
accept: "application/vnd.github+json",
|
|
708
|
-
"x-github-api-version": "2022-11-28"
|
|
709
|
-
},
|
|
710
|
-
vendor: "GitHub",
|
|
711
|
-
fetch: opts.fetch
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
//#endregion
|
|
715
561
|
//#region src/verify-token.ts
|
|
716
562
|
/**
|
|
717
563
|
* `verifyToken` — plugin-level export used by `holocron auth set` +
|
|
@@ -724,13 +570,13 @@ function createGitHubRestClient(opts) {
|
|
|
724
570
|
* what we don't have yet at bootstrap time.
|
|
725
571
|
*/
|
|
726
572
|
async function verifyToken(token, opts = {}) {
|
|
727
|
-
const
|
|
573
|
+
const client = createGitHubClient({
|
|
728
574
|
token,
|
|
729
575
|
baseUrl: opts.baseUrl,
|
|
730
576
|
fetch: opts.fetch
|
|
731
577
|
});
|
|
732
578
|
try {
|
|
733
|
-
const me = await
|
|
579
|
+
const me = await client.user.getCurrentUser();
|
|
734
580
|
return {
|
|
735
581
|
ok: true,
|
|
736
582
|
subject: `user @ ${me.login ?? me.email ?? "unknown"}`
|
|
@@ -747,7 +593,7 @@ async function verifyToken(token, opts = {}) {
|
|
|
747
593
|
function createContext(options) {
|
|
748
594
|
return {
|
|
749
595
|
options,
|
|
750
|
-
|
|
596
|
+
client: createGitHubClient$1({
|
|
751
597
|
token: resolveToken(options),
|
|
752
598
|
baseUrl: options.baseUrl,
|
|
753
599
|
fetch: options.fetch
|
|
@@ -757,24 +603,24 @@ function createContext(options) {
|
|
|
757
603
|
};
|
|
758
604
|
}
|
|
759
605
|
function source(ctx) {
|
|
760
|
-
return new GitHubSource(ctx.
|
|
606
|
+
return new GitHubSource(ctx.client, {
|
|
761
607
|
repo: ctx.repo,
|
|
762
608
|
repoRoot: ctx.repoRoot
|
|
763
609
|
});
|
|
764
610
|
}
|
|
765
611
|
function secrets(ctx) {
|
|
766
|
-
return new GitHubSecrets(ctx.
|
|
612
|
+
return new GitHubSecrets(ctx.client, { repo: ctx.repo });
|
|
767
613
|
}
|
|
768
614
|
function environments(ctx) {
|
|
769
|
-
return new GitHubEnvironments(ctx.
|
|
615
|
+
return new GitHubEnvironments(ctx.client, { repo: ctx.repo });
|
|
770
616
|
}
|
|
771
617
|
function ci(ctx) {
|
|
772
|
-
return new GitHubCi(ctx.
|
|
618
|
+
return new GitHubCi(ctx.client, { repo: ctx.repo });
|
|
773
619
|
}
|
|
774
620
|
function issues(ctx) {
|
|
775
621
|
const opts = { repo: ctx.repo };
|
|
776
622
|
if (ctx.options.labels !== void 0) opts.labels = ctx.options.labels;
|
|
777
|
-
return new GitHubIssues(ctx.
|
|
623
|
+
return new GitHubIssues(ctx.client, opts);
|
|
778
624
|
}
|
|
779
625
|
function createPlugin(options) {
|
|
780
626
|
const ctx = createContext(options);
|
|
@@ -797,4 +643,4 @@ function createPlugin(options) {
|
|
|
797
643
|
*/
|
|
798
644
|
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>";
|
|
799
645
|
//#endregion
|
|
800
|
-
export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createContext,
|
|
646
|
+
export { AUTH_HINT, AuthError, GitHubCi, GitHubEnvironments, GitHubIssues, GitHubSecrets, GitHubSource, ci, createContext, createGitHubClient, createPlugin, encryptSecret, environments, issues, resolveToken, secrets, source, syncLabels, 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.47",
|
|
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,12 +21,14 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@theholocron/
|
|
24
|
+
"@theholocron/github-client": "^0.5.0",
|
|
25
|
+
"@theholocron/cli": "2.0.0-alpha.47"
|
|
25
26
|
},
|
|
26
27
|
"dependencies": {
|
|
27
28
|
"libsodium-wrappers": "^0.7.15"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
31
|
+
"@theholocron/github-client": "^0.5.0",
|
|
30
32
|
"@types/node": "^26",
|
|
31
33
|
"@theholocron/eslint-config": "^5.2.0",
|
|
32
34
|
"@theholocron/tsconfig": "^6.0.0",
|
|
@@ -41,7 +43,7 @@
|
|
|
41
43
|
"tsx": "^4.22.4",
|
|
42
44
|
"typescript": "^5.9.3",
|
|
43
45
|
"vitest": "^4.1.10",
|
|
44
|
-
"@theholocron/cli": "2.0.0-alpha.
|
|
46
|
+
"@theholocron/cli": "2.0.0-alpha.47"
|
|
45
47
|
},
|
|
46
48
|
"publishConfig": {
|
|
47
49
|
"access": "public"
|