@effected/github 0.3.0 → 0.4.0

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.
@@ -0,0 +1,186 @@
1
+ import { GitHubError } from "./GitHubError.js";
2
+ import { GitHubClient } from "./GitHubClient.js";
3
+ import { Repo } from "./Repo.js";
4
+ import { encryptSecret } from "./internal/crypto.js";
5
+ import { Context, Effect, Layer, Redacted, Result } from "effect";
6
+
7
+ //#region src/RepositorySecret.ts
8
+ const ROUTES = {
9
+ actions: {
10
+ publicKey: "GET /repos/{owner}/{repo}/actions/secrets/public-key",
11
+ put: "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}",
12
+ list: "GET /repos/{owner}/{repo}/actions/secrets",
13
+ remove: "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}"
14
+ },
15
+ dependabot: {
16
+ publicKey: "GET /repos/{owner}/{repo}/dependabot/secrets/public-key",
17
+ put: "PUT /repos/{owner}/{repo}/dependabot/secrets/{secret_name}",
18
+ list: "GET /repos/{owner}/{repo}/dependabot/secrets",
19
+ remove: "DELETE /repos/{owner}/{repo}/dependabot/secrets/{secret_name}"
20
+ },
21
+ codespaces: {
22
+ publicKey: "GET /repos/{owner}/{repo}/codespaces/secrets/public-key",
23
+ put: "PUT /repos/{owner}/{repo}/codespaces/secrets/{secret_name}",
24
+ list: "GET /repos/{owner}/{repo}/codespaces/secrets",
25
+ remove: "DELETE /repos/{owner}/{repo}/codespaces/secrets/{secret_name}"
26
+ }
27
+ };
28
+ /**
29
+ * Secrets, encrypted client-side before they leave the process.
30
+ *
31
+ * @remarks
32
+ * Every write is a **two-step**: fetch the store's public key, then `PUT` a
33
+ * libsodium sealed box. The plaintext never crosses the wire, and the key fetch
34
+ * cannot be cached across stores because each has its own key.
35
+ *
36
+ * ## The value is `Redacted`
37
+ *
38
+ * Not decoration. A plaintext secret in a `string` is one interpolation, one
39
+ * `JSON.stringify` of a params object, or one logged error away from a
40
+ * transcript — and the log line that leaks it usually looks like a diagnostic
41
+ * someone added to debug an unrelated failure. `Redacted` closes those paths at
42
+ * the type; this module performs the single `Redacted.value` unwrap, at the
43
+ * moment of encryption, and the sealed box is what continues.
44
+ *
45
+ * @public
46
+ */
47
+ var RepositorySecret = class RepositorySecret extends Context.Service()("@effected/github/RepositorySecret") {
48
+ /**
49
+ * @remarks
50
+ * `(client) => make(client)` rather than `make`: a static initializer runs
51
+ * while the module body is still evaluating, so naming a `const` declared
52
+ * further down throws at import time with a clean typecheck.
53
+ */
54
+ static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
55
+ /** An in-memory double; unstubbed members die naming themselves. */
56
+ static makeTest = (overrides = {}) => ({
57
+ set: overrides.set ?? (() => unstubbed("set")),
58
+ list: overrides.list ?? (() => unstubbed("list")),
59
+ delete: overrides.delete ?? (() => unstubbed("delete")),
60
+ setForEnvironment: overrides.setForEnvironment ?? (() => unstubbed("setForEnvironment")),
61
+ listForEnvironment: overrides.listForEnvironment ?? (() => unstubbed("listForEnvironment")),
62
+ deleteForEnvironment: overrides.deleteForEnvironment ?? (() => unstubbed("deleteForEnvironment"))
63
+ });
64
+ /** {@link RepositorySecret.makeTest} behind a `Layer`. */
65
+ static layerTest = (overrides = {}) => Layer.succeed(RepositorySecret, RepositorySecret.makeTest(overrides));
66
+ };
67
+ const unstubbed = (member) => {
68
+ throw new Error(`RepositorySecret.makeTest: ${member}() was called but not stubbed — pass an override.`);
69
+ };
70
+ /**
71
+ * Seal a value, turning a malformed public key into a typed failure.
72
+ *
73
+ * @remarks
74
+ * `encryptSecret` returns a `Result` because a base64 decode can fail. A public
75
+ * key GitHub cannot have produced is still *input*, and input failures are
76
+ * typed rather than thrown — so this maps it onto the same `GitHubError` a
77
+ * caller already handles, naming the route it came from.
78
+ */
79
+ const seal = (route, publicKey, value) => Result.match(encryptSecret(publicKey, Redacted.value(value)), {
80
+ onSuccess: (sealed) => Effect.succeed(sealed),
81
+ onFailure: () => Effect.fail(GitHubError.decode(route, "the secrets public key was not valid base64"))
82
+ });
83
+ const make = (client) => {
84
+ return {
85
+ set: Effect.fn("RepositorySecret.set")(function* (name, value, scope = "actions") {
86
+ const { owner, repo } = yield* Repo;
87
+ yield* Effect.annotateCurrentSpan({
88
+ owner,
89
+ repo,
90
+ scope,
91
+ secret: name
92
+ });
93
+ const routes = ROUTES[scope];
94
+ const publicKey = yield* client.request(routes.publicKey, {
95
+ owner,
96
+ repo
97
+ });
98
+ yield* client.request(routes.put, {
99
+ owner,
100
+ repo,
101
+ secret_name: name,
102
+ encrypted_value: yield* seal(routes.publicKey, publicKey.key, value),
103
+ key_id: publicKey.key_id
104
+ });
105
+ }),
106
+ list: Effect.fn("RepositorySecret.list")(function* (scope = "actions") {
107
+ const { owner, repo } = yield* Repo;
108
+ yield* Effect.annotateCurrentSpan({
109
+ owner,
110
+ repo,
111
+ scope
112
+ });
113
+ return (yield* client.paginate(ROUTES[scope].list, {
114
+ owner,
115
+ repo
116
+ })).map((secret) => ({ name: secret.name }));
117
+ }),
118
+ delete: Effect.fn("RepositorySecret.delete")(function* (name, scope = "actions") {
119
+ const { owner, repo } = yield* Repo;
120
+ yield* Effect.annotateCurrentSpan({
121
+ owner,
122
+ repo,
123
+ scope,
124
+ secret: name
125
+ });
126
+ yield* client.request(ROUTES[scope].remove, {
127
+ owner,
128
+ repo,
129
+ secret_name: name
130
+ });
131
+ }),
132
+ setForEnvironment: Effect.fn("RepositorySecret.setForEnvironment")(function* (environment, name, value) {
133
+ const { owner, repo } = yield* Repo;
134
+ yield* Effect.annotateCurrentSpan({
135
+ owner,
136
+ repo,
137
+ environment,
138
+ secret: name
139
+ });
140
+ const publicKey = yield* client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", {
141
+ owner,
142
+ repo,
143
+ environment_name: environment
144
+ });
145
+ yield* client.request("PUT /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", {
146
+ owner,
147
+ repo,
148
+ environment_name: environment,
149
+ secret_name: name,
150
+ encrypted_value: yield* seal("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets/public-key", publicKey.key, value),
151
+ key_id: publicKey.key_id
152
+ });
153
+ }),
154
+ listForEnvironment: Effect.fn("RepositorySecret.listForEnvironment")(function* (environment) {
155
+ const { owner, repo } = yield* Repo;
156
+ yield* Effect.annotateCurrentSpan({
157
+ owner,
158
+ repo,
159
+ environment
160
+ });
161
+ return (yield* client.paginate("GET /repos/{owner}/{repo}/environments/{environment_name}/secrets", {
162
+ owner,
163
+ repo,
164
+ environment_name: environment
165
+ })).map((secret) => ({ name: secret.name }));
166
+ }),
167
+ deleteForEnvironment: Effect.fn("RepositorySecret.deleteForEnvironment")(function* (environment, name) {
168
+ const { owner, repo } = yield* Repo;
169
+ yield* Effect.annotateCurrentSpan({
170
+ owner,
171
+ repo,
172
+ environment,
173
+ secret: name
174
+ });
175
+ yield* client.request("DELETE /repos/{owner}/{repo}/environments/{environment_name}/secrets/{secret_name}", {
176
+ owner,
177
+ repo,
178
+ environment_name: environment,
179
+ secret_name: name
180
+ });
181
+ })
182
+ };
183
+ };
184
+
185
+ //#endregion
186
+ export { RepositorySecret };
@@ -0,0 +1,141 @@
1
+ import { GitHubClient } from "./GitHubClient.js";
2
+ import { Repo } from "./Repo.js";
3
+ import { Context, Effect, Layer } from "effect";
4
+
5
+ //#region src/RepositorySecurity.ts
6
+ /**
7
+ * Repository security features with dedicated endpoints.
8
+ *
9
+ * @remarks
10
+ * These are **not** `security_and_analysis` fields and cannot ride along on the
11
+ * settings `PATCH`. Each is its own pair of endpoints where **the HTTP verb is
12
+ * the value**, which is why every setter branches on `enabled` rather than
13
+ * sending a body.
14
+ *
15
+ * ## Reading them is inconsistent, and the inconsistency is GitHub's
16
+ *
17
+ * Preserved faithfully rather than smoothed over, because smoothing it would
18
+ * mean inventing a behaviour for one of the three:
19
+ *
20
+ * | Feature | Enabled | Disabled |
21
+ * | :--- | :--- | :--- |
22
+ * | `vulnerability-alerts` | `204` | **`404`** |
23
+ * | `automated-security-fixes` | `200 { enabled: true }` | `200 { enabled: false }` |
24
+ * | `private-vulnerability-reporting` | `200 { enabled: true }` | `200 { enabled: false }` |
25
+ *
26
+ * So `vulnerabilityAlerts` maps `notFound` to `false` — and **only** `notFound`;
27
+ * every other failure still fails. A 404 from the other two is a real failure
28
+ * and stays one, which is why the mapping is not applied uniformly.
29
+ *
30
+ * @public
31
+ */
32
+ var RepositorySecurity = class RepositorySecurity extends Context.Service()("@effected/github/RepositorySecurity") {
33
+ /**
34
+ * @remarks
35
+ * `(client) => make(client)` rather than `make`: a static initializer runs
36
+ * while the module body is still evaluating, so naming a `const` declared
37
+ * further down throws at import time with a clean typecheck.
38
+ */
39
+ static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
40
+ /** An in-memory double; unstubbed members die naming themselves. */
41
+ static makeTest = (overrides = {}) => ({
42
+ vulnerabilityAlerts: overrides.vulnerabilityAlerts ?? (() => unstubbed("vulnerabilityAlerts")),
43
+ setVulnerabilityAlerts: overrides.setVulnerabilityAlerts ?? (() => unstubbed("setVulnerabilityAlerts")),
44
+ automatedSecurityFixes: overrides.automatedSecurityFixes ?? (() => unstubbed("automatedSecurityFixes")),
45
+ setAutomatedSecurityFixes: overrides.setAutomatedSecurityFixes ?? (() => unstubbed("setAutomatedSecurityFixes")),
46
+ privateVulnerabilityReporting: overrides.privateVulnerabilityReporting ?? (() => unstubbed("privateVulnerabilityReporting")),
47
+ setPrivateVulnerabilityReporting: overrides.setPrivateVulnerabilityReporting ?? (() => unstubbed("setPrivateVulnerabilityReporting"))
48
+ });
49
+ /** {@link RepositorySecurity.makeTest} behind a `Layer`. */
50
+ static layerTest = (overrides = {}) => Layer.succeed(RepositorySecurity, RepositorySecurity.makeTest(overrides));
51
+ };
52
+ const unstubbed = (member) => {
53
+ throw new Error(`RepositorySecurity.makeTest: ${member}() was called but not stubbed — pass an override.`);
54
+ };
55
+ const make = (client) => {
56
+ return {
57
+ vulnerabilityAlerts: Effect.fn("RepositorySecurity.vulnerabilityAlerts")(function* () {
58
+ const { owner, repo } = yield* Repo;
59
+ yield* Effect.annotateCurrentSpan({
60
+ owner,
61
+ repo
62
+ });
63
+ return yield* client.request("GET /repos/{owner}/{repo}/vulnerability-alerts", {
64
+ owner,
65
+ repo
66
+ }).pipe(Effect.as(true), Effect.catchIf((error) => error.kind === "notFound", () => Effect.succeed(false)));
67
+ }),
68
+ setVulnerabilityAlerts: Effect.fn("RepositorySecurity.setVulnerabilityAlerts")(function* (enabled) {
69
+ const { owner, repo } = yield* Repo;
70
+ yield* Effect.annotateCurrentSpan({
71
+ owner,
72
+ repo,
73
+ enabled
74
+ });
75
+ yield* enabled ? client.request("PUT /repos/{owner}/{repo}/vulnerability-alerts", {
76
+ owner,
77
+ repo
78
+ }) : client.request("DELETE /repos/{owner}/{repo}/vulnerability-alerts", {
79
+ owner,
80
+ repo
81
+ });
82
+ }),
83
+ automatedSecurityFixes: Effect.fn("RepositorySecurity.automatedSecurityFixes")(function* () {
84
+ const { owner, repo } = yield* Repo;
85
+ yield* Effect.annotateCurrentSpan({
86
+ owner,
87
+ repo
88
+ });
89
+ const data = yield* client.request("GET /repos/{owner}/{repo}/automated-security-fixes", {
90
+ owner,
91
+ repo
92
+ });
93
+ return Boolean(data.enabled);
94
+ }),
95
+ setAutomatedSecurityFixes: Effect.fn("RepositorySecurity.setAutomatedSecurityFixes")(function* (enabled) {
96
+ const { owner, repo } = yield* Repo;
97
+ yield* Effect.annotateCurrentSpan({
98
+ owner,
99
+ repo,
100
+ enabled
101
+ });
102
+ yield* enabled ? client.request("PUT /repos/{owner}/{repo}/automated-security-fixes", {
103
+ owner,
104
+ repo
105
+ }) : client.request("DELETE /repos/{owner}/{repo}/automated-security-fixes", {
106
+ owner,
107
+ repo
108
+ });
109
+ }),
110
+ privateVulnerabilityReporting: Effect.fn("RepositorySecurity.privateVulnerabilityReporting")(function* () {
111
+ const { owner, repo } = yield* Repo;
112
+ yield* Effect.annotateCurrentSpan({
113
+ owner,
114
+ repo
115
+ });
116
+ const data = yield* client.request("GET /repos/{owner}/{repo}/private-vulnerability-reporting", {
117
+ owner,
118
+ repo
119
+ });
120
+ return Boolean(data.enabled);
121
+ }),
122
+ setPrivateVulnerabilityReporting: Effect.fn("RepositorySecurity.setPrivateVulnerabilityReporting")(function* (enabled) {
123
+ const { owner, repo } = yield* Repo;
124
+ yield* Effect.annotateCurrentSpan({
125
+ owner,
126
+ repo,
127
+ enabled
128
+ });
129
+ yield* enabled ? client.request("PUT /repos/{owner}/{repo}/private-vulnerability-reporting", {
130
+ owner,
131
+ repo
132
+ }) : client.request("DELETE /repos/{owner}/{repo}/private-vulnerability-reporting", {
133
+ owner,
134
+ repo
135
+ });
136
+ })
137
+ };
138
+ };
139
+
140
+ //#endregion
141
+ export { RepositorySecurity };
@@ -0,0 +1,174 @@
1
+ import { GitHubClient } from "./GitHubClient.js";
2
+ import { Repo } from "./Repo.js";
3
+ import { Context, Effect, Layer } from "effect";
4
+
5
+ //#region src/RepositoryVariable.ts
6
+ /**
7
+ * Repository and environment variables.
8
+ *
9
+ * @remarks
10
+ * No encryption and no public key, unlike secrets — but also **no upsert**,
11
+ * which is the asymmetry worth knowing: every write costs a read first, because
12
+ * the create and update routes are different endpoints with different verbs and
13
+ * neither tolerates the other's case.
14
+ *
15
+ * @public
16
+ */
17
+ var RepositoryVariable = class RepositoryVariable extends Context.Service()("@effected/github/RepositoryVariable") {
18
+ /**
19
+ * @remarks
20
+ * `(client) => make(client)` rather than `make`: a static initializer runs
21
+ * while the module body is still evaluating, so naming a `const` declared
22
+ * further down throws at import time with a clean typecheck.
23
+ */
24
+ static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
25
+ /** An in-memory double; unstubbed members die naming themselves. */
26
+ static makeTest = (overrides = {}) => ({
27
+ set: overrides.set ?? (() => unstubbed("set")),
28
+ list: overrides.list ?? (() => unstubbed("list")),
29
+ delete: overrides.delete ?? (() => unstubbed("delete")),
30
+ setForEnvironment: overrides.setForEnvironment ?? (() => unstubbed("setForEnvironment")),
31
+ listForEnvironment: overrides.listForEnvironment ?? (() => unstubbed("listForEnvironment")),
32
+ deleteForEnvironment: overrides.deleteForEnvironment ?? (() => unstubbed("deleteForEnvironment"))
33
+ });
34
+ /** {@link RepositoryVariable.makeTest} behind a `Layer`. */
35
+ static layerTest = (overrides = {}) => Layer.succeed(RepositoryVariable, RepositoryVariable.makeTest(overrides));
36
+ };
37
+ const unstubbed = (member) => {
38
+ throw new Error(`RepositoryVariable.makeTest: ${member}() was called but not stubbed — pass an override.`);
39
+ };
40
+ const make = (client) => {
41
+ /**
42
+ * Does this variable already exist? One by-name read, not a listing.
43
+ *
44
+ * @remarks
45
+ * GitHub answers 404 for an absent variable, which makes the pre-write check
46
+ * constant cost — paginating the whole collection to answer one yes/no grows
47
+ * with a repository that has nothing to do with the variable being written.
48
+ * Only `notFound` is absorbed: a 403 from a mis-scoped token still fails,
49
+ * which a blanket "treat any error as absent" would destroy, turning a
50
+ * permissions problem into a spurious create.
51
+ */
52
+ const exists = (route, params) => client.request(route, params).pipe(Effect.as(true), Effect.catchIf((error) => error.kind === "notFound", () => Effect.succeed(false)));
53
+ return {
54
+ set: Effect.fn("RepositoryVariable.set")(function* (name, value) {
55
+ const { owner, repo } = yield* Repo;
56
+ yield* Effect.annotateCurrentSpan({
57
+ owner,
58
+ repo,
59
+ variable: name
60
+ });
61
+ if (yield* exists("GET /repos/{owner}/{repo}/actions/variables/{name}", {
62
+ owner,
63
+ repo,
64
+ name
65
+ })) {
66
+ yield* client.request("PATCH /repos/{owner}/{repo}/actions/variables/{name}", {
67
+ owner,
68
+ repo,
69
+ name,
70
+ value
71
+ });
72
+ return;
73
+ }
74
+ yield* client.request("POST /repos/{owner}/{repo}/actions/variables", {
75
+ owner,
76
+ repo,
77
+ name,
78
+ value
79
+ });
80
+ }),
81
+ list: Effect.fn("RepositoryVariable.list")(function* () {
82
+ const { owner, repo } = yield* Repo;
83
+ yield* Effect.annotateCurrentSpan({
84
+ owner,
85
+ repo
86
+ });
87
+ return (yield* client.paginate("GET /repos/{owner}/{repo}/actions/variables", {
88
+ owner,
89
+ repo
90
+ })).map((variable) => ({
91
+ name: variable.name,
92
+ value: variable.value
93
+ }));
94
+ }),
95
+ delete: Effect.fn("RepositoryVariable.delete")(function* (name) {
96
+ const { owner, repo } = yield* Repo;
97
+ yield* Effect.annotateCurrentSpan({
98
+ owner,
99
+ repo,
100
+ variable: name
101
+ });
102
+ yield* client.request("DELETE /repos/{owner}/{repo}/actions/variables/{name}", {
103
+ owner,
104
+ repo,
105
+ name
106
+ });
107
+ }),
108
+ setForEnvironment: Effect.fn("RepositoryVariable.setForEnvironment")(function* (environment, name, value) {
109
+ const { owner, repo } = yield* Repo;
110
+ yield* Effect.annotateCurrentSpan({
111
+ owner,
112
+ repo,
113
+ environment,
114
+ variable: name
115
+ });
116
+ if (yield* client.request("GET /repos/{owner}/{repo}/environments/{environment_name}/variables/{name}", {
117
+ owner,
118
+ repo,
119
+ environment_name: environment,
120
+ name
121
+ }).pipe(Effect.as(true), Effect.catchIf((error) => error.kind === "notFound", () => Effect.succeed(false)))) {
122
+ yield* client.request("PATCH /repos/{owner}/{repo}/environments/{environment_name}/variables/{name}", {
123
+ owner,
124
+ repo,
125
+ environment_name: environment,
126
+ name,
127
+ value
128
+ });
129
+ return;
130
+ }
131
+ yield* client.request("POST /repos/{owner}/{repo}/environments/{environment_name}/variables", {
132
+ owner,
133
+ repo,
134
+ environment_name: environment,
135
+ name,
136
+ value
137
+ });
138
+ }),
139
+ listForEnvironment: Effect.fn("RepositoryVariable.listForEnvironment")(function* (environment) {
140
+ const { owner, repo } = yield* Repo;
141
+ yield* Effect.annotateCurrentSpan({
142
+ owner,
143
+ repo,
144
+ environment
145
+ });
146
+ return (yield* client.paginate("GET /repos/{owner}/{repo}/environments/{environment_name}/variables", {
147
+ owner,
148
+ repo,
149
+ environment_name: environment
150
+ })).map((variable) => ({
151
+ name: variable.name,
152
+ value: variable.value
153
+ }));
154
+ }),
155
+ deleteForEnvironment: Effect.fn("RepositoryVariable.deleteForEnvironment")(function* (environment, name) {
156
+ const { owner, repo } = yield* Repo;
157
+ yield* Effect.annotateCurrentSpan({
158
+ owner,
159
+ repo,
160
+ environment,
161
+ variable: name
162
+ });
163
+ yield* client.request("DELETE /repos/{owner}/{repo}/environments/{environment_name}/variables/{name}", {
164
+ owner,
165
+ repo,
166
+ environment_name: environment,
167
+ name
168
+ });
169
+ })
170
+ };
171
+ };
172
+
173
+ //#endregion
174
+ export { RepositoryVariable };
package/Ruleset.js ADDED
@@ -0,0 +1,143 @@
1
+ import { GitHubError } from "./GitHubError.js";
2
+ import { GitHubClient } from "./GitHubClient.js";
3
+ import { Repo } from "./Repo.js";
4
+ import { Context, Effect, Layer } from "effect";
5
+
6
+ //#region src/Ruleset.ts
7
+ /**
8
+ * Repository rulesets.
9
+ *
10
+ * @remarks
11
+ * ## An inherited ruleset is never written to
12
+ *
13
+ * `GET /repos/{owner}/{repo}/rulesets` returns rulesets **inherited from the
14
+ * organization** alongside the repository's own. Matching by name alone lets a
15
+ * repository-scoped call issue a `PUT` against the organization's ruleset id —
16
+ * rewriting policy for **every repository the organization owns**, from a caller
17
+ * that never mentioned the organization.
18
+ *
19
+ * {@link RulesetShape.upsert} filters on `source_type` before matching, so an
20
+ * inherited ruleset can never be the target of a write. This arrived as a fix
21
+ * for a live defect in the consumer this module was ported from, where the
22
+ * filter was absent.
23
+ *
24
+ * @public
25
+ */
26
+ var Ruleset = class Ruleset extends Context.Service()("@effected/github/Ruleset") {
27
+ /**
28
+ * @remarks
29
+ * `(client) => make(client)` rather than `make`: a static initializer runs
30
+ * while the module body is still evaluating, so naming a `const` declared
31
+ * further down throws at import time with a clean typecheck.
32
+ */
33
+ static layer = Layer.effect(this, Effect.map(GitHubClient, (client) => make(client)));
34
+ /** An in-memory double; unstubbed members die naming themselves. */
35
+ static makeTest = (overrides = {}) => ({
36
+ upsert: overrides.upsert ?? (() => unstubbed("upsert")),
37
+ list: overrides.list ?? (() => unstubbed("list")),
38
+ delete: overrides.delete ?? (() => unstubbed("delete")),
39
+ teamId: overrides.teamId ?? (() => unstubbed("teamId")),
40
+ roleId: overrides.roleId ?? (() => unstubbed("roleId"))
41
+ });
42
+ /** {@link Ruleset.makeTest} behind a `Layer`. */
43
+ static layerTest = (overrides = {}) => Layer.succeed(Ruleset, Ruleset.makeTest(overrides));
44
+ };
45
+ const unstubbed = (member) => {
46
+ throw new Error(`Ruleset.makeTest: ${member}() was called but not stubbed — pass an override.`);
47
+ };
48
+ /** An inherited ruleset belongs to the organization and is not this repository's to write. */
49
+ const isOwnedByRepository = (ruleset) => ruleset.source_type !== "Organization";
50
+ const make = (client) => {
51
+ return {
52
+ upsert: Effect.fn("Ruleset.upsert")(function* (payload) {
53
+ const { owner, repo } = yield* Repo;
54
+ yield* Effect.annotateCurrentSpan({
55
+ owner,
56
+ repo,
57
+ ruleset: payload.name
58
+ });
59
+ const match = (yield* client.paginate("GET /repos/{owner}/{repo}/rulesets", {
60
+ owner,
61
+ repo
62
+ })).find((ruleset) => ruleset.name === payload.name && isOwnedByRepository(ruleset));
63
+ const body = {
64
+ name: payload.name,
65
+ target: payload.target,
66
+ enforcement: payload.enforcement,
67
+ ...payload.conditions !== void 0 ? { conditions: payload.conditions } : {},
68
+ ...payload.rules !== void 0 ? { rules: payload.rules } : {},
69
+ ...payload.bypass_actors !== void 0 ? { bypass_actors: payload.bypass_actors } : {}
70
+ };
71
+ if (match !== void 0) {
72
+ yield* client.request("PUT /repos/{owner}/{repo}/rulesets/{ruleset_id}", {
73
+ owner,
74
+ repo,
75
+ ruleset_id: match.id,
76
+ ...body
77
+ });
78
+ return;
79
+ }
80
+ yield* client.request("POST /repos/{owner}/{repo}/rulesets", {
81
+ owner,
82
+ repo,
83
+ ...body
84
+ });
85
+ }),
86
+ list: Effect.fn("Ruleset.list")(function* () {
87
+ const { owner, repo } = yield* Repo;
88
+ yield* Effect.annotateCurrentSpan({
89
+ owner,
90
+ repo
91
+ });
92
+ return (yield* client.paginate("GET /repos/{owner}/{repo}/rulesets", {
93
+ owner,
94
+ repo
95
+ })).map((ruleset) => ({
96
+ id: ruleset.id,
97
+ name: ruleset.name,
98
+ source_type: ruleset.source_type
99
+ }));
100
+ }),
101
+ delete: Effect.fn("Ruleset.delete")(function* (rulesetId) {
102
+ const { owner, repo } = yield* Repo;
103
+ yield* Effect.annotateCurrentSpan({
104
+ owner,
105
+ repo,
106
+ ruleset_id: rulesetId
107
+ });
108
+ yield* client.request("DELETE /repos/{owner}/{repo}/rulesets/{ruleset_id}", {
109
+ owner,
110
+ repo,
111
+ ruleset_id: rulesetId
112
+ });
113
+ }),
114
+ teamId: Effect.fn("Ruleset.teamId")(function* (slug) {
115
+ const { owner } = yield* Repo;
116
+ yield* Effect.annotateCurrentSpan({
117
+ org: owner,
118
+ team_slug: slug
119
+ });
120
+ return (yield* client.request("GET /orgs/{org}/teams/{team_slug}", {
121
+ org: owner,
122
+ team_slug: slug
123
+ })).id;
124
+ }),
125
+ roleId: Effect.fn("Ruleset.roleId")(function* (name) {
126
+ const { owner } = yield* Repo;
127
+ yield* Effect.annotateCurrentSpan({
128
+ org: owner,
129
+ role: name
130
+ });
131
+ const roles = (yield* client.request("GET /orgs/{org}/organization-roles", { org: owner })).roles ?? [];
132
+ const role = roles.find((candidate) => candidate.name === name);
133
+ if (role === void 0) {
134
+ const available = roles.map((candidate) => candidate.name).join(", ");
135
+ return yield* GitHubError.notFound("Ruleset.roleId", `organization role '${name}' in '${owner}' (available: ${available || "none"})`);
136
+ }
137
+ return role.id;
138
+ })
139
+ };
140
+ };
141
+
142
+ //#endregion
143
+ export { Ruleset };
@@ -36,6 +36,7 @@ var WorkflowDispatch = class WorkflowDispatch extends Context.Service()("@effect
36
36
  static makeTest = (overrides = {}) => ({
37
37
  dispatch: overrides.dispatch ?? (() => unstubbed("dispatch")),
38
38
  runStatus: overrides.runStatus ?? (() => unstubbed("runStatus")),
39
+ list: overrides.list ?? Effect.sync(() => unstubbed("list")),
39
40
  dispatchAndWait: overrides.dispatchAndWait ?? (() => unstubbed("dispatchAndWait"))
40
41
  });
41
42
  /** {@link WorkflowDispatch.makeTest} behind a `Layer`. */
@@ -83,6 +84,22 @@ const make = (client) => {
83
84
  });
84
85
  return statusOf(raw);
85
86
  }),
87
+ list: Effect.fn("WorkflowDispatch.list")(function* () {
88
+ const { owner, repo } = yield* Repo;
89
+ yield* Effect.annotateCurrentSpan({
90
+ owner,
91
+ repo
92
+ });
93
+ return (yield* client.paginate("GET /repos/{owner}/{repo}/actions/workflows", {
94
+ owner,
95
+ repo
96
+ })).map((workflow) => ({
97
+ id: workflow.id,
98
+ name: workflow.name,
99
+ path: workflow.path,
100
+ state: workflow.state
101
+ }));
102
+ })(),
86
103
  dispatchAndWait: Effect.fn("WorkflowDispatch.dispatchAndWait")(function* (workflow, ref, options) {
87
104
  const { owner, repo } = yield* Repo;
88
105
  const interval = options?.poll?.interval ?? DEFAULT_INTERVAL;