@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 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 { CONFLICT, isOk, stringField, twirpCall } from "./internal/twirp.js";
5
- import { Context, Effect, FileSystem, Layer, Option, Path, Schema, Stream } from "effect";
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) => createHash("sha256").update([
79
+ const versionOf = (paths) => sha256Hex([
78
80
  ...paths,
79
81
  "gzip",
80
82
  "1.0"
81
- ].join("|")).digest("hex");
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* Effect.map(env.getOptional("RUNNER_OS"), (found) => Option.match(found, {
138
- onNone: () => false,
139
- onSome: (os) => os.toLowerCase() === "windows"
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: `${name} is not set — the Actions cache is only reachable from a \`uses:\` step, never from \`run:\``
137
+ detail: misconfiguredDetail(name, "Actions cache")
144
138
  })));
145
- const call = (method, body, key) => Effect.gen(function* () {
146
- const { baseUrl, token } = yield* backend;
147
- return yield* twirpCall({
148
- http,
149
- baseUrl,
150
- service: SERVICE,
151
- token,
152
- method,
153
- body
154
- }).pipe(Effect.mapError((failure) => fromTwirp(failure, key)));
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: the spawner's
166
- * `string` and `exitCode` convenience members each spawn independently
167
- * (core derives both from `spawn`), so calling them back-to-back
168
- * double-executed every archive operation — and on a failure reported the
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.scoped(Effect.gen(function* () {
174
- const command = ChildProcess.make("tar", [...args]);
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((cause) => new ActionCacheError({
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((cause) => new ActionCacheError({
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((cause) => new ActionCacheError({
315
- reason: "archiveFailed",
316
- key: primary,
317
- cause
318
- })));
319
- const created = yield* call("CreateCacheEntry", {
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 version = versionOf(paths);
347
- const found = yield* call("GetCacheEntryDownloadURL", {
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(stringField(found, "matchedKey") ?? primary);
317
+ return Option.some(hit.value.matchedKey ?? primary);
360
318
  })
361
319
  };
362
320
  });
363
- const unimplemented = (member) => {
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: () => Effect.sync(() => unimplemented("save")),
420
- restore: () => Effect.sync(() => unimplemented("restore")),
375
+ save: () => dies("save"),
376
+ restore: () => dies("restore"),
421
377
  ...overrides
422
378
  });
423
379
  /** {@link ActionCache.makeTest} behind `Layer.succeed`. */
@@ -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.gen(function* () {
133
- const [repository, repositoryOwner, ref, refName, sha, workflow, job] = yield* Effect.all([
134
- get("GITHUB_REPOSITORY"),
135
- get("GITHUB_REPOSITORY_OWNER"),
136
- get("GITHUB_REF"),
137
- get("GITHUB_REF_NAME"),
138
- get("GITHUB_SHA"),
139
- get("GITHUB_WORKFLOW"),
140
- get("GITHUB_JOB")
141
- ]);
142
- const runId = yield* Effect.flatMap(get("GITHUB_RUN_ID"), (raw) => numeric("GITHUB_RUN_ID", raw));
143
- const runAttempt = yield* Effect.flatMap(get("GITHUB_RUN_ATTEMPT"), (raw) => numeric("GITHUB_RUN_ATTEMPT", raw));
144
- const headRef = yield* lookup("GITHUB_HEAD_REF");
145
- const [eventName, actor, serverUrl, apiUrl, graphqlUrl, workspace] = yield* Effect.all([
146
- get("GITHUB_EVENT_NAME"),
147
- get("GITHUB_ACTOR"),
148
- get("GITHUB_SERVER_URL"),
149
- get("GITHUB_API_URL"),
150
- get("GITHUB_GRAPHQL_URL"),
151
- get("GITHUB_WORKSPACE")
152
- ]);
153
- return GitHubContext.make({
154
- repository,
155
- repositoryOwner,
156
- ref,
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
- * Resolve one runner-variable name: the verbatim entry wins; an
378
- * `INPUT_`-prefixed miss then consults the input-name entries through the
379
- * one spelling of the derivation (`inputVariable`).
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
- if (present(direct)) return direct;
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
- /** The inverse: annotation fields as a record `Effect.annotateLogs` accepts. */
58
- const annotationRecord = (properties) => ({
59
- ...properties.title === void 0 ? {} : { title: properties.title },
60
- ...properties.file === void 0 ? {} : { file: properties.file },
61
- ...properties.startLine === void 0 ? {} : { startLine: properties.startLine },
62
- ...properties.endLine === void 0 ? {} : { endLine: properties.endLine },
63
- ...properties.startColumn === void 0 ? {} : { startColumn: properties.startColumn },
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
- const annotations = options.fiber.getRef(References.CurrentLogAnnotations);
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 = Logger.make((options) => {
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
  /**