@effected/github 0.15.1 → 0.17.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.
package/README.md CHANGED
@@ -25,7 +25,7 @@ Hand-rolled octokit wrappers tend to converge on the same shape: a `rest<T>(oper
25
25
 
26
26
  The error model gets the same treatment. `GitHubError.kind` is a literal union (`notFound`, `alreadyExists`, `rejected`, `unauthorized`, `rateLimited`, `transport`, `decode`) produced by one classification step, `GitHubError.fromOctokit`, instead of one hand-written mapper per resource. `kind: "alreadyExists"` is what makes `GitBranch.upsert` and `GitTag.upsert` a single round trip in the common case, rather than a create-then-catch-then-check-then-reset dance repeated at every call site that needs it.
27
27
 
28
- This package also owns the octokit runtime so nothing downstream has to. `@octokit/rest` and `@octokit/auth-app` are deliberately absent — the rest wrapper is a request-logging plugin plus 1.4 MB of duplicate endpoint types, and `auth-app` drags in `createOAuthUserAuth`, roughly 492 KB of OAuth machinery this package never calls. GitHub App JWTs are signed by `universal-github-app-jwt` instead, the same zero-dependency signer `@octokit/auth-app` itself uses. A consumer that only authenticates with a token it already holds never links the JWT signer at all: the App-authenticated layer lives in its own module (`GitHubApp`), and nothing here is gathered into a namespace object that would defeat that split.
28
+ This package also owns the octokit runtime so nothing downstream has to. `@octokit/rest` and `@octokit/auth-app` are deliberately absent — the rest wrapper is a request-logging plugin plus 1.4 MB of duplicate endpoint types, and `auth-app` drags in `createOAuthUserAuth`, roughly 492 KB of OAuth machinery this package never calls. GitHub App JWTs are signed by the kit's own `@effected/jwt` instead: WebCrypto RS256 with no runtime dependencies, so App auth runs on Cloudflare Workers as well as Node. A consumer that only authenticates with a token it already holds never links the JWT signer at all: the App-authenticated layer lives in its own module (`GitHubApp`), and nothing here is gathered into a namespace object that would defeat that split.
29
29
 
30
30
  ## Install
31
31
 
package/Repo.js CHANGED
@@ -16,6 +16,15 @@ input: Schema.String }) {
16
16
  /**
17
17
  * Which repository an operation acts on.
18
18
  *
19
+ * @remarks
20
+ * **`RepoRef.make` validates at construction and throws** on an empty `owner`
21
+ * or `repo`, so inside an `Effect` a bad value dies rather than failing. That
22
+ * is right for a coordinate the program itself spells out, and wrong for one
23
+ * built from data: for a slug read from a database row, a config file or a
24
+ * webhook, use {@link RepoRef.parse}, which fails with a typed
25
+ * {@link InvalidRepoRefError} instead (or {@link RepoRef.parseResult}
26
+ * outside an `Effect`).
27
+ *
19
28
  * @public
20
29
  */
21
30
  var RepoRef = class RepoRef extends Schema.Class("RepoRef")({
@@ -41,7 +50,13 @@ var RepoRef = class RepoRef extends Schema.Class("RepoRef")({
41
50
  repo
42
51
  }));
43
52
  }
44
- /** Parse `"owner/repo"`. */
53
+ /**
54
+ * Parse `"owner/repo"`, failing with a typed {@link InvalidRepoRefError}.
55
+ *
56
+ * @remarks
57
+ * The typed path for building a `Repo` from stored data, such as a stored
58
+ * `full_name`: a malformed value fails here, where `make` would die.
59
+ */
45
60
  static parse = Effect.fn("RepoRef.parse")((slug) => Effect.fromResult(RepoRef.parseResult(slug)));
46
61
  /** `"owner/repo"`. */
47
62
  get slug() {
@@ -26,6 +26,24 @@ var WorkflowRunStatus = class extends Schema.Class("WorkflowRunStatus")({
26
26
  return this.status === "completed";
27
27
  }
28
28
  };
29
+ /**
30
+ * The run a `workflow_dispatch` created, as GitHub reports it when asked.
31
+ *
32
+ * @remarks
33
+ * Returned by {@link WorkflowDispatchShape.dispatchWithRun}. `runId` is usable
34
+ * directly with {@link WorkflowDispatchShape.runStatus} and
35
+ * {@link WorkflowDispatchShape.cancelRun}.
36
+ *
37
+ * @public
38
+ */
39
+ var DispatchedRun = class extends Schema.Class("DispatchedRun")({
40
+ /** The created run's numeric id. */
41
+ runId: Schema.Int,
42
+ /** The run's API URL. */
43
+ runUrl: Schema.String,
44
+ /** The run's web URL. */
45
+ htmlUrl: Schema.String
46
+ }) {};
29
47
  const DEFAULT_INTERVAL = Duration.seconds(10);
30
48
  const DEFAULT_TIMEOUT = Duration.minutes(5);
31
49
  /**
@@ -60,7 +78,9 @@ var WorkflowDispatch = class WorkflowDispatch extends Context.Service()("@effect
60
78
  /** An in-memory double; unstubbed members die naming themselves. */
61
79
  static makeTest = (overrides = {}) => ({
62
80
  dispatch: overrides.dispatch ?? (() => unstubbed("dispatch")),
81
+ dispatchWithRun: overrides.dispatchWithRun ?? (() => unstubbed("dispatchWithRun")),
63
82
  runStatus: overrides.runStatus ?? (() => unstubbed("runStatus")),
83
+ cancelRun: overrides.cancelRun ?? (() => unstubbed("cancelRun")),
64
84
  list: overrides.list ?? Effect.sync(() => unstubbed("list")),
65
85
  dispatchAndWait: overrides.dispatchAndWait ?? (() => unstubbed("dispatchAndWait"))
66
86
  });
@@ -76,6 +96,14 @@ const statusOf = (raw) => WorkflowRunStatus.make({
76
96
  ...raw.conclusion != null ? { conclusion: raw.conclusion } : {},
77
97
  url: raw.html_url
78
98
  });
99
+ /** GitHub's `workflow-dispatch-response`; the id is `number | bigint` in the generated types. */
100
+ const DispatchResponse = Schema.Struct({
101
+ workflow_run_id: Schema.Union([Schema.Number, Schema.BigInt]),
102
+ run_url: Schema.String,
103
+ html_url: Schema.String
104
+ });
105
+ const decodeDispatchResponse = Schema.decodeUnknownEffect(DispatchResponse);
106
+ const decodeDispatchedRun = Schema.decodeUnknownEffect(DispatchedRun);
79
107
  const make = (client) => {
80
108
  const dispatch = Effect.fn("WorkflowDispatch.dispatch")(function* (workflow, ref, inputs) {
81
109
  const { owner, repo } = yield* Repo;
@@ -93,38 +121,78 @@ const make = (client) => {
93
121
  ...inputs !== void 0 ? { inputs } : {}
94
122
  });
95
123
  });
124
+ const dispatchWithRun = Effect.fn("WorkflowDispatch.dispatchWithRun")(function* (workflow, ref, inputs) {
125
+ const { owner, repo } = yield* Repo;
126
+ yield* Effect.annotateCurrentSpan({
127
+ owner,
128
+ repo,
129
+ workflow,
130
+ ref
131
+ });
132
+ const data = yield* client.request("POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches", {
133
+ owner,
134
+ repo,
135
+ workflow_id: workflow,
136
+ ref,
137
+ ...inputs !== void 0 ? { inputs } : {},
138
+ return_run_details: true
139
+ });
140
+ if (data === "" || data === null) return Option.none();
141
+ const run = yield* decodeDispatchResponse(data).pipe(Effect.flatMap((raw) => decodeDispatchedRun({
142
+ runId: numericId(raw.workflow_run_id),
143
+ runUrl: raw.run_url,
144
+ htmlUrl: raw.html_url
145
+ })), Effect.catchTag("SchemaError", (error) => Effect.fail(GitHubError.decode("WorkflowDispatch.dispatchWithRun", "GitHub returned unexpected run details", error))));
146
+ return Option.some(run);
147
+ });
148
+ const runStatus = Effect.fn("WorkflowDispatch.runStatus")(function* (runId) {
149
+ const { owner, repo } = yield* Repo;
150
+ yield* Effect.annotateCurrentSpan({
151
+ owner,
152
+ repo,
153
+ runId
154
+ });
155
+ const raw = yield* client.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
156
+ owner,
157
+ repo,
158
+ run_id: runId
159
+ });
160
+ return statusOf(raw);
161
+ });
162
+ const list = Effect.fn("WorkflowDispatch.list")(function* () {
163
+ const { owner, repo } = yield* Repo;
164
+ yield* Effect.annotateCurrentSpan({
165
+ owner,
166
+ repo
167
+ });
168
+ return (yield* client.paginate("GET /repos/{owner}/{repo}/actions/workflows", {
169
+ owner,
170
+ repo
171
+ })).map((workflow) => ({
172
+ id: workflow.id,
173
+ name: workflow.name,
174
+ path: workflow.path,
175
+ state: workflow.state
176
+ }));
177
+ });
96
178
  return {
97
179
  dispatch,
98
- runStatus: Effect.fn("WorkflowDispatch.runStatus")(function* (runId) {
180
+ dispatchWithRun,
181
+ runStatus,
182
+ cancelRun: Effect.fn("WorkflowDispatch.cancelRun")(function* (runId) {
99
183
  const { owner, repo } = yield* Repo;
100
184
  yield* Effect.annotateCurrentSpan({
101
185
  owner,
102
186
  repo,
103
187
  runId
104
188
  });
105
- const raw = yield* client.request("GET /repos/{owner}/{repo}/actions/runs/{run_id}", {
189
+ return yield* client.request("POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel", {
106
190
  owner,
107
191
  repo,
108
192
  run_id: runId
109
- });
110
- return statusOf(raw);
193
+ }).pipe(Effect.as("cancelled"), Effect.catchIf((error) => error.status === 409, () => Effect.succeed("alreadyCompleted")));
111
194
  }),
112
- list: Effect.fn("WorkflowDispatch.list")(function* () {
113
- const { owner, repo } = yield* Repo;
114
- yield* Effect.annotateCurrentSpan({
115
- owner,
116
- repo
117
- });
118
- return (yield* client.paginate("GET /repos/{owner}/{repo}/actions/workflows", {
119
- owner,
120
- repo
121
- })).map((workflow) => ({
122
- id: workflow.id,
123
- name: workflow.name,
124
- path: workflow.path,
125
- state: workflow.state
126
- }));
127
- })(),
195
+ list: list(),
128
196
  dispatchAndWait: Effect.fn("WorkflowDispatch.dispatchAndWait")(function* (workflow, ref, options) {
129
197
  const { owner, repo } = yield* Repo;
130
198
  const interval = options?.poll?.interval ?? DEFAULT_INTERVAL;
@@ -138,8 +206,8 @@ const make = (client) => {
138
206
  attempts
139
207
  });
140
208
  const dispatchedAt = new Date(yield* Clock.currentTimeMillis).toISOString();
141
- yield* dispatch(workflow, ref, options?.inputs);
142
- const findRun = Effect.gen(function* () {
209
+ const dispatched = yield* dispatchWithRun(workflow, ref, options?.inputs);
210
+ const findRun = Option.isSome(dispatched) ? Effect.map(runStatus(dispatched.value.runId), Option.some) : Effect.gen(function* () {
143
211
  const match = (yield* client.paginate("GET /repos/{owner}/{repo}/actions/runs", {
144
212
  owner,
145
213
  repo,
@@ -163,4 +231,4 @@ const make = (client) => {
163
231
  };
164
232
 
165
233
  //#endregion
166
- export { WorkflowDispatch, WorkflowRunStatus };
234
+ export { DispatchedRun, WorkflowDispatch, WorkflowRunStatus };