@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
|
@@ -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 };
|
package/internal/jwt.js
ADDED
|
@@ -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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|