@effected/github-actions 0.12.0 → 0.13.1
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/ActionCache.js +62 -106
- package/ActionEnvironment.js +25 -56
- package/ActionInput.js +8 -7
- package/ActionLogger.js +13 -15
- package/ActionOutputs.js +144 -52
- package/ActionState.js +13 -25
- package/Artifact.js +27 -47
- package/BlobEnvelope.js +3 -9
- package/BlobStore.githubCache.js +33 -54
- package/BlobStore.js +14 -21
- package/CacheKey.js +57 -32
- package/CheckDocument.js +18 -11
- package/DetachedProcess.js +11 -15
- package/DryRun.js +9 -9
- package/GitHubMarkdown.js +3 -3
- package/GitHubToken.js +1 -1
- package/ManagedDocument.js +26 -29
- package/OidcTokenIssuer.js +18 -42
- package/PackageManagerInstaller.js +33 -29
- package/ToolInstaller.js +24 -34
- package/index.d.ts +118 -25
- package/index.js +2 -2
- package/internal/actionsResults.js +10 -20
- package/internal/cacheService.js +70 -0
- package/internal/digest.js +42 -0
- package/internal/fsProbe.js +14 -0
- package/internal/jwt.js +63 -0
- package/internal/runner.js +31 -0
- package/internal/runnerFile.js +45 -0
- package/internal/sigv4.js +3 -3
- package/internal/spawn.js +18 -0
- package/internal/twirp.js +18 -1
- package/internal/unstubbed.js +28 -0
- package/package.json +5 -4
package/ActionOutputs.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
2
|
import { WorkflowCommand } from "./WorkflowCommand.js";
|
|
3
|
+
import { heredocBlock, isUsableName } from "./internal/runnerFile.js";
|
|
4
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
5
|
import { Console, Context, Effect, FileSystem, Layer, Schema } from "effect";
|
|
4
6
|
|
|
5
7
|
//#region src/ActionOutputs.ts
|
|
@@ -83,26 +85,65 @@ var DetachedOutputError = class extends Schema.TaggedError()("DetachedOutputErro
|
|
|
83
85
|
return `Runner file "${this.file}" cannot be reached from a detached worker — it configures the parent job's later steps, and a worker has none and may outlive the job. Publish this from the parent process instead.`;
|
|
84
86
|
}
|
|
85
87
|
};
|
|
86
|
-
/** The base delimiter for a heredoc block. */
|
|
87
|
-
const BASE_DELIMITER = "EFFECTED_EOF";
|
|
88
88
|
/**
|
|
89
|
-
*
|
|
89
|
+
* The runner file each member appends to — spelled once, so the real layer,
|
|
90
|
+
* the detached layer and the recording double cannot disagree about which
|
|
91
|
+
* file a member's failure names.
|
|
92
|
+
*/
|
|
93
|
+
const RUNNER_FILE = {
|
|
94
|
+
set: "GITHUB_OUTPUT",
|
|
95
|
+
setJson: "GITHUB_OUTPUT",
|
|
96
|
+
exportVariable: "GITHUB_ENV",
|
|
97
|
+
summary: "GITHUB_STEP_SUMMARY",
|
|
98
|
+
addPath: "GITHUB_PATH"
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Run `effect` only if `name` can head a heredoc block; otherwise fail typed,
|
|
102
|
+
* naming the file. The one name check every construction of the service
|
|
103
|
+
* applies before a named write.
|
|
104
|
+
*/
|
|
105
|
+
const withUsableName = (file, name, effect) => isUsableName(name) ? effect : Effect.fail(new InvalidOutputNameError({
|
|
106
|
+
file,
|
|
107
|
+
name
|
|
108
|
+
}));
|
|
109
|
+
/**
|
|
110
|
+
* The JSON text `setJson` publishes for `value`: encoded through `schema`,
|
|
111
|
+
* then stringified. The one step of `setJson` that can fail, shared by the
|
|
112
|
+
* real layer and both test doubles so that none of them can skip it.
|
|
113
|
+
*/
|
|
114
|
+
const encodeJson = (name, value, schema) => Schema.encodeUnknownEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError((cause) => new OutputEncodeError({
|
|
115
|
+
name,
|
|
116
|
+
cause
|
|
117
|
+
})));
|
|
118
|
+
/**
|
|
119
|
+
* One call an {@link ActionOutputs.recording} double observed.
|
|
90
120
|
*
|
|
91
121
|
* @remarks
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
122
|
+
* `value` is always the string the runner would have seen: for `setJson` that
|
|
123
|
+
* is the **encoded** JSON text, after the same schema encode the real layer
|
|
124
|
+
* performs, so a test asserting on it reads exactly what a later step's
|
|
125
|
+
* `steps.<id>.outputs.<name>` expression would. `name` is present for the
|
|
126
|
+
* members that take one (`set`, `setJson`, `exportVariable`) and absent
|
|
127
|
+
* otherwise.
|
|
128
|
+
*
|
|
129
|
+
* @public
|
|
98
130
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
131
|
+
var RecordedOutput = class extends Schema.Class("RecordedOutput")({
|
|
132
|
+
/** Which {@link ActionOutputsShape} member was called. */
|
|
133
|
+
member: Schema.Literals([
|
|
134
|
+
"set",
|
|
135
|
+
"setJson",
|
|
136
|
+
"summary",
|
|
137
|
+
"exportVariable",
|
|
138
|
+
"addPath",
|
|
139
|
+
"setFailed",
|
|
140
|
+
"setSecret"
|
|
141
|
+
]),
|
|
142
|
+
/** The output or variable name, for the members that take one. */
|
|
143
|
+
name: Schema.optionalKey(Schema.String),
|
|
144
|
+
/** What was published — value, message, path, content or secret — as the runner would have read it. */
|
|
145
|
+
value: Schema.String
|
|
146
|
+
}) {};
|
|
106
147
|
const make = Effect.gen(function* () {
|
|
107
148
|
const env = yield* ActionEnvironment;
|
|
108
149
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -113,31 +154,19 @@ const make = Effect.gen(function* () {
|
|
|
113
154
|
cause
|
|
114
155
|
})));
|
|
115
156
|
});
|
|
116
|
-
const appendBlock = (file, name, value) =>
|
|
117
|
-
|
|
118
|
-
file,
|
|
119
|
-
name
|
|
120
|
-
}));
|
|
121
|
-
const delimiter = delimiterFor(value);
|
|
122
|
-
return append(file, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
|
|
123
|
-
};
|
|
124
|
-
const set = (name, value) => appendBlock("GITHUB_OUTPUT", name, value);
|
|
157
|
+
const appendBlock = (file, name, value) => withUsableName(file, name, append(file, heredocBlock(name, value)));
|
|
158
|
+
const set = (name, value) => appendBlock(RUNNER_FILE.set, name, value);
|
|
125
159
|
return {
|
|
126
160
|
set,
|
|
127
|
-
setJson: (name, value, schema) =>
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
summary: (content) => append("GITHUB_STEP_SUMMARY", content),
|
|
132
|
-
exportVariable: (name, value) => appendBlock("GITHUB_ENV", name, value),
|
|
133
|
-
addPath: (path) => append("GITHUB_PATH", `${path}\n`),
|
|
161
|
+
setJson: (name, value, schema) => encodeJson(name, value, schema).pipe(Effect.flatMap((json) => set(name, json))),
|
|
162
|
+
summary: (content) => append(RUNNER_FILE.summary, content),
|
|
163
|
+
exportVariable: (name, value) => appendBlock(RUNNER_FILE.exportVariable, name, value),
|
|
164
|
+
addPath: (path) => append(RUNNER_FILE.addPath, `${path}\n`),
|
|
134
165
|
setFailed: (message) => Console.log(WorkflowCommand.error(message)),
|
|
135
166
|
setSecret: (value) => Console.log(WorkflowCommand.addMask(value))
|
|
136
167
|
};
|
|
137
168
|
});
|
|
138
|
-
const
|
|
139
|
-
throw new Error(`ActionOutputs.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
140
|
-
};
|
|
169
|
+
const dies = unstubbed("ActionOutputs.makeTest");
|
|
141
170
|
/**
|
|
142
171
|
* Everything an action publishes: step outputs, exported variables, `PATH`
|
|
143
172
|
* additions, the job summary, log masking and failure annotations.
|
|
@@ -195,36 +224,99 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
|
|
|
195
224
|
*/
|
|
196
225
|
static layerDetached = Layer.succeed(this, {
|
|
197
226
|
set: (name) => Effect.fail(new DetachedOutputError({
|
|
198
|
-
file:
|
|
227
|
+
file: RUNNER_FILE.set,
|
|
199
228
|
name
|
|
200
229
|
})),
|
|
201
230
|
setJson: (name) => Effect.fail(new DetachedOutputError({
|
|
202
|
-
file:
|
|
231
|
+
file: RUNNER_FILE.setJson,
|
|
203
232
|
name
|
|
204
233
|
})),
|
|
205
|
-
summary: () => Effect.fail(new DetachedOutputError({ file:
|
|
234
|
+
summary: () => Effect.fail(new DetachedOutputError({ file: RUNNER_FILE.summary })),
|
|
206
235
|
exportVariable: (name) => Effect.fail(new DetachedOutputError({
|
|
207
|
-
file:
|
|
236
|
+
file: RUNNER_FILE.exportVariable,
|
|
208
237
|
name
|
|
209
238
|
})),
|
|
210
|
-
addPath: () => Effect.fail(new DetachedOutputError({ file:
|
|
239
|
+
addPath: () => Effect.fail(new DetachedOutputError({ file: RUNNER_FILE.addPath })),
|
|
211
240
|
setFailed: (message) => Console.error(message),
|
|
212
241
|
setSecret: () => Effect.void
|
|
213
242
|
});
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
243
|
+
/**
|
|
244
|
+
* A test double. Unstubbed members die rather than silently succeeding.
|
|
245
|
+
*
|
|
246
|
+
* @remarks
|
|
247
|
+
* **`setJson` always encodes first.** The value is encoded through its
|
|
248
|
+
* schema exactly as the real layer does — failing typed with
|
|
249
|
+
* {@link OutputEncodeError} — and only then is a supplied `setJson`
|
|
250
|
+
* override called, with the original `(name, value, schema)`. An override
|
|
251
|
+
* that accepts `schema` and ignores it therefore **cannot disable
|
|
252
|
+
* output-schema checking**: a value/schema drift is a typed failure under
|
|
253
|
+
* this double whether or not the override looks at the schema. Without an
|
|
254
|
+
* override, a valid value still dies unimplemented as every other unstubbed
|
|
255
|
+
* member does. Most tests want neither: reach for
|
|
256
|
+
* {@link ActionOutputs.recording} instead.
|
|
257
|
+
*/
|
|
258
|
+
static makeTest = (overrides = {}) => {
|
|
259
|
+
const setJson = overrides.setJson ?? (() => dies("setJson"));
|
|
260
|
+
return {
|
|
261
|
+
set: () => dies("set"),
|
|
262
|
+
summary: () => dies("summary"),
|
|
263
|
+
exportVariable: () => dies("exportVariable"),
|
|
264
|
+
addPath: () => dies("addPath"),
|
|
265
|
+
setFailed: () => dies("setFailed"),
|
|
266
|
+
setSecret: () => dies("setSecret"),
|
|
267
|
+
...overrides,
|
|
268
|
+
setJson: (name, value, schema) => encodeJson(name, value, schema).pipe(Effect.flatMap(() => setJson(name, value, schema)))
|
|
269
|
+
};
|
|
270
|
+
};
|
|
225
271
|
/** {@link ActionOutputs.makeTest} behind `Layer.succeed`. */
|
|
226
272
|
static layerTest = (overrides = {}) => Layer.succeed(ActionOutputs, ActionOutputs.makeTest(overrides));
|
|
273
|
+
/**
|
|
274
|
+
* A recording double: every member appends a {@link RecordedOutput} to a
|
|
275
|
+
* journal the test reads back, in call order.
|
|
276
|
+
*
|
|
277
|
+
* @remarks
|
|
278
|
+
* This is the double most tests of an action want — "what did it publish?"
|
|
279
|
+
* — shipped so that nobody hand-writes a `setJson` override that drops the
|
|
280
|
+
* schema encode (the trap `makeTest` now closes too). `setJson` encodes
|
|
281
|
+
* through its schema exactly as the real layer does, failing typed with
|
|
282
|
+
* {@link OutputEncodeError} and recording nothing on a drift, and records
|
|
283
|
+
* the **encoded JSON text** — what the runner would have read — not the
|
|
284
|
+
* decoded value. `set`, `exportVariable` and `setJson` refuse an unusable
|
|
285
|
+
* name with {@link InvalidOutputNameError}, as the real layer does, rather
|
|
286
|
+
* than recording it.
|
|
287
|
+
*
|
|
288
|
+
* `setSecret` records the secret's **plaintext** under
|
|
289
|
+
* `member: "setSecret"`. That is deliberate for a recording double: the
|
|
290
|
+
* journal never leaves the test, and a test asserting that a value *was*
|
|
291
|
+
* masked needs to see which one. Do not hand this journal to anything that
|
|
292
|
+
* logs.
|
|
293
|
+
*
|
|
294
|
+
* Each call returns a fresh, independent journal — there is no state shared
|
|
295
|
+
* between two `recording()` calls.
|
|
296
|
+
*/
|
|
297
|
+
static recording = () => {
|
|
298
|
+
const entries = [];
|
|
299
|
+
const record = (member, value, name) => Effect.sync(() => {
|
|
300
|
+
entries.push(RecordedOutput.make({
|
|
301
|
+
member,
|
|
302
|
+
value,
|
|
303
|
+
...name === void 0 ? {} : { name }
|
|
304
|
+
}));
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
layer: Layer.succeed(ActionOutputs, {
|
|
308
|
+
set: (name, value) => withUsableName(RUNNER_FILE.set, name, record("set", value, name)),
|
|
309
|
+
setJson: (name, value, schema) => withUsableName(RUNNER_FILE.setJson, name, encodeJson(name, value, schema).pipe(Effect.flatMap((json) => record("setJson", json, name)))),
|
|
310
|
+
summary: (content) => record("summary", content),
|
|
311
|
+
exportVariable: (name, value) => withUsableName(RUNNER_FILE.exportVariable, name, record("exportVariable", value, name)),
|
|
312
|
+
addPath: (path) => record("addPath", path),
|
|
313
|
+
setFailed: (message) => record("setFailed", message),
|
|
314
|
+
setSecret: (value) => record("setSecret", value)
|
|
315
|
+
}),
|
|
316
|
+
entries: () => entries.slice()
|
|
317
|
+
};
|
|
318
|
+
};
|
|
227
319
|
};
|
|
228
320
|
|
|
229
321
|
//#endregion
|
|
230
|
-
export { ActionOutputs, DetachedOutputError, InvalidOutputNameError, OutputEncodeError, RunnerFileUnavailableError, RunnerFileWriteError };
|
|
322
|
+
export { ActionOutputs, DetachedOutputError, InvalidOutputNameError, OutputEncodeError, RecordedOutput, RunnerFileUnavailableError, RunnerFileWriteError };
|
package/ActionState.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { heredocBlock, isUsableName } from "./internal/runnerFile.js";
|
|
3
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
2
4
|
import { ActionOutputs } from "./ActionOutputs.js";
|
|
3
5
|
import { Context, Effect, FileSystem, Layer, Option, Schema } from "effect";
|
|
4
6
|
|
|
@@ -43,31 +45,19 @@ const make = Effect.gen(function* () {
|
|
|
43
45
|
const fs = yield* FileSystem.FileSystem;
|
|
44
46
|
const outputs = yield* ActionOutputs;
|
|
45
47
|
const write = (key, serialized) => Effect.gen(function* () {
|
|
46
|
-
const
|
|
48
|
+
const writeFailed = (cause) => new ActionStateError({
|
|
47
49
|
reason: "writeFailed",
|
|
48
50
|
key,
|
|
49
51
|
cause
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
yield* fs.writeFileString(path,
|
|
54
|
-
reason: "writeFailed",
|
|
55
|
-
key,
|
|
56
|
-
cause
|
|
57
|
-
})));
|
|
52
|
+
});
|
|
53
|
+
if (!isUsableName(key)) return yield* Effect.fail(writeFailed(/* @__PURE__ */ new Error(`"${key}" cannot name a GITHUB_STATE entry`)));
|
|
54
|
+
const path = yield* env.get("GITHUB_STATE").pipe(Effect.mapError(writeFailed));
|
|
55
|
+
yield* fs.writeFileString(path, heredocBlock(key, serialized), { flag: "a" }).pipe(Effect.mapError(writeFailed));
|
|
58
56
|
});
|
|
59
57
|
const read = (key, schema) => Effect.gen(function* () {
|
|
60
58
|
const raw = yield* env.getOptional(stateVariable(key));
|
|
61
59
|
if (Option.isNone(raw)) return Option.none();
|
|
62
|
-
const
|
|
63
|
-
try: () => JSON.parse(raw.value),
|
|
64
|
-
catch: (cause) => new ActionStateError({
|
|
65
|
-
reason: "malformed",
|
|
66
|
-
key,
|
|
67
|
-
cause
|
|
68
|
-
})
|
|
69
|
-
});
|
|
70
|
-
const decoded = yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(Effect.mapError((cause) => new ActionStateError({
|
|
60
|
+
const decoded = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(schema))(raw.value).pipe(Effect.mapError((cause) => new ActionStateError({
|
|
71
61
|
reason: "malformed",
|
|
72
62
|
key,
|
|
73
63
|
cause
|
|
@@ -111,9 +101,7 @@ const make = Effect.gen(function* () {
|
|
|
111
101
|
saveSecret: (key, secret) => Effect.flatMap(outputs.setSecret(secret), () => write(key, JSON.stringify(secret)))
|
|
112
102
|
};
|
|
113
103
|
});
|
|
114
|
-
const
|
|
115
|
-
throw new Error(`ActionState.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
116
|
-
};
|
|
104
|
+
const dies = unstubbed("ActionState.makeTest");
|
|
117
105
|
/**
|
|
118
106
|
* State that survives the `pre` → `main` → `post` phase boundary.
|
|
119
107
|
*
|
|
@@ -130,10 +118,10 @@ var ActionState = class ActionState extends Context.Service()("@effected/github-
|
|
|
130
118
|
static layer = Layer.effect(this, make);
|
|
131
119
|
/** A test double. Unstubbed members die rather than answering wrongly. */
|
|
132
120
|
static makeTest = (overrides = {}) => ({
|
|
133
|
-
save: () =>
|
|
134
|
-
get: () =>
|
|
135
|
-
getOptional: () =>
|
|
136
|
-
saveSecret: () =>
|
|
121
|
+
save: () => dies("save"),
|
|
122
|
+
get: () => dies("get"),
|
|
123
|
+
getOptional: () => dies("getOptional"),
|
|
124
|
+
saveSecret: () => dies("saveSecret"),
|
|
137
125
|
...overrides
|
|
138
126
|
});
|
|
139
127
|
/** {@link ActionState.makeTest} behind `Layer.succeed`. */
|
package/Artifact.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
2
3
|
import { BlobTransferError } from "./BlobTransfer.js";
|
|
3
|
-
import { resultsBackend } from "./internal/actionsResults.js";
|
|
4
|
-
import { CONFLICT, field, isOk, stringField, twirpCall } from "./internal/twirp.js";
|
|
5
|
-
import {
|
|
4
|
+
import { misconfiguredDetail, resultsBackend } from "./internal/actionsResults.js";
|
|
5
|
+
import { CONFLICT, field, isOk, stringField, twirpCall, twirpFailureFields } from "./internal/twirp.js";
|
|
6
|
+
import { digestFileHex } from "./internal/digest.js";
|
|
7
|
+
import { isWindowsRunner } from "./internal/runner.js";
|
|
8
|
+
import { spawnOnce } from "./internal/spawn.js";
|
|
9
|
+
import { Context, Effect, FileSystem, Layer, Option, Path, Result, Schema } from "effect";
|
|
6
10
|
import { HttpClient } from "effect/unstable/http";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
11
|
import { BlobClient, BlockBlobClient } from "@azure/storage-blob";
|
|
9
12
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
10
13
|
|
|
@@ -101,17 +104,6 @@ const azure = {
|
|
|
101
104
|
})
|
|
102
105
|
}).pipe(Effect.asVoid)
|
|
103
106
|
};
|
|
104
|
-
const fromTwirp = (failure, artifact) => failure.kind === "status" ? new ArtifactError({
|
|
105
|
-
reason: "refused",
|
|
106
|
-
artifact,
|
|
107
|
-
detail: failure.method,
|
|
108
|
-
...failure.status === void 0 ? {} : { status: failure.status }
|
|
109
|
-
}) : new ArtifactError({
|
|
110
|
-
reason: "unreachable",
|
|
111
|
-
artifact,
|
|
112
|
-
detail: `${failure.method} did not answer with a Twirp body`,
|
|
113
|
-
...failure.cause === void 0 ? {} : { cause: failure.cause }
|
|
114
|
-
});
|
|
115
107
|
/** One row of a `ListArtifacts` answer, read under either field spelling. */
|
|
116
108
|
const toItem = (row) => {
|
|
117
109
|
const createdAt = stringField(row, "createdAt");
|
|
@@ -128,14 +120,11 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
128
120
|
const path = yield* Path.Path;
|
|
129
121
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
130
122
|
const env = yield* ActionEnvironment;
|
|
131
|
-
const windows = yield*
|
|
132
|
-
onNone: () => false,
|
|
133
|
-
onSome: (os) => os.toLowerCase() === "windows"
|
|
134
|
-
}));
|
|
123
|
+
const windows = yield* isWindowsRunner(env);
|
|
135
124
|
const backend = (artifact) => resultsBackend(env).pipe(Effect.mapError((name) => new ArtifactError({
|
|
136
125
|
reason: "misconfigured",
|
|
137
126
|
artifact,
|
|
138
|
-
detail:
|
|
127
|
+
detail: misconfiguredDetail(name, "artifact service")
|
|
139
128
|
})), Effect.flatMap((resolved) => Result.isFailure(resolved.backendIds) ? Effect.fail(new ArtifactError({
|
|
140
129
|
reason: "misconfigured",
|
|
141
130
|
artifact,
|
|
@@ -157,25 +146,24 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
157
146
|
...ids,
|
|
158
147
|
...body(ids)
|
|
159
148
|
}
|
|
160
|
-
}).pipe(Effect.mapError((failure) =>
|
|
149
|
+
}).pipe(Effect.mapError((failure) => new ArtifactError({
|
|
150
|
+
...twirpFailureFields(failure),
|
|
151
|
+
artifact
|
|
152
|
+
})));
|
|
161
153
|
});
|
|
162
154
|
const listAll = (artifact) => Effect.map(call("ListArtifacts", () => ({}), artifact), (answer) => {
|
|
163
155
|
if (answer === CONFLICT) return [];
|
|
164
156
|
const rows = field(answer, "artifacts");
|
|
165
157
|
return Array.isArray(rows) ? rows.map(toItem) : [];
|
|
166
158
|
});
|
|
167
|
-
/** Run an archiving command, keeping its stderr. */
|
|
159
|
+
/** Run an archiving command ONCE (`internal/spawn.ts`), keeping its stderr. */
|
|
168
160
|
const archive = (command, artifact) => Effect.gen(function* () {
|
|
169
|
-
const output = yield* spawner
|
|
161
|
+
const { output, code } = yield* spawnOnce(spawner, command).pipe(Effect.mapError((cause) => new ArtifactError({
|
|
170
162
|
reason: "archiveFailed",
|
|
171
163
|
artifact,
|
|
172
164
|
cause
|
|
173
165
|
})));
|
|
174
|
-
if (
|
|
175
|
-
reason: "archiveFailed",
|
|
176
|
-
artifact,
|
|
177
|
-
cause
|
|
178
|
-
})))) !== 0) return yield* Effect.fail(new ArtifactError({
|
|
166
|
+
if (code !== 0) return yield* Effect.fail(new ArtifactError({
|
|
179
167
|
reason: "archiveFailed",
|
|
180
168
|
artifact,
|
|
181
169
|
stderr: output.trim()
|
|
@@ -197,17 +185,11 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
197
185
|
* payload in this package with no upper bound on size, and a runner has
|
|
198
186
|
* roughly seven gigabytes of memory for everything.
|
|
199
187
|
*/
|
|
200
|
-
const digestOf = (file, artifact) => Effect.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
reason: "archiveFailed",
|
|
206
|
-
artifact,
|
|
207
|
-
cause
|
|
208
|
-
})));
|
|
209
|
-
return accumulator.digest("hex");
|
|
210
|
-
});
|
|
188
|
+
const digestOf = (file, artifact) => digestFileHex(fs, file, "sha256").pipe(Effect.mapError((cause) => new ArtifactError({
|
|
189
|
+
reason: "archiveFailed",
|
|
190
|
+
artifact,
|
|
191
|
+
cause
|
|
192
|
+
})));
|
|
211
193
|
const moved = (artifact) => Effect.mapError((cause) => new ArtifactError({
|
|
212
194
|
reason: "transferFailed",
|
|
213
195
|
artifact,
|
|
@@ -351,9 +333,7 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
351
333
|
})
|
|
352
334
|
};
|
|
353
335
|
});
|
|
354
|
-
const
|
|
355
|
-
throw new Error(`Artifact.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
356
|
-
};
|
|
336
|
+
const dies = unstubbed("Artifact.makeTest");
|
|
357
337
|
/**
|
|
358
338
|
* Upload, list, download and delete GitHub Actions artifacts.
|
|
359
339
|
*
|
|
@@ -408,11 +388,11 @@ var Artifact = class Artifact extends Context.Service()("@effected/github-action
|
|
|
408
388
|
static layerWith = (transfer) => Layer.effect(Artifact, make(transfer));
|
|
409
389
|
/** A test double. Unstubbed members die rather than reporting an empty run. */
|
|
410
390
|
static makeTest = (overrides = {}) => ({
|
|
411
|
-
upload: () =>
|
|
412
|
-
list: () =>
|
|
413
|
-
get: () =>
|
|
414
|
-
download: () =>
|
|
415
|
-
delete: () =>
|
|
391
|
+
upload: () => dies("upload"),
|
|
392
|
+
list: () => dies("list"),
|
|
393
|
+
get: () => dies("get"),
|
|
394
|
+
download: () => dies("download"),
|
|
395
|
+
delete: () => dies("delete"),
|
|
416
396
|
...overrides
|
|
417
397
|
});
|
|
418
398
|
/** {@link Artifact.makeTest} behind `Layer.succeed`. */
|
package/BlobEnvelope.js
CHANGED
|
@@ -111,9 +111,9 @@ var BlobEnvelope = class {
|
|
|
111
111
|
static version = VERSION;
|
|
112
112
|
/** Frame metadata and a body into a single blob. */
|
|
113
113
|
static encodeResult(metadata, body, schema) {
|
|
114
|
-
const encoded = Schema.encodeUnknownResult(schema)(metadata);
|
|
114
|
+
const encoded = Schema.encodeUnknownResult(Schema.fromJsonString(schema))(metadata);
|
|
115
115
|
if (Result.isFailure(encoded)) return Result.fail(new BlobMetadataEncodeError({ cause: encoded.failure }));
|
|
116
|
-
const metaBytes = new TextEncoder().encode(
|
|
116
|
+
const metaBytes = new TextEncoder().encode(encoded.success);
|
|
117
117
|
const out = new Uint8Array(HEADER_BYTES + metaBytes.length + body.length);
|
|
118
118
|
out.set(MAGIC, 0);
|
|
119
119
|
out[MAGIC.length] = VERSION;
|
|
@@ -131,13 +131,7 @@ var BlobEnvelope = class {
|
|
|
131
131
|
const metaLength = new DataView(bytes.buffer, bytes.byteOffset).getUint32(MAGIC.length + 1, false);
|
|
132
132
|
if (bytes.length < HEADER_BYTES + metaLength) return Result.fail(new TruncatedBlobEnvelopeError({}));
|
|
133
133
|
const metaText = new TextDecoder().decode(bytes.subarray(HEADER_BYTES, HEADER_BYTES + metaLength));
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
parsed = JSON.parse(metaText);
|
|
137
|
-
} catch (cause) {
|
|
138
|
-
return Result.fail(new BlobMetadataDecodeError({ cause }));
|
|
139
|
-
}
|
|
140
|
-
const decoded = Schema.decodeUnknownResult(schema)(parsed);
|
|
134
|
+
const decoded = Schema.decodeUnknownResult(Schema.fromJsonString(schema))(metaText);
|
|
141
135
|
if (Result.isFailure(decoded)) return Result.fail(new BlobMetadataDecodeError({ cause: decoded.failure }));
|
|
142
136
|
return Result.succeed({
|
|
143
137
|
metadata: decoded.success,
|
package/BlobStore.githubCache.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
2
|
import { BlobTransferError } from "./BlobTransfer.js";
|
|
3
|
-
import { resultsBackend } from "./internal/actionsResults.js";
|
|
4
|
-
import {
|
|
3
|
+
import { misconfiguredDetail, resultsBackend } from "./internal/actionsResults.js";
|
|
4
|
+
import { twirpCall, twirpFailureFields } from "./internal/twirp.js";
|
|
5
|
+
import { CACHE_SERVICE, finalizeUpload, lookupDownload, reserveUpload } from "./internal/cacheService.js";
|
|
6
|
+
import { sha256Hex } from "./internal/digest.js";
|
|
5
7
|
import { BlobEnvelope } from "./BlobEnvelope.js";
|
|
6
8
|
import { BlobStore, BlobStoreError } from "./BlobStore.js";
|
|
7
9
|
import { Effect, Layer, Option } from "effect";
|
|
8
10
|
import { HttpClient } from "effect/unstable/http";
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
11
|
import { BlobClient, BlockBlobClient } from "@azure/storage-blob";
|
|
11
12
|
|
|
12
13
|
//#region src/BlobStore.githubCache.ts
|
|
13
|
-
/** The Twirp service the Actions cache protocol lives under. */
|
|
14
|
-
const SERVICE = "github.actions.results.api.v1.CacheService";
|
|
15
14
|
/**
|
|
16
15
|
* The `version` every entry is filed under.
|
|
17
16
|
*
|
|
@@ -25,7 +24,7 @@ const SERVICE = "github.actions.results.api.v1.CacheService";
|
|
|
25
24
|
* inside the blob, so a framing change is a clean miss rather than a key change
|
|
26
25
|
* that strands every existing entry.
|
|
27
26
|
*/
|
|
28
|
-
const VERSION =
|
|
27
|
+
const VERSION = sha256Hex("blobstore|1.0");
|
|
29
28
|
/**
|
|
30
29
|
* The Azure half, ~15 lines and duplicated on purpose.
|
|
31
30
|
*
|
|
@@ -52,40 +51,37 @@ const azure = {
|
|
|
52
51
|
})
|
|
53
52
|
}).pipe(Effect.map((buffer) => new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)))
|
|
54
53
|
};
|
|
55
|
-
const fromTwirp = (failure, key) => failure.kind === "status" ? new BlobStoreError({
|
|
56
|
-
reason: "refused",
|
|
57
|
-
key,
|
|
58
|
-
...failure.status === void 0 ? {} : { status: failure.status }
|
|
59
|
-
}) : new BlobStoreError({
|
|
60
|
-
reason: "unreachable",
|
|
61
|
-
key,
|
|
62
|
-
detail: `${failure.method} answered with something that is not a Twirp body`,
|
|
63
|
-
...failure.cause === void 0 ? {} : { cause: failure.cause }
|
|
64
|
-
});
|
|
65
54
|
const make = (transfer) => Effect.gen(function* () {
|
|
66
55
|
const http = yield* HttpClient.HttpClient;
|
|
67
56
|
const env = yield* ActionEnvironment;
|
|
68
57
|
const backend = resultsBackend(env).pipe(Effect.mapError((name) => new BlobStoreError({
|
|
69
58
|
reason: "misconfigured",
|
|
70
|
-
detail:
|
|
59
|
+
detail: misconfiguredDetail(name, "Actions results backend")
|
|
71
60
|
})));
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
/** The cache service, for one key: every failure names it. */
|
|
62
|
+
const cacheService = (key) => ({
|
|
63
|
+
call: (method, body) => Effect.gen(function* () {
|
|
64
|
+
const { baseUrl, token } = yield* backend;
|
|
65
|
+
return yield* twirpCall({
|
|
66
|
+
http,
|
|
67
|
+
baseUrl,
|
|
68
|
+
service: CACHE_SERVICE,
|
|
69
|
+
token,
|
|
70
|
+
method,
|
|
71
|
+
body
|
|
72
|
+
}).pipe(Effect.mapError((failure) => new BlobStoreError({
|
|
73
|
+
...twirpFailureFields(failure),
|
|
74
|
+
key
|
|
75
|
+
})));
|
|
76
|
+
}),
|
|
77
|
+
refused: (detail) => new BlobStoreError({
|
|
78
|
+
reason: "refused",
|
|
79
|
+
key,
|
|
80
|
+
detail
|
|
81
|
+
})
|
|
82
82
|
});
|
|
83
83
|
/** The signed download url for a key, or nothing — a miss is not a failure. */
|
|
84
|
-
const download = (key) => Effect.map(
|
|
85
|
-
key,
|
|
86
|
-
restore_keys: [],
|
|
87
|
-
version: VERSION
|
|
88
|
-
}, key), (answer) => answer === CONFLICT || !isOk(answer) ? Option.none() : Option.fromNullishOr(stringField(answer, "signedDownloadUrl")));
|
|
84
|
+
const download = (key) => Effect.map(lookupDownload(cacheService(key), key, [], VERSION), Option.map((hit) => hit.url));
|
|
89
85
|
const moved = (key) => Effect.mapError((cause) => new BlobStoreError({
|
|
90
86
|
reason: "unreachable",
|
|
91
87
|
key,
|
|
@@ -100,28 +96,11 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
100
96
|
}),
|
|
101
97
|
put: (key, blob, schema) => Effect.gen(function* () {
|
|
102
98
|
const framed = yield* Effect.fromResult(BlobEnvelope.encodeResult(blob.metadata, blob.body, schema));
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const url = stringField(created, "signedUploadUrl");
|
|
109
|
-
if (!isOk(created) || url === void 0) return yield* Effect.fail(new BlobStoreError({
|
|
110
|
-
reason: "refused",
|
|
111
|
-
key,
|
|
112
|
-
detail: "CreateCacheEntry returned no upload url"
|
|
113
|
-
}));
|
|
114
|
-
yield* transfer.uploadData(url, framed).pipe(moved(key));
|
|
115
|
-
const finalized = yield* call("FinalizeCacheEntryUpload", {
|
|
116
|
-
key,
|
|
117
|
-
version: VERSION,
|
|
118
|
-
size_bytes: String(framed.byteLength)
|
|
119
|
-
}, key);
|
|
120
|
-
if (finalized === CONFLICT || !isOk(finalized)) return yield* Effect.fail(new BlobStoreError({
|
|
121
|
-
reason: "refused",
|
|
122
|
-
key,
|
|
123
|
-
detail: "FinalizeCacheEntryUpload did not confirm the upload"
|
|
124
|
-
}));
|
|
99
|
+
const service = cacheService(key);
|
|
100
|
+
const url = yield* reserveUpload(service, key, VERSION);
|
|
101
|
+
if (Option.isNone(url)) return;
|
|
102
|
+
yield* transfer.uploadData(url.value, framed).pipe(moved(key));
|
|
103
|
+
yield* finalizeUpload(service, key, VERSION, framed.byteLength);
|
|
125
104
|
}),
|
|
126
105
|
has: (key) => Effect.map(download(key), Option.isSome)
|
|
127
106
|
};
|