@effected/github-actions 0.13.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 +43 -56
- 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 +37 -23
- 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 +3 -2
package/ActionCache.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
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 {
|
|
5
|
-
import {
|
|
4
|
+
import { misconfiguredDetail, resultsBackend } from "./internal/actionsResults.js";
|
|
5
|
+
import { twirpCall, twirpFailureFields } from "./internal/twirp.js";
|
|
6
|
+
import { CACHE_SERVICE, finalizeUpload, lookupDownload, reserveUpload } from "./internal/cacheService.js";
|
|
7
|
+
import { sha256Hex } from "./internal/digest.js";
|
|
8
|
+
import { isWindowsRunner } from "./internal/runner.js";
|
|
9
|
+
import { spawnOnce } from "./internal/spawn.js";
|
|
10
|
+
import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
6
11
|
import { HttpClient } from "effect/unstable/http";
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
12
|
import { BlobClient, BlockBlobClient } from "@azure/storage-blob";
|
|
9
13
|
import { GlobPattern, GlobSet } from "@effected/glob";
|
|
10
14
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
@@ -53,8 +57,6 @@ var ActionCacheError = class extends Schema.TaggedError()("ActionCacheError", {
|
|
|
53
57
|
}
|
|
54
58
|
}
|
|
55
59
|
};
|
|
56
|
-
/** The Twirp service the Actions cache protocol lives under. */
|
|
57
|
-
const SERVICE = "github.actions.results.api.v1.CacheService";
|
|
58
60
|
/**
|
|
59
61
|
* The entry version, byte-compatible with `actions/cache`.
|
|
60
62
|
*
|
|
@@ -74,11 +76,11 @@ const SERVICE = "github.actions.results.api.v1.CacheService";
|
|
|
74
76
|
* version the restore side cannot reproduce, and every save would be an entry
|
|
75
77
|
* no restore ever finds.
|
|
76
78
|
*/
|
|
77
|
-
const versionOf = (paths) =>
|
|
79
|
+
const versionOf = (paths) => sha256Hex([
|
|
78
80
|
...paths,
|
|
79
81
|
"gzip",
|
|
80
82
|
"1.0"
|
|
81
|
-
].join("|"))
|
|
83
|
+
].join("|"));
|
|
82
84
|
/**
|
|
83
85
|
* The Azure half, duplicated on purpose.
|
|
84
86
|
*
|
|
@@ -109,17 +111,6 @@ const azure = {
|
|
|
109
111
|
})
|
|
110
112
|
}).pipe(Effect.asVoid)
|
|
111
113
|
};
|
|
112
|
-
const fromTwirp = (failure, key) => failure.kind === "status" ? new ActionCacheError({
|
|
113
|
-
reason: "refused",
|
|
114
|
-
key,
|
|
115
|
-
detail: failure.method,
|
|
116
|
-
...failure.status === void 0 ? {} : { status: failure.status }
|
|
117
|
-
}) : new ActionCacheError({
|
|
118
|
-
reason: "unreachable",
|
|
119
|
-
key,
|
|
120
|
-
detail: `${failure.method} did not answer with a Twirp body`,
|
|
121
|
-
...failure.cause === void 0 ? {} : { cause: failure.cause }
|
|
122
|
-
});
|
|
123
114
|
/** The primary key and the ladder to fall back through. */
|
|
124
115
|
const ladder = (key, restoreKeys) => typeof key === "string" ? {
|
|
125
116
|
primary: key,
|
|
@@ -134,24 +125,38 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
134
125
|
const path = yield* Path.Path;
|
|
135
126
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
136
127
|
const env = yield* ActionEnvironment;
|
|
137
|
-
const windows = yield*
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
const windows = yield* isWindowsRunner(env);
|
|
129
|
+
/** A filesystem or subprocess failure on the way to or from the archive. */
|
|
130
|
+
const archiveFailed = (key) => (cause) => new ActionCacheError({
|
|
131
|
+
reason: "archiveFailed",
|
|
132
|
+
key,
|
|
133
|
+
cause
|
|
134
|
+
});
|
|
141
135
|
const backend = resultsBackend(env).pipe(Effect.mapError((name) => new ActionCacheError({
|
|
142
136
|
reason: "misconfigured",
|
|
143
|
-
detail:
|
|
137
|
+
detail: misconfiguredDetail(name, "Actions cache")
|
|
144
138
|
})));
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
139
|
+
/** The cache service, for one key: every failure names it. */
|
|
140
|
+
const cacheService = (key) => ({
|
|
141
|
+
call: (method, body) => Effect.gen(function* () {
|
|
142
|
+
const { baseUrl, token } = yield* backend;
|
|
143
|
+
return yield* twirpCall({
|
|
144
|
+
http,
|
|
145
|
+
baseUrl,
|
|
146
|
+
service: CACHE_SERVICE,
|
|
147
|
+
token,
|
|
148
|
+
method,
|
|
149
|
+
body
|
|
150
|
+
}).pipe(Effect.mapError((failure) => new ActionCacheError({
|
|
151
|
+
...twirpFailureFields(failure),
|
|
152
|
+
key
|
|
153
|
+
})));
|
|
154
|
+
}),
|
|
155
|
+
refused: (detail) => new ActionCacheError({
|
|
156
|
+
reason: "refused",
|
|
157
|
+
key,
|
|
158
|
+
detail
|
|
159
|
+
})
|
|
155
160
|
});
|
|
156
161
|
/**
|
|
157
162
|
* Run `tar` ONCE, keeping its stderr.
|
|
@@ -162,39 +167,24 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
162
167
|
* exits 1 to say so, which is a warning rather than an error. Exit 2 and
|
|
163
168
|
* above stays a failure on every platform.
|
|
164
169
|
*
|
|
165
|
-
* Output and exit code come from the SAME `spawn` handle
|
|
166
|
-
* `
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* FIRST run's output as the second run's complaint. `tar` being idempotent
|
|
170
|
-
* kept this latent here; the same shape failed loudly in
|
|
171
|
-
* `ToolInstaller.extractZip` on Windows.
|
|
170
|
+
* Output and exit code come from the SAME `spawn` handle
|
|
171
|
+
* (`internal/spawn.ts` — the spawner's convenience members each spawn
|
|
172
|
+
* independently, and the double run failed loudly in
|
|
173
|
+
* `ToolInstaller.extractZip` on Windows).
|
|
172
174
|
*/
|
|
173
|
-
const tar = (args, key, tolerateWarnings) => Effect.
|
|
174
|
-
const
|
|
175
|
-
const archiveError = (cause) => new ActionCacheError({
|
|
176
|
-
reason: "archiveFailed",
|
|
177
|
-
key,
|
|
178
|
-
cause
|
|
179
|
-
});
|
|
180
|
-
const handle = yield* spawner.spawn(command).pipe(Effect.mapError(archiveError));
|
|
181
|
-
const output = yield* Stream.mkString(Stream.decodeText(handle.all)).pipe(Effect.mapError(archiveError));
|
|
182
|
-
const code = yield* handle.exitCode.pipe(Effect.mapError(archiveError));
|
|
175
|
+
const tar = (args, key, tolerateWarnings) => Effect.gen(function* () {
|
|
176
|
+
const { output, code } = yield* spawnOnce(spawner, ChildProcess.make("tar", [...args])).pipe(Effect.mapError(archiveFailed(key)));
|
|
183
177
|
if (code !== 0 && !(tolerateWarnings && code === 1)) return yield* Effect.fail(new ActionCacheError({
|
|
184
178
|
reason: "archiveFailed",
|
|
185
179
|
key,
|
|
186
180
|
stderr: output.trim()
|
|
187
181
|
}));
|
|
188
|
-
})
|
|
182
|
+
});
|
|
189
183
|
/**
|
|
190
184
|
* A scratch archive path — plus the scratch directory itself, where the
|
|
191
185
|
* save side writes its tar manifest — removed on every exit path.
|
|
192
186
|
*/
|
|
193
|
-
const withArchive = (key, use) => Effect.acquireUseRelease(fs.makeTempDirectory({ prefix: "effected-cache-" }).pipe(Effect.mapError((
|
|
194
|
-
reason: "archiveFailed",
|
|
195
|
-
key,
|
|
196
|
-
cause
|
|
197
|
-
}))), (directory) => use(path.join(directory, "cache.tar.gz"), directory), (directory) => Effect.ignore(fs.remove(directory, {
|
|
187
|
+
const withArchive = (key, use) => Effect.acquireUseRelease(fs.makeTempDirectory({ prefix: "effected-cache-" }).pipe(Effect.mapError(archiveFailed(key))), (directory) => use(path.join(directory, "cache.tar.gz"), directory), (directory) => Effect.ignore(fs.remove(directory, {
|
|
198
188
|
recursive: true,
|
|
199
189
|
force: true
|
|
200
190
|
})));
|
|
@@ -291,78 +281,44 @@ const make = (transfer) => Effect.gen(function* () {
|
|
|
291
281
|
save: Effect.fn("ActionCache.save")(function* (paths, key) {
|
|
292
282
|
const { primary } = ladder(key, void 0);
|
|
293
283
|
yield* Effect.annotateCurrentSpan({ key: primary });
|
|
294
|
-
const version = versionOf(paths);
|
|
295
284
|
if (paths.length === 0) return yield* Effect.fail(new ActionCacheError({
|
|
296
285
|
reason: "archiveFailed",
|
|
297
286
|
key: primary,
|
|
298
287
|
detail: "no paths were given to cache"
|
|
299
288
|
}));
|
|
289
|
+
const version = versionOf(paths);
|
|
300
290
|
const resolved = yield* resolvePaths(paths, primary);
|
|
301
291
|
yield* withArchive(primary, (archive, scratch) => Effect.gen(function* () {
|
|
302
292
|
const manifest = path.join(scratch, "manifest.txt");
|
|
303
|
-
yield* fs.writeFileString(manifest, `${resolved.join("\n")}\n`).pipe(Effect.mapError((
|
|
304
|
-
reason: "archiveFailed",
|
|
305
|
-
key: primary,
|
|
306
|
-
cause
|
|
307
|
-
})));
|
|
293
|
+
yield* fs.writeFileString(manifest, `${resolved.join("\n")}\n`).pipe(Effect.mapError(archiveFailed(primary)));
|
|
308
294
|
yield* tar([
|
|
309
295
|
"czPf",
|
|
310
296
|
archive,
|
|
311
297
|
"-T",
|
|
312
298
|
manifest
|
|
313
299
|
], primary, false);
|
|
314
|
-
const size = yield* fs.stat(archive).pipe(Effect.mapError((
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
key: primary,
|
|
321
|
-
version
|
|
322
|
-
}, primary);
|
|
323
|
-
if (created === CONFLICT) return;
|
|
324
|
-
const url = stringField(created, "signedUploadUrl");
|
|
325
|
-
if (!isOk(created) || url === void 0) return yield* Effect.fail(new ActionCacheError({
|
|
326
|
-
reason: "refused",
|
|
327
|
-
key: primary,
|
|
328
|
-
detail: "CreateCacheEntry returned no upload url"
|
|
329
|
-
}));
|
|
330
|
-
yield* transfer.uploadFile(url, archive).pipe(moved(primary));
|
|
331
|
-
const finalized = yield* call("FinalizeCacheEntryUpload", {
|
|
332
|
-
key: primary,
|
|
333
|
-
version,
|
|
334
|
-
size_bytes: String(size.size)
|
|
335
|
-
}, primary);
|
|
336
|
-
if (finalized === CONFLICT || !isOk(finalized)) return yield* Effect.fail(new ActionCacheError({
|
|
337
|
-
reason: "refused",
|
|
338
|
-
key: primary,
|
|
339
|
-
detail: "FinalizeCacheEntryUpload did not confirm the upload"
|
|
340
|
-
}));
|
|
300
|
+
const size = yield* fs.stat(archive).pipe(Effect.mapError(archiveFailed(primary)));
|
|
301
|
+
const service = cacheService(primary);
|
|
302
|
+
const url = yield* reserveUpload(service, primary, version);
|
|
303
|
+
if (Option.isNone(url)) return;
|
|
304
|
+
yield* transfer.uploadFile(url.value, archive).pipe(moved(primary));
|
|
305
|
+
yield* finalizeUpload(service, primary, version, size.size);
|
|
341
306
|
}));
|
|
342
307
|
}),
|
|
343
308
|
restore: Effect.fn("ActionCache.restore")(function* (paths, key, restoreKeys) {
|
|
344
309
|
const { primary, fallbacks } = ladder(key, restoreKeys);
|
|
345
310
|
yield* Effect.annotateCurrentSpan({ key: primary });
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
key: primary,
|
|
349
|
-
restore_keys: [...fallbacks],
|
|
350
|
-
version
|
|
351
|
-
}, primary);
|
|
352
|
-
if (found === CONFLICT || !isOk(found)) return Option.none();
|
|
353
|
-
const url = stringField(found, "signedDownloadUrl");
|
|
354
|
-
if (url === void 0) return Option.none();
|
|
311
|
+
const hit = yield* lookupDownload(cacheService(primary), primary, fallbacks, versionOf(paths));
|
|
312
|
+
if (Option.isNone(hit)) return Option.none();
|
|
355
313
|
yield* withArchive(primary, (archive) => Effect.gen(function* () {
|
|
356
|
-
yield* transfer.downloadToFile(url, archive).pipe(moved(primary));
|
|
314
|
+
yield* transfer.downloadToFile(hit.value.url, archive).pipe(moved(primary));
|
|
357
315
|
yield* tar([windows ? "xzPkf" : "xzPf", archive], primary, windows);
|
|
358
316
|
}));
|
|
359
|
-
return Option.some(
|
|
317
|
+
return Option.some(hit.value.matchedKey ?? primary);
|
|
360
318
|
})
|
|
361
319
|
};
|
|
362
320
|
});
|
|
363
|
-
const
|
|
364
|
-
throw new Error(`ActionCache.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
365
|
-
};
|
|
321
|
+
const dies = unstubbed("ActionCache.makeTest");
|
|
366
322
|
/**
|
|
367
323
|
* The runner's own cache: archive a set of paths under a key, and get them back
|
|
368
324
|
* in a later job.
|
|
@@ -416,8 +372,8 @@ var ActionCache = class ActionCache extends Context.Service()("@effected/github-
|
|
|
416
372
|
static layerWith = (transfer) => Layer.effect(ActionCache, make(transfer));
|
|
417
373
|
/** A test double. Unstubbed members die rather than reporting a miss. */
|
|
418
374
|
static makeTest = (overrides = {}) => ({
|
|
419
|
-
save: () =>
|
|
420
|
-
restore: () =>
|
|
375
|
+
save: () => dies("save"),
|
|
376
|
+
restore: () => dies("restore"),
|
|
421
377
|
...overrides
|
|
422
378
|
});
|
|
423
379
|
/** {@link ActionCache.makeTest} behind `Layer.succeed`. */
|
package/ActionEnvironment.js
CHANGED
|
@@ -129,62 +129,31 @@ const make = (base) => Effect.gen(function* () {
|
|
|
129
129
|
get,
|
|
130
130
|
getOptional: lookup,
|
|
131
131
|
isDebug: Effect.map(lookup("RUNNER_DEBUG"), (value) => Option.isSome(value) && value.value === "1"),
|
|
132
|
-
github: Effect.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
refName,
|
|
158
|
-
headRef,
|
|
159
|
-
sha,
|
|
160
|
-
workflow,
|
|
161
|
-
job,
|
|
162
|
-
runId,
|
|
163
|
-
runAttempt,
|
|
164
|
-
eventName,
|
|
165
|
-
actor,
|
|
166
|
-
serverUrl,
|
|
167
|
-
apiUrl,
|
|
168
|
-
graphqlUrl,
|
|
169
|
-
workspace
|
|
170
|
-
});
|
|
171
|
-
}),
|
|
172
|
-
runner: Effect.gen(function* () {
|
|
173
|
-
const [os, arch, name, temp, toolCache] = yield* Effect.all([
|
|
174
|
-
get("RUNNER_OS"),
|
|
175
|
-
get("RUNNER_ARCH"),
|
|
176
|
-
get("RUNNER_NAME"),
|
|
177
|
-
get("RUNNER_TEMP"),
|
|
178
|
-
get("RUNNER_TOOL_CACHE")
|
|
179
|
-
]);
|
|
180
|
-
return RunnerContext.make({
|
|
181
|
-
os,
|
|
182
|
-
arch,
|
|
183
|
-
name,
|
|
184
|
-
temp,
|
|
185
|
-
toolCache
|
|
186
|
-
});
|
|
187
|
-
}),
|
|
132
|
+
github: Effect.map(Effect.all({
|
|
133
|
+
repository: get("GITHUB_REPOSITORY"),
|
|
134
|
+
repositoryOwner: get("GITHUB_REPOSITORY_OWNER"),
|
|
135
|
+
ref: get("GITHUB_REF"),
|
|
136
|
+
refName: get("GITHUB_REF_NAME"),
|
|
137
|
+
headRef: lookup("GITHUB_HEAD_REF"),
|
|
138
|
+
sha: get("GITHUB_SHA"),
|
|
139
|
+
workflow: get("GITHUB_WORKFLOW"),
|
|
140
|
+
job: get("GITHUB_JOB"),
|
|
141
|
+
runId: Effect.flatMap(get("GITHUB_RUN_ID"), (raw) => numeric("GITHUB_RUN_ID", raw)),
|
|
142
|
+
runAttempt: Effect.flatMap(get("GITHUB_RUN_ATTEMPT"), (raw) => numeric("GITHUB_RUN_ATTEMPT", raw)),
|
|
143
|
+
eventName: get("GITHUB_EVENT_NAME"),
|
|
144
|
+
actor: get("GITHUB_ACTOR"),
|
|
145
|
+
serverUrl: get("GITHUB_SERVER_URL"),
|
|
146
|
+
apiUrl: get("GITHUB_API_URL"),
|
|
147
|
+
graphqlUrl: get("GITHUB_GRAPHQL_URL"),
|
|
148
|
+
workspace: get("GITHUB_WORKSPACE")
|
|
149
|
+
}), (fields) => GitHubContext.make(fields)),
|
|
150
|
+
runner: Effect.map(Effect.all({
|
|
151
|
+
os: get("RUNNER_OS"),
|
|
152
|
+
arch: get("RUNNER_ARCH"),
|
|
153
|
+
name: get("RUNNER_NAME"),
|
|
154
|
+
temp: get("RUNNER_TEMP"),
|
|
155
|
+
toolCache: get("RUNNER_TOOL_CACHE")
|
|
156
|
+
}), (fields) => RunnerContext.make(fields)),
|
|
188
157
|
payload: Effect.gen(function* () {
|
|
189
158
|
const path = yield* get("GITHUB_EVENT_PATH");
|
|
190
159
|
const raw = yield* fs.readFileString(path).pipe(Effect.mapError((cause) => new ActionEnvironmentError({
|
package/ActionInput.js
CHANGED
|
@@ -374,16 +374,17 @@ var ActionInput = class ActionInput {
|
|
|
374
374
|
/** Unset, and the `""` the runner writes for an unsupplied input, are both absent. */
|
|
375
375
|
const present = (value) => value !== void 0 && value !== "";
|
|
376
376
|
/**
|
|
377
|
-
*
|
|
378
|
-
* `
|
|
379
|
-
*
|
|
377
|
+
* The input-name entries, keyed by the runner variable they would be
|
|
378
|
+
* (`inputVariable`, the one spelling of the derivation) — built once,
|
|
379
|
+
* so an `INPUT_`-prefixed miss is a map read rather than a scan of the
|
|
380
|
+
* whole environment per `Config` read.
|
|
380
381
|
*/
|
|
382
|
+
const byInputName = /* @__PURE__ */ new Map();
|
|
383
|
+
for (const [key, value] of Object.entries(env)) if (!key.startsWith("INPUT_") && present(value) && !byInputName.has(inputVariable(key))) byInputName.set(inputVariable(key), value);
|
|
384
|
+
/** Resolve one runner-variable name: the verbatim entry wins, then the input-name entry. */
|
|
381
385
|
const lookup = (name) => {
|
|
382
386
|
const direct = env[name];
|
|
383
|
-
|
|
384
|
-
if (name.startsWith("INPUT_")) {
|
|
385
|
-
for (const [key, value] of Object.entries(env)) if (!key.startsWith("INPUT_") && inputVariable(key) === name && present(value)) return value;
|
|
386
|
-
}
|
|
387
|
+
return present(direct) ? direct : byInputName.get(name);
|
|
387
388
|
};
|
|
388
389
|
return ConfigProvider.make((path) => {
|
|
389
390
|
const single = path.length === 1 && typeof path[0] === "string" ? path[0] : void 0;
|
package/ActionLogger.js
CHANGED
|
@@ -54,15 +54,13 @@ const readAnnotations = (record) => {
|
|
|
54
54
|
...endColumn === void 0 ? {} : { endColumn }
|
|
55
55
|
};
|
|
56
56
|
};
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
...properties.endColumn === void 0 ? {} : { endColumn: properties.endColumn }
|
|
65
|
-
});
|
|
57
|
+
/**
|
|
58
|
+
* The inverse: annotation fields as a record `Effect.annotateLogs` accepts.
|
|
59
|
+
* A plain copy: `exactOptionalPropertyTypes` keeps an absent field absent
|
|
60
|
+
* rather than present-and-`undefined`, and {@link readAnnotations} drops
|
|
61
|
+
* anything that is not a string or number on the way back out.
|
|
62
|
+
*/
|
|
63
|
+
const annotationRecord = (properties) => ({ ...properties });
|
|
66
64
|
/**
|
|
67
65
|
* Map one log entry onto the line the runner should see.
|
|
68
66
|
*
|
|
@@ -79,6 +77,10 @@ const renderEntry = (level, message, annotations) => {
|
|
|
79
77
|
}
|
|
80
78
|
return LogLevel.isGreaterThanOrEqualTo(level, "Info") ? text : WorkflowCommand.debug(text);
|
|
81
79
|
};
|
|
80
|
+
/** The line the runner should see for one log event, with the fiber's annotations. */
|
|
81
|
+
const renderLogger = Logger.make((options) => renderEntry(options.logLevel, options.message, options.fiber.getRef(References.CurrentLogAnnotations)));
|
|
82
|
+
/** {@link renderLogger} written through core `Console`. */
|
|
83
|
+
const commandLogger = Logger.withConsoleLog(renderLogger);
|
|
82
84
|
/**
|
|
83
85
|
* The buffer the current fiber is writing into, or `null` when not buffering.
|
|
84
86
|
*
|
|
@@ -118,8 +120,7 @@ const make = Effect.gen(function* () {
|
|
|
118
120
|
};
|
|
119
121
|
const buffering = Logger.make((options) => {
|
|
120
122
|
if (LogLevel.isGreaterThanOrEqualTo(options.logLevel, "Warn")) {
|
|
121
|
-
|
|
122
|
-
options.fiber.getRef(Console.Console).log(renderEntry(options.logLevel, options.message, annotations));
|
|
123
|
+
commandLogger.log(options);
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
126
|
state.entries.push(formatMessage(options.message));
|
|
@@ -170,10 +171,7 @@ var ActionLogger = class ActionLogger extends Context.Service()("@effected/githu
|
|
|
170
171
|
* It writes through core `Console`, which is what makes its output
|
|
171
172
|
* observable in a test without a runner.
|
|
172
173
|
*/
|
|
173
|
-
static logger =
|
|
174
|
-
const annotations = options.fiber.getRef(References.CurrentLogAnnotations);
|
|
175
|
-
options.fiber.getRef(Console.Console).log(renderEntry(options.logLevel, options.message, annotations));
|
|
176
|
-
});
|
|
174
|
+
static logger = commandLogger;
|
|
177
175
|
/** {@link ActionLogger.logger} installed as the only logger. */
|
|
178
176
|
static layerLogger = Logger.layer([ActionLogger.logger]);
|
|
179
177
|
/**
|
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,35 +85,36 @@ 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
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* GitHub's own toolkit uses a random UUID here and accepts the (tiny) chance
|
|
93
|
-
* of collision. Deriving the delimiter instead makes collision **impossible**
|
|
94
|
-
* rather than improbable, needs no randomness — so no `Crypto` in `R` and no
|
|
95
|
-
* unreproducible output — and is trivially testable. A value that contains a
|
|
96
|
-
* delimiter would terminate its block early and corrupt every entry after it,
|
|
97
|
-
* which is a value-controlled injection into the runner's own file.
|
|
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.
|
|
98
92
|
*/
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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"
|
|
103
99
|
};
|
|
104
|
-
/**
|
|
105
|
-
|
|
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
|
+
}));
|
|
106
109
|
/**
|
|
107
110
|
* The JSON text `setJson` publishes for `value`: encoded through `schema`,
|
|
108
111
|
* then stringified. The one step of `setJson` that can fail, shared by the
|
|
109
112
|
* real layer and both test doubles so that none of them can skip it.
|
|
110
113
|
*/
|
|
111
|
-
const encodeJson = (name, value, schema) => Schema.encodeUnknownEffect(schema)(value).pipe(Effect.mapError((cause) => new OutputEncodeError({
|
|
114
|
+
const encodeJson = (name, value, schema) => Schema.encodeUnknownEffect(Schema.fromJsonString(schema))(value).pipe(Effect.mapError((cause) => new OutputEncodeError({
|
|
112
115
|
name,
|
|
113
116
|
cause
|
|
114
|
-
}))
|
|
117
|
+
})));
|
|
115
118
|
/**
|
|
116
119
|
* One call an {@link ActionOutputs.recording} double observed.
|
|
117
120
|
*
|
|
@@ -151,28 +154,19 @@ const make = Effect.gen(function* () {
|
|
|
151
154
|
cause
|
|
152
155
|
})));
|
|
153
156
|
});
|
|
154
|
-
const appendBlock = (file, name, value) =>
|
|
155
|
-
|
|
156
|
-
file,
|
|
157
|
-
name
|
|
158
|
-
}));
|
|
159
|
-
const delimiter = delimiterFor(value);
|
|
160
|
-
return append(file, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
|
|
161
|
-
};
|
|
162
|
-
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);
|
|
163
159
|
return {
|
|
164
160
|
set,
|
|
165
161
|
setJson: (name, value, schema) => encodeJson(name, value, schema).pipe(Effect.flatMap((json) => set(name, json))),
|
|
166
|
-
summary: (content) => append(
|
|
167
|
-
exportVariable: (name, value) => appendBlock(
|
|
168
|
-
addPath: (path) => append(
|
|
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`),
|
|
169
165
|
setFailed: (message) => Console.log(WorkflowCommand.error(message)),
|
|
170
166
|
setSecret: (value) => Console.log(WorkflowCommand.addMask(value))
|
|
171
167
|
};
|
|
172
168
|
});
|
|
173
|
-
const
|
|
174
|
-
throw new Error(`ActionOutputs.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
175
|
-
};
|
|
169
|
+
const dies = unstubbed("ActionOutputs.makeTest");
|
|
176
170
|
/**
|
|
177
171
|
* Everything an action publishes: step outputs, exported variables, `PATH`
|
|
178
172
|
* additions, the job summary, log masking and failure annotations.
|
|
@@ -230,19 +224,19 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
|
|
|
230
224
|
*/
|
|
231
225
|
static layerDetached = Layer.succeed(this, {
|
|
232
226
|
set: (name) => Effect.fail(new DetachedOutputError({
|
|
233
|
-
file:
|
|
227
|
+
file: RUNNER_FILE.set,
|
|
234
228
|
name
|
|
235
229
|
})),
|
|
236
230
|
setJson: (name) => Effect.fail(new DetachedOutputError({
|
|
237
|
-
file:
|
|
231
|
+
file: RUNNER_FILE.setJson,
|
|
238
232
|
name
|
|
239
233
|
})),
|
|
240
|
-
summary: () => Effect.fail(new DetachedOutputError({ file:
|
|
234
|
+
summary: () => Effect.fail(new DetachedOutputError({ file: RUNNER_FILE.summary })),
|
|
241
235
|
exportVariable: (name) => Effect.fail(new DetachedOutputError({
|
|
242
|
-
file:
|
|
236
|
+
file: RUNNER_FILE.exportVariable,
|
|
243
237
|
name
|
|
244
238
|
})),
|
|
245
|
-
addPath: () => Effect.fail(new DetachedOutputError({ file:
|
|
239
|
+
addPath: () => Effect.fail(new DetachedOutputError({ file: RUNNER_FILE.addPath })),
|
|
246
240
|
setFailed: (message) => Console.error(message),
|
|
247
241
|
setSecret: () => Effect.void
|
|
248
242
|
});
|
|
@@ -262,14 +256,14 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
|
|
|
262
256
|
* {@link ActionOutputs.recording} instead.
|
|
263
257
|
*/
|
|
264
258
|
static makeTest = (overrides = {}) => {
|
|
265
|
-
const setJson = overrides.setJson ?? (() =>
|
|
259
|
+
const setJson = overrides.setJson ?? (() => dies("setJson"));
|
|
266
260
|
return {
|
|
267
|
-
set: () =>
|
|
268
|
-
summary: () =>
|
|
269
|
-
exportVariable: () =>
|
|
270
|
-
addPath: () =>
|
|
271
|
-
setFailed: () =>
|
|
272
|
-
setSecret: () =>
|
|
261
|
+
set: () => dies("set"),
|
|
262
|
+
summary: () => dies("summary"),
|
|
263
|
+
exportVariable: () => dies("exportVariable"),
|
|
264
|
+
addPath: () => dies("addPath"),
|
|
265
|
+
setFailed: () => dies("setFailed"),
|
|
266
|
+
setSecret: () => dies("setSecret"),
|
|
273
267
|
...overrides,
|
|
274
268
|
setJson: (name, value, schema) => encodeJson(name, value, schema).pipe(Effect.flatMap(() => setJson(name, value, schema)))
|
|
275
269
|
};
|
|
@@ -309,19 +303,12 @@ var ActionOutputs = class ActionOutputs extends Context.Service()("@effected/git
|
|
|
309
303
|
...name === void 0 ? {} : { name }
|
|
310
304
|
}));
|
|
311
305
|
});
|
|
312
|
-
const recordNamed = (member, file, name, value) => isUsableName(name) ? record(member, value, name) : Effect.fail(new InvalidOutputNameError({
|
|
313
|
-
file,
|
|
314
|
-
name
|
|
315
|
-
}));
|
|
316
306
|
return {
|
|
317
307
|
layer: Layer.succeed(ActionOutputs, {
|
|
318
|
-
set: (name, value) =>
|
|
319
|
-
setJson: (name, value, schema) =>
|
|
320
|
-
file: "GITHUB_OUTPUT",
|
|
321
|
-
name
|
|
322
|
-
})),
|
|
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)))),
|
|
323
310
|
summary: (content) => record("summary", content),
|
|
324
|
-
exportVariable: (name, value) =>
|
|
311
|
+
exportVariable: (name, value) => withUsableName(RUNNER_FILE.exportVariable, name, record("exportVariable", value, name)),
|
|
325
312
|
addPath: (path) => record("addPath", path),
|
|
326
313
|
setFailed: (message) => record("setFailed", message),
|
|
327
314
|
setSecret: (value) => record("setSecret", value)
|