@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.
@@ -0,0 +1,14 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/fsProbe.ts
4
+ /**
5
+ * The entry's type, or `undefined` when nothing readable is there. Absence
6
+ * and unreadability collapse together on purpose: every caller's next move
7
+ * (reinstall, write the shim, fail `layoutUnexpected`) is the same for both.
8
+ *
9
+ * @internal
10
+ */
11
+ const typeAt = (fs, path) => Effect.map(Effect.option(fs.stat(path)), (info) => info._tag === "Some" ? info.value.type : void 0);
12
+
13
+ //#endregion
14
+ export { typeAt };
@@ -0,0 +1,63 @@
1
+ import { Encoding, Result } from "effect";
2
+
3
+ //#region src/internal/jwt.ts
4
+ /**
5
+ * The JWT segment codec — read a payload, or frame an unsigned token — for the
6
+ * two places this package reads a runner-issued token's claims.
7
+ *
8
+ * @remarks
9
+ * **No signature verification, deliberately.** Every token read here was
10
+ * handed to this process by the runner that started it, and the claim read
11
+ * scopes a request (`actionsResults`) or is republished for a verifier
12
+ * elsewhere (`OidcTokenIssuer`); neither authorizes anything on the strength
13
+ * of the claim alone. Do not "fix" it here.
14
+ *
15
+ * Core `Encoding` does the base64url work, so no `Buffer` is involved: it is
16
+ * strict about the alphabet where Node's decoder is forgiving, which is why
17
+ * there is a test whose payload actually contains `-` and `_`.
18
+ *
19
+ * @internal
20
+ */
21
+ /**
22
+ * The decoded payload of `token` — the middle segment, base64url JSON —
23
+ * without any check on the signature.
24
+ *
25
+ * @internal
26
+ */
27
+ const payloadOf = (token) => {
28
+ const segments = token.split(".");
29
+ const payload = segments[1];
30
+ if (segments.length !== 3 || payload === void 0 || payload === "") return Result.fail({
31
+ kind: "segments",
32
+ detail: `expected three segments, got ${segments.length}`
33
+ });
34
+ const json = Encoding.decodeBase64UrlString(payload);
35
+ if (Result.isFailure(json)) return Result.fail({
36
+ kind: "payload",
37
+ detail: "the payload is not base64url JSON",
38
+ cause: json.failure
39
+ });
40
+ try {
41
+ return Result.succeed(JSON.parse(json.success));
42
+ } catch (cause) {
43
+ return Result.fail({
44
+ kind: "payload",
45
+ detail: "the payload is not base64url JSON",
46
+ cause
47
+ });
48
+ }
49
+ };
50
+ /**
51
+ * An **unsigned** JWT: `header.payload.unsigned`, each JSON segment
52
+ * base64url-encoded. For building test doubles and nothing else — the
53
+ * signature segment is a placeholder, so this token fails any verifier.
54
+ *
55
+ * @internal
56
+ */
57
+ const unsignedJwt = (header, payload) => {
58
+ const segment = (value) => Encoding.encodeBase64Url(JSON.stringify(value));
59
+ return `${segment(header)}.${segment(payload)}.unsigned`;
60
+ };
61
+
62
+ //#endregion
63
+ export { payloadOf, unsignedJwt };
@@ -0,0 +1,31 @@
1
+ import { Effect, Option } from "effect";
2
+
3
+ //#region src/internal/runner.ts
4
+ /**
5
+ * Whether `RUNNER_OS` says Windows. `false` when unset — off a runner, the
6
+ * POSIX shape is the honest default.
7
+ *
8
+ * @internal
9
+ */
10
+ const isWindowsRunner = (env) => Effect.map(env.getOptional("RUNNER_OS"), (found) => Option.match(found, {
11
+ onNone: () => false,
12
+ onSome: (os) => os.toLowerCase() === "windows"
13
+ }));
14
+ /**
15
+ * The tool-cache root: `RUNNER_TOOL_CACHE`, or `/tmp/runner-tool-cache` off a
16
+ * runner.
17
+ *
18
+ * @remarks
19
+ * Resolved at layer construction, never at import: the source package read
20
+ * the variable into a module-level constant, which fixed the root before any
21
+ * layer could say otherwise and made it impossible to point a test elsewhere.
22
+ * `PackageManagerInstaller` resolves the same root because the shims it
23
+ * writes into a staged entry must name the FINAL cache path; sharing the
24
+ * resolution is what keeps its post-swap equality check a formality.
25
+ *
26
+ * @internal
27
+ */
28
+ const toolCacheRoot = (env, path) => Effect.map(env.getOptional("RUNNER_TOOL_CACHE"), (found) => Option.getOrElse(found, () => path.join("/tmp", "runner-tool-cache")));
29
+
30
+ //#endregion
31
+ export { isWindowsRunner, toolCacheRoot };
@@ -0,0 +1,45 @@
1
+ //#region src/internal/runnerFile.ts
2
+ /**
3
+ * The runner-file heredoc protocol — `GITHUB_OUTPUT`, `GITHUB_ENV` and
4
+ * `GITHUB_STATE` all take `name<<DELIM\nvalue\nDELIM\n` — spelled once for
5
+ * `ActionOutputs` and `ActionState`.
6
+ *
7
+ * @remarks
8
+ * **Delimiters are derived, never random.** GitHub's own toolkit uses a
9
+ * random UUID here and accepts the (tiny) chance of collision. Deriving the
10
+ * delimiter instead makes collision **impossible** rather than improbable,
11
+ * needs no randomness — so no `Crypto` in `R` and no unreproducible output —
12
+ * and is trivially testable. A value that contains a delimiter would
13
+ * terminate its block early and corrupt every entry after it, which is a
14
+ * value-controlled injection into the runner's own file.
15
+ *
16
+ * @internal
17
+ */
18
+ /** The base delimiter for a heredoc block. */
19
+ const BASE_DELIMITER = "EFFECTED_EOF";
20
+ /** A delimiter guaranteed absent from `value`. @internal */
21
+ const delimiterFor = (value) => {
22
+ let delimiter = BASE_DELIMITER;
23
+ while (value.includes(delimiter)) delimiter = `${delimiter}_`;
24
+ return delimiter;
25
+ };
26
+ /**
27
+ * Whether `name` can head a block without breaking the structure it lives
28
+ * in: non-empty, and free of the line breaks that would end it early.
29
+ *
30
+ * @internal
31
+ */
32
+ const isUsableName = (name) => name !== "" && !/[\r\n]/.test(name);
33
+ /**
34
+ * One heredoc block, ready to append. The caller has checked the name with
35
+ * {@link isUsableName}.
36
+ *
37
+ * @internal
38
+ */
39
+ const heredocBlock = (name, value) => {
40
+ const delimiter = delimiterFor(value);
41
+ return `${name}<<${delimiter}\n${value}\n${delimiter}\n`;
42
+ };
43
+
44
+ //#endregion
45
+ export { delimiterFor, heredocBlock, isUsableName };
package/internal/sigv4.js CHANGED
@@ -1,4 +1,5 @@
1
- import { createHash, createHmac } from "node:crypto";
1
+ import { sha256Hex } from "./digest.js";
2
+ import { createHmac } from "node:crypto";
2
3
 
3
4
  //#region src/internal/sigv4.ts
4
5
  /**
@@ -18,7 +19,6 @@ import { createHash, createHmac } from "node:crypto";
18
19
  * @internal
19
20
  */
20
21
  const ALGORITHM = "AWS4-HMAC-SHA256";
21
- const sha256Hex = (value) => createHash("sha256").update(value).digest("hex");
22
22
  const hmac = (key, value) => new Uint8Array(createHmac("sha256", key).update(value).digest());
23
23
  /**
24
24
  * Percent-encode one path segment the way SigV4 requires.
@@ -114,4 +114,4 @@ const sign = (request, credentials) => {
114
114
  };
115
115
 
116
116
  //#endregion
117
- export { canonicalize, sign, signingKey, uriEncode };
117
+ export { canonicalize, sha256Hex as digestHex, sign, signingKey, uriEncode };
@@ -0,0 +1,18 @@
1
+ import { Effect, Stream } from "effect";
2
+
3
+ //#region src/internal/spawn.ts
4
+ /**
5
+ * Spawn `command` once, drain `stdout`+`stderr`, then read the exit code from
6
+ * the same handle. Never fails on a non-zero exit — that is the caller's
7
+ * policy.
8
+ */
9
+ const spawnOnce = (spawner, command) => Effect.scoped(Effect.gen(function* () {
10
+ const handle = yield* spawner.spawn(command);
11
+ return {
12
+ output: yield* Stream.mkString(Stream.decodeText(handle.all)),
13
+ code: yield* handle.exitCode
14
+ };
15
+ }));
16
+
17
+ //#endregion
18
+ export { spawnOnce };
package/internal/twirp.js CHANGED
@@ -14,6 +14,23 @@ import { HttpClientRequest } from "effect/unstable/http";
14
14
  */
15
15
  const CONFLICT = Symbol.for("@effected/github-actions/twirp/conflict");
16
16
  /**
17
+ * The error fields a {@link TwirpFailure} contributes to a caller's own error
18
+ * class — the cache, artifact and blob-store errors all carry `reason`,
19
+ * `status`, `detail` and `cause` under the same names, so each spreads this
20
+ * and adds its own identifier.
21
+ *
22
+ * @internal
23
+ */
24
+ const twirpFailureFields = (failure) => failure.kind === "status" ? {
25
+ reason: "refused",
26
+ detail: failure.method,
27
+ ...failure.status === void 0 ? {} : { status: failure.status }
28
+ } : {
29
+ reason: "unreachable",
30
+ detail: `${failure.method} did not answer with a Twirp body`,
31
+ ...failure.cause === void 0 ? {} : { cause: failure.cause }
32
+ };
33
+ /**
17
34
  * Whether retrying could plausibly help.
18
35
  *
19
36
  * @remarks
@@ -94,4 +111,4 @@ const stringField = (body, name) => {
94
111
  const isOk = (body) => field(body, "ok") === true;
95
112
 
96
113
  //#endregion
97
- export { CONFLICT, field, isOk, isRetryable, stringField, twirpCall };
114
+ export { CONFLICT, field, isOk, isRetryable, stringField, twirpCall, twirpFailureFields };
@@ -0,0 +1,28 @@
1
+ import { Effect } from "effect";
2
+
3
+ //#region src/internal/unstubbed.ts
4
+ /**
5
+ * The one way an unstubbed test-double member dies, for every `makeTest` in
6
+ * this package.
7
+ *
8
+ * @remarks
9
+ * A double's unstubbed member must die rather than silently succeed — a stub
10
+ * that returns nothing teaches a test that nothing happened. The message names
11
+ * the double, the member and the override that fixes it, and it is spelled
12
+ * here once so a wording change is one edit rather than ten.
13
+ *
14
+ * @internal
15
+ */
16
+ /**
17
+ * The die-on-call members of `<double>.makeTest`: `dies("save")` is an
18
+ * `Effect` that defects naming `save` when run. Each package double binds its
19
+ * own name once, then lists its members.
20
+ *
21
+ * @internal
22
+ */
23
+ const unstubbed = (double) => (member) => Effect.sync(() => {
24
+ throw new Error(`${double}: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
25
+ });
26
+
27
+ //#endregion
28
+ export { unstubbed };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/github-actions",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "private": false,
5
5
  "description": "The GitHub Actions runtime: env, inputs, outputs, state, logging, cache, artifacts, blob storage, OIDC and the token bridge.",
6
6
  "keywords": [
@@ -39,12 +39,13 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@azure/storage-blob": "^12.33.0",
42
- "@effected/github": "^0.10.0",
42
+ "@effected/github": "^0.10.1",
43
43
  "@effected/glob": "^0.6.0",
44
44
  "@effected/markdown": "^0.10.0",
45
45
  "@effected/npm": "^0.14.0",
46
- "@effected/sbom": "^0.6.0",
47
- "@effected/templates": "^0.6.0"
46
+ "@effected/sbom": "^0.6.1",
47
+ "@effected/templates": "^0.6.0",
48
+ "@effected/walker": "^0.9.0"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "@effect/platform-node": "4.0.0-rc.115",