@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/DetachedProcess.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
2
|
+
import { Effect, Schedule, Schema } from "effect";
|
|
2
3
|
import { HttpClient } from "effect/unstable/http";
|
|
3
4
|
import { ChildProcessSpawner } from "effect/unstable/process";
|
|
4
5
|
import { spawn } from "node:child_process";
|
|
@@ -116,9 +117,7 @@ const ProcessId = Schema.Number.pipe(Schema.check(Schema.makeFilter((value) => N
|
|
|
116
117
|
/** Whether a thrown value is a Node errno error with the given code. */
|
|
117
118
|
const isErrno = (cause, code) => typeof cause === "object" && cause !== null && cause.code === code;
|
|
118
119
|
/** See {@link DetachedProcess.makeTestOps}: an unstubbed member dies naming itself. */
|
|
119
|
-
const
|
|
120
|
-
throw new Error(`DetachedProcess.makeTestOps: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
121
|
-
};
|
|
120
|
+
const dies = unstubbed("DetachedProcess.makeTestOps");
|
|
122
121
|
/**
|
|
123
122
|
* A long-lived child that outlives the phase that started it.
|
|
124
123
|
*
|
|
@@ -213,14 +212,11 @@ var DetachedProcess = class DetachedProcess {
|
|
|
213
212
|
* timeout six seconds later with nothing to show for it.
|
|
214
213
|
*/
|
|
215
214
|
static awaitReady = Effect.fn("DetachedProcess.awaitReady")(function* (probe, options) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return Effect.flatMap(Effect.sleep(interval), () => poll(remaining - 1));
|
|
222
|
-
});
|
|
223
|
-
return yield* poll(attempts);
|
|
215
|
+
if (!(yield* Effect.repeat(probe, {
|
|
216
|
+
schedule: Schedule.spaced(options?.interval ?? "150 millis"),
|
|
217
|
+
times: options?.attempts ?? 40,
|
|
218
|
+
until: (answer) => answer
|
|
219
|
+
}))) return yield* Effect.fail(new DetachedNotReadyError({}));
|
|
224
220
|
});
|
|
225
221
|
/**
|
|
226
222
|
* A readiness probe over HTTP: `true` when a `GET` answers 2xx.
|
|
@@ -310,9 +306,9 @@ var DetachedProcess = class DetachedProcess {
|
|
|
310
306
|
* production form signals a real pid.
|
|
311
307
|
*/
|
|
312
308
|
static makeTestOps = (overrides = {}) => ({
|
|
313
|
-
spawn: () =>
|
|
314
|
-
awaitReady: () =>
|
|
315
|
-
reap: () =>
|
|
309
|
+
spawn: () => dies("spawn"),
|
|
310
|
+
awaitReady: () => dies("awaitReady"),
|
|
311
|
+
reap: () => dies("reap"),
|
|
316
312
|
...overrides
|
|
317
313
|
});
|
|
318
314
|
};
|
package/DryRun.js
CHANGED
|
@@ -32,15 +32,6 @@ const make = (enabled) => ({
|
|
|
32
32
|
* @public
|
|
33
33
|
*/
|
|
34
34
|
var DryRun = class DryRun extends Context.Service()("@effected/github-actions/DryRun") {
|
|
35
|
-
/**
|
|
36
|
-
* Driven by the `dry-run` action input, defaulting to a real run.
|
|
37
|
-
*
|
|
38
|
-
* @remarks
|
|
39
|
-
* Fails with a `ConfigError` when the input is present but is not a YAML 1.2
|
|
40
|
-
* core-schema boolean — a workflow that writes `dry-run: yes` should stop,
|
|
41
|
-
* not quietly perform the mutations it meant to rehearse.
|
|
42
|
-
*/
|
|
43
|
-
static layer = Layer.effect(this, Effect.map(ActionInput.boolean(DEFAULT_INPUT).pipe(Config.withDefault(false)), make));
|
|
44
35
|
/**
|
|
45
36
|
* Driven by a named input.
|
|
46
37
|
*
|
|
@@ -50,6 +41,15 @@ var DryRun = class DryRun extends Context.Service()("@effected/github-actions/Dr
|
|
|
50
41
|
* composition site.
|
|
51
42
|
*/
|
|
52
43
|
static layerFromInput = (name) => Layer.effect(DryRun, Effect.map(ActionInput.boolean(name).pipe(Config.withDefault(false)), make));
|
|
44
|
+
/**
|
|
45
|
+
* Driven by the `dry-run` action input, defaulting to a real run.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* Fails with a `ConfigError` when the input is present but is not a YAML 1.2
|
|
49
|
+
* core-schema boolean — a workflow that writes `dry-run: yes` should stop,
|
|
50
|
+
* not quietly perform the mutations it meant to rehearse.
|
|
51
|
+
*/
|
|
52
|
+
static layer = DryRun.layerFromInput(DEFAULT_INPUT);
|
|
53
53
|
/** Driven by an explicit decision the caller has already made. */
|
|
54
54
|
static layerFrom = (enabled) => Layer.succeed(DryRun, make(enabled));
|
|
55
55
|
/**
|
package/GitHubMarkdown.js
CHANGED
|
@@ -159,12 +159,12 @@ var GitHubMarkdown = class GitHubMarkdown {
|
|
|
159
159
|
const columns = Object.keys(schema.fields).map((key) => {
|
|
160
160
|
const field = schema.fields[key];
|
|
161
161
|
const column = overrides[key];
|
|
162
|
-
const
|
|
163
|
-
const
|
|
162
|
+
const encode = Schema.encodeSync(field);
|
|
163
|
+
const project = column?.format ?? ((value) => encode(value) ?? "");
|
|
164
164
|
return {
|
|
165
165
|
key,
|
|
166
166
|
header: column?.header ?? SchemaAST.resolveTitle(field.ast) ?? key,
|
|
167
|
-
cell: (value) => value === void 0 ? "" :
|
|
167
|
+
cell: (value) => value === void 0 ? "" : project(value)
|
|
168
168
|
};
|
|
169
169
|
});
|
|
170
170
|
const headers = columns.map((column) => column.header);
|
package/GitHubToken.js
CHANGED
|
@@ -160,7 +160,7 @@ var GitHubToken = class GitHubToken {
|
|
|
160
160
|
static read = Effect.fn("GitHubToken.read")(function* (options = {}) {
|
|
161
161
|
const token = yield* (yield* ActionState).get(options.stateKey ?? DEFAULT_KEY, InstallationToken);
|
|
162
162
|
const now = yield* DateTime.now;
|
|
163
|
-
if (
|
|
163
|
+
if (token.isExpired(DateTime.toEpochMillis(now), options.skew)) return yield* Effect.fail(new GitHubTokenError({
|
|
164
164
|
reason: "expired",
|
|
165
165
|
expiresAt: DateTime.formatIso(token.expiresAt)
|
|
166
166
|
}));
|
package/ManagedDocument.js
CHANGED
|
@@ -83,6 +83,12 @@ const STRUCTURAL_KIND = {
|
|
|
83
83
|
overlappingSections: "overlappingRegions",
|
|
84
84
|
duplicateSection: "duplicateRegion"
|
|
85
85
|
};
|
|
86
|
+
/** A structurally corrupt region layout, in this surface's vocabulary. */
|
|
87
|
+
const structural = (failure) => new ManagedDocumentError({
|
|
88
|
+
kind: STRUCTURAL_KIND[failure.reason],
|
|
89
|
+
line: failure.line,
|
|
90
|
+
...failure.key === void 0 ? {} : { key: failure.key }
|
|
91
|
+
});
|
|
86
92
|
/**
|
|
87
93
|
* A marker-delimited document: named regions a tool owns, inside text a human
|
|
88
94
|
* may also edit.
|
|
@@ -145,21 +151,12 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
145
151
|
* a silent choice would destroy content a human wrote.
|
|
146
152
|
*/
|
|
147
153
|
static parseResult(source) {
|
|
148
|
-
const
|
|
149
|
-
const scanned = SectionDocument.parseResult(text, REGION_DIALECT);
|
|
150
|
-
if (Result.isFailure(scanned)) {
|
|
151
|
-
const failure = scanned.failure;
|
|
152
|
-
return Result.fail(new ManagedDocumentError({
|
|
153
|
-
kind: STRUCTURAL_KIND[failure.reason],
|
|
154
|
-
line: failure.line,
|
|
155
|
-
...failure.key === void 0 ? {} : { key: failure.key }
|
|
156
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
return Result.succeed(ManagedDocument.make({
|
|
154
|
+
const document = ManagedDocument.make({
|
|
159
155
|
namespace: source.namespace,
|
|
160
156
|
key: source.key,
|
|
161
|
-
text
|
|
162
|
-
})
|
|
157
|
+
text: source.text ?? ""
|
|
158
|
+
});
|
|
159
|
+
return Result.map(Result.mapError(document.scan(), structural), () => document);
|
|
163
160
|
}
|
|
164
161
|
/**
|
|
165
162
|
* Read a document out of existing text, in `Effect`.
|
|
@@ -179,8 +176,7 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
179
176
|
}
|
|
180
177
|
/** The content of one region, if the document carries it. */
|
|
181
178
|
region(key) {
|
|
182
|
-
|
|
183
|
-
return found === void 0 ? Option.none() : Option.some(found.content);
|
|
179
|
+
return Option.map(this.entry(key), (found) => found.content);
|
|
184
180
|
}
|
|
185
181
|
/**
|
|
186
182
|
* One region's content and metadata together, if the document carries it.
|
|
@@ -235,15 +231,8 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
235
231
|
* surfacing in the typed channel.
|
|
236
232
|
*/
|
|
237
233
|
withRegionsResult(entries) {
|
|
238
|
-
const parsed =
|
|
239
|
-
if (Result.isFailure(parsed))
|
|
240
|
-
const failure = parsed.failure;
|
|
241
|
-
return Result.fail(new ManagedDocumentError({
|
|
242
|
-
kind: STRUCTURAL_KIND[failure.reason],
|
|
243
|
-
line: failure.line,
|
|
244
|
-
...failure.key === void 0 ? {} : { key: failure.key }
|
|
245
|
-
}));
|
|
246
|
-
}
|
|
234
|
+
const parsed = this.scan();
|
|
235
|
+
if (Result.isFailure(parsed)) return Result.fail(structural(parsed.failure));
|
|
247
236
|
const declared = entries.map(([key, content, meta]) => SectionId.make({
|
|
248
237
|
key: this.wireKey(key),
|
|
249
238
|
commentStyle: CommentStyle.html
|
|
@@ -275,14 +264,22 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
275
264
|
withRegions(entries) {
|
|
276
265
|
return Effect.fromResult(this.withRegionsResult(entries));
|
|
277
266
|
}
|
|
267
|
+
#scanned;
|
|
268
|
+
scan() {
|
|
269
|
+
this.#scanned ??= SectionDocument.parseResult(this.text, REGION_DIALECT);
|
|
270
|
+
return this.#scanned;
|
|
271
|
+
}
|
|
272
|
+
/** What every wire key of this document's regions begins with. */
|
|
273
|
+
get wirePrefix() {
|
|
274
|
+
return `${this.namespace}.${this.key}.`;
|
|
275
|
+
}
|
|
278
276
|
/** The marker key a region is written under: `namespace.key.region`. */
|
|
279
277
|
wireKey(region) {
|
|
280
|
-
return `${this.
|
|
278
|
+
return `${this.wirePrefix}${region}`;
|
|
281
279
|
}
|
|
282
280
|
/** The region key back out of a wire key, for error reporting. */
|
|
283
281
|
regionKeyOf(wireKey) {
|
|
284
|
-
|
|
285
|
-
return wireKey.startsWith(prefix) ? wireKey.slice(prefix.length) : wireKey;
|
|
282
|
+
return wireKey.startsWith(this.wirePrefix) ? wireKey.slice(this.wirePrefix.length) : wireKey;
|
|
286
283
|
}
|
|
287
284
|
/**
|
|
288
285
|
* This document's own regions, in document order.
|
|
@@ -296,9 +293,9 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
296
293
|
* never reported.
|
|
297
294
|
*/
|
|
298
295
|
ownRegions() {
|
|
299
|
-
const parsed =
|
|
296
|
+
const parsed = this.scan();
|
|
300
297
|
if (Result.isFailure(parsed)) return [];
|
|
301
|
-
const prefix =
|
|
298
|
+
const prefix = this.wirePrefix;
|
|
302
299
|
return parsed.success.sections.filter((placed) => placed.section.key.startsWith(prefix)).map((placed) => ({
|
|
303
300
|
key: placed.section.key.slice(prefix.length),
|
|
304
301
|
content: placed.section.content,
|
package/OidcTokenIssuer.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
|
+
import { payloadOf, unsignedJwt } from "./internal/jwt.js";
|
|
2
4
|
import { Context, Effect, Layer, Redacted, Schema } from "effect";
|
|
3
|
-
import { HttpClient } from "effect/unstable/http";
|
|
5
|
+
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
4
6
|
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
5
7
|
|
|
6
8
|
//#region src/OidcTokenIssuer.ts
|
|
@@ -89,18 +91,6 @@ const TokenEnvelope = Schema.Struct({
|
|
|
89
91
|
const REQUEST_TOKEN = "ACTIONS_ID_TOKEN_REQUEST_TOKEN";
|
|
90
92
|
const REQUEST_URL = "ACTIONS_ID_TOKEN_REQUEST_URL";
|
|
91
93
|
/**
|
|
92
|
-
* Decode a JWT payload segment.
|
|
93
|
-
*
|
|
94
|
-
* @remarks
|
|
95
|
-
* `base64url` states the intent, though it is not what makes this work: Node's
|
|
96
|
-
* plain `base64` decoder happens to accept the url alphabet and unpadded input
|
|
97
|
-
* too (probed at beta.101), so the two are interchangeable *here*. They are not
|
|
98
|
-
* interchangeable everywhere — `atob` is not forgiving — which is why the
|
|
99
|
-
* declaration is the strict one and there is a test whose payload actually
|
|
100
|
-
* contains `-` and `_`.
|
|
101
|
-
*/
|
|
102
|
-
const decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
103
|
-
/**
|
|
104
94
|
* Read the claims out of a JWT **without verifying its signature**.
|
|
105
95
|
*
|
|
106
96
|
* @remarks
|
|
@@ -108,20 +98,14 @@ const decodeSegment = (segment) => JSON.parse(Buffer.from(segment, "base64url").
|
|
|
108
98
|
* {@link OidcTokenIssuerShape.claims}; do not "fix" it here.
|
|
109
99
|
*/
|
|
110
100
|
const readClaims = (token) => Effect.gen(function* () {
|
|
111
|
-
const
|
|
112
|
-
const payload = parts[1];
|
|
113
|
-
if (parts.length !== 3 || payload === void 0 || payload === "") return yield* Effect.fail(new OidcTokenError({
|
|
101
|
+
const decoded = yield* Effect.fromResult(payloadOf(token)).pipe(Effect.mapError((failure) => failure.kind === "segments" ? new OidcTokenError({
|
|
114
102
|
reason: "malformedToken",
|
|
115
|
-
detail:
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
detail: "the payload is not base64url JSON",
|
|
122
|
-
cause
|
|
123
|
-
})
|
|
124
|
-
});
|
|
103
|
+
detail: failure.detail
|
|
104
|
+
}) : new OidcTokenError({
|
|
105
|
+
reason: "malformedToken",
|
|
106
|
+
detail: failure.detail,
|
|
107
|
+
cause: failure.cause
|
|
108
|
+
})));
|
|
125
109
|
return yield* Schema.decodeUnknownEffect(OidcClaims)(decoded).pipe(Effect.mapError((cause) => new OidcTokenError({
|
|
126
110
|
reason: "missingClaims",
|
|
127
111
|
cause
|
|
@@ -150,10 +134,7 @@ const make = Effect.gen(function* () {
|
|
|
150
134
|
const bearer = yield* required(REQUEST_TOKEN);
|
|
151
135
|
const base = yield* required(REQUEST_URL);
|
|
152
136
|
const url = audience === void 0 ? base : `${base}&audience=${encodeURIComponent(audience)}`;
|
|
153
|
-
const response = yield* http.get(url,
|
|
154
|
-
authorization: `Bearer ${bearer}`,
|
|
155
|
-
accept: "application/json"
|
|
156
|
-
} }).pipe(Effect.mapError((cause) => new OidcTokenError({
|
|
137
|
+
const response = yield* http.execute(HttpClientRequest.get(url).pipe(HttpClientRequest.bearerToken(bearer), HttpClientRequest.acceptJson)).pipe(Effect.mapError((cause) => new OidcTokenError({
|
|
157
138
|
reason: "requestFailed",
|
|
158
139
|
cause
|
|
159
140
|
})));
|
|
@@ -173,9 +154,7 @@ const make = Effect.gen(function* () {
|
|
|
173
154
|
})
|
|
174
155
|
};
|
|
175
156
|
});
|
|
176
|
-
const
|
|
177
|
-
throw new Error(`OidcTokenIssuer.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
178
|
-
};
|
|
157
|
+
const dies = unstubbed("OidcTokenIssuer.makeTest");
|
|
179
158
|
/**
|
|
180
159
|
* The runner's OIDC token service.
|
|
181
160
|
*
|
|
@@ -211,17 +190,14 @@ var OidcTokenIssuer = class OidcTokenIssuer extends Context.Service()("@effected
|
|
|
211
190
|
* provenance path structurally unreachable in the source package, drawing
|
|
212
191
|
* four separate apologetic comments from one consumer.
|
|
213
192
|
*/
|
|
214
|
-
static unsignedTokenFor = (claims) => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
typ: "JWT"
|
|
219
|
-
})}.${segment(Schema.encodeUnknownSync(OidcClaims)(claims))}.unsigned`);
|
|
220
|
-
};
|
|
193
|
+
static unsignedTokenFor = (claims) => Redacted.make(unsignedJwt({
|
|
194
|
+
alg: "RS256",
|
|
195
|
+
typ: "JWT"
|
|
196
|
+
}, Schema.encodeUnknownSync(OidcClaims)(claims)));
|
|
221
197
|
/** A test double. Unstubbed members die rather than answering with a non-token. */
|
|
222
198
|
static makeTest = (overrides = {}) => ({
|
|
223
|
-
token: () =>
|
|
224
|
-
claims: () =>
|
|
199
|
+
token: () => dies("token"),
|
|
200
|
+
claims: () => dies("claims"),
|
|
225
201
|
...overrides
|
|
226
202
|
});
|
|
227
203
|
/** {@link OidcTokenIssuer.makeTest} behind `Layer.succeed`. */
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
|
+
import { digestFileHex } from "./internal/digest.js";
|
|
4
|
+
import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
|
|
5
|
+
import { typeAt } from "./internal/fsProbe.js";
|
|
2
6
|
import { ToolInstaller } from "./ToolInstaller.js";
|
|
3
|
-
import { Context, Effect, FileSystem, Layer, Option, Path, Schema
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
7
|
+
import { Context, Effect, FileSystem, Layer, Option, Path, Schema } from "effect";
|
|
5
8
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
6
|
-
import { PackageManagerPinName } from "@effected/npm";
|
|
9
|
+
import { DEFAULT_REGISTRY, PackageManagerPinName } from "@effected/npm";
|
|
7
10
|
|
|
8
11
|
//#region src/PackageManagerInstaller.ts
|
|
9
12
|
/**
|
|
@@ -130,7 +133,6 @@ var CachedPackageManager = class extends Schema.Class("CachedPackageManager")({
|
|
|
130
133
|
* @public
|
|
131
134
|
*/
|
|
132
135
|
const InstalledPackageManager = Schema.Union([AmbientPackageManager, CachedPackageManager]);
|
|
133
|
-
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
134
136
|
/** The shim directory name inside a cached npm-registry-manager entry. */
|
|
135
137
|
const SHIM_DIR = ".bin";
|
|
136
138
|
/**
|
|
@@ -149,9 +151,18 @@ const cmdShim = (target) => `@echo off\r\nnode "${target}" %*\r\n`;
|
|
|
149
151
|
* bun's spelling (`macOS` → `darwin`) and the Node arch spelling to bun's
|
|
150
152
|
* (`arm64` → `aarch64`).
|
|
151
153
|
*/
|
|
154
|
+
const BUN_OS = {
|
|
155
|
+
linux: "linux",
|
|
156
|
+
macos: "darwin",
|
|
157
|
+
windows: "windows"
|
|
158
|
+
};
|
|
159
|
+
const BUN_CPU = {
|
|
160
|
+
x64: "x64",
|
|
161
|
+
arm64: "aarch64"
|
|
162
|
+
};
|
|
152
163
|
const bunTarget = (runnerOs, arch) => {
|
|
153
|
-
const os = runnerOs.toLowerCase()
|
|
154
|
-
const cpu = arch
|
|
164
|
+
const os = BUN_OS[runnerOs.toLowerCase()];
|
|
165
|
+
const cpu = BUN_CPU[arch];
|
|
155
166
|
return os === void 0 || cpu === void 0 ? Option.none() : Option.some(`bun-${os}-${cpu}`);
|
|
156
167
|
};
|
|
157
168
|
/**
|
|
@@ -174,7 +185,10 @@ const normalizeBins = (bin, fallbackName) => {
|
|
|
174
185
|
return Object.keys(entries).length === 0 ? Option.none() : Option.some(entries);
|
|
175
186
|
};
|
|
176
187
|
/** Map a `RUNNER_ARCH` value (`X64`, `ARM64`) onto the Node arch spelling. */
|
|
177
|
-
const archFromRunner = (runnerArch) =>
|
|
188
|
+
const archFromRunner = (runnerArch) => {
|
|
189
|
+
const lowered = runnerArch.toLowerCase();
|
|
190
|
+
return lowered === "x64" || lowered === "arm64" ? lowered : runnerArch;
|
|
191
|
+
};
|
|
178
192
|
const make = Effect.gen(function* () {
|
|
179
193
|
const env = yield* ActionEnvironment;
|
|
180
194
|
const fs = yield* FileSystem.FileSystem;
|
|
@@ -186,11 +200,11 @@ const make = Effect.gen(function* () {
|
|
|
186
200
|
onNone: () => process.arch,
|
|
187
201
|
onSome: archFromRunner
|
|
188
202
|
}));
|
|
189
|
-
const windows =
|
|
203
|
+
const windows = yield* isWindowsRunner(env);
|
|
190
204
|
const bunBinaryName = windows ? "bun.exe" : "bun";
|
|
191
205
|
const shimFileName = (name) => windows ? `${name}.cmd` : name;
|
|
192
206
|
const shimBody = windows ? cmdShim : posixShim;
|
|
193
|
-
const cacheRoot = yield*
|
|
207
|
+
const cacheRoot = yield* toolCacheRoot(env, path);
|
|
194
208
|
const finalCachePath = (tool, version) => ToolInstaller.cachePath({
|
|
195
209
|
root: cacheRoot,
|
|
196
210
|
tool,
|
|
@@ -209,16 +223,11 @@ const make = Effect.gen(function* () {
|
|
|
209
223
|
cause
|
|
210
224
|
});
|
|
211
225
|
/** The hex digest of a file's bytes, streamed rather than buffered. */
|
|
212
|
-
const hashFile = (pin, file, algorithm) => Effect.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
reason: "integrityMismatch",
|
|
218
|
-
subject: file,
|
|
219
|
-
cause
|
|
220
|
-
})));
|
|
221
|
-
});
|
|
226
|
+
const hashFile = (pin, file, algorithm) => digestFileHex(fs, file, algorithm).pipe(Effect.mapError((cause) => errorFor(pin)({
|
|
227
|
+
reason: "integrityMismatch",
|
|
228
|
+
subject: file,
|
|
229
|
+
cause
|
|
230
|
+
})));
|
|
222
231
|
/**
|
|
223
232
|
* Verify a downloaded artifact against the pin's integrity, fail-closed.
|
|
224
233
|
*
|
|
@@ -250,8 +259,7 @@ const make = Effect.gen(function* () {
|
|
|
250
259
|
*/
|
|
251
260
|
const ambientNpmVersion = Effect.map(Effect.result(spawner.string(ChildProcess.make("npm", ["--version"]))), (result) => result._tag === "Success" ? Option.some(result.success.trim()) : Option.none());
|
|
252
261
|
const assertFile = (pin, file, subject) => Effect.gen(function* () {
|
|
253
|
-
|
|
254
|
-
if (stat._tag !== "Success" || stat.success.type !== "File") return yield* Effect.fail(errorFor(pin)({
|
|
262
|
+
if ((yield* typeAt(fs, file)) !== "File") return yield* Effect.fail(errorFor(pin)({
|
|
255
263
|
reason: "layoutUnexpected",
|
|
256
264
|
subject
|
|
257
265
|
}));
|
|
@@ -319,8 +327,7 @@ const make = Effect.gen(function* () {
|
|
|
319
327
|
for (const [name, relative] of Object.entries(bins)) {
|
|
320
328
|
const shim = path.join(shimDir, shimFileName(name));
|
|
321
329
|
if (options.skipExisting) {
|
|
322
|
-
|
|
323
|
-
if (present._tag === "Success" && present.success.type === "File") continue;
|
|
330
|
+
if ((yield* typeAt(fs, shim)) === "File") continue;
|
|
324
331
|
}
|
|
325
332
|
yield* fs.writeFileString(shim, shimBody(path.join(finalDirectory, relative))).pipe(Effect.mapError(cacheError(shim)));
|
|
326
333
|
if (!windows) yield* fs.chmod(shim, 493).pipe(Effect.mapError(cacheError(shim)));
|
|
@@ -386,8 +393,7 @@ const make = Effect.gen(function* () {
|
|
|
386
393
|
if (!berry) yield* verifyIntegrity(pin, archive);
|
|
387
394
|
const extracted = yield* installer.extractTar(archive).pipe(Effect.mapError(fromInstaller(pin)));
|
|
388
395
|
const packageDir = path.join(extracted, "package");
|
|
389
|
-
|
|
390
|
-
if (stat._tag !== "Success" || stat.success.type !== "Directory") return yield* Effect.fail(errorFor(pin)({
|
|
396
|
+
if ((yield* typeAt(fs, packageDir)) !== "Directory") return yield* Effect.fail(errorFor(pin)({
|
|
391
397
|
reason: "layoutUnexpected",
|
|
392
398
|
subject: "tarball has no package/ root"
|
|
393
399
|
}));
|
|
@@ -437,9 +443,7 @@ const make = Effect.gen(function* () {
|
|
|
437
443
|
return yield* installFromRegistry(pin, pin.name, registry);
|
|
438
444
|
}) };
|
|
439
445
|
});
|
|
440
|
-
const
|
|
441
|
-
throw new Error(`PackageManagerInstaller.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
442
|
-
};
|
|
446
|
+
const dies = unstubbed("PackageManagerInstaller.makeTest");
|
|
443
447
|
/**
|
|
444
448
|
* First-class exact-version package-manager provisioning on a GitHub runner.
|
|
445
449
|
*
|
|
@@ -483,7 +487,7 @@ var PackageManagerInstaller = class PackageManagerInstaller extends Context.Serv
|
|
|
483
487
|
static layer = Layer.effect(this, make);
|
|
484
488
|
/** A test double. The unstubbed member dies rather than reporting an install that did not happen. */
|
|
485
489
|
static makeTest = (overrides = {}) => ({
|
|
486
|
-
install: () =>
|
|
490
|
+
install: () => dies("install"),
|
|
487
491
|
...overrides
|
|
488
492
|
});
|
|
489
493
|
/** {@link PackageManagerInstaller.makeTest} behind `Layer.succeed`. */
|
package/ToolInstaller.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { ActionEnvironment } from "./ActionEnvironment.js";
|
|
2
|
+
import { unstubbed } from "./internal/unstubbed.js";
|
|
3
|
+
import { isWindowsRunner, toolCacheRoot } from "./internal/runner.js";
|
|
4
|
+
import { spawnOnce } from "./internal/spawn.js";
|
|
5
|
+
import { typeAt } from "./internal/fsProbe.js";
|
|
2
6
|
import { Context, Effect, FileSystem, Layer, Option, Path, Schedule, Schema, Stream } from "effect";
|
|
3
7
|
import { HttpClient } from "effect/unstable/http";
|
|
4
8
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
|
|
@@ -61,11 +65,8 @@ const make = Effect.gen(function* () {
|
|
|
61
65
|
const path = yield* Path.Path;
|
|
62
66
|
const http = yield* HttpClient.HttpClient;
|
|
63
67
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
64
|
-
const root = yield*
|
|
65
|
-
const windows = yield*
|
|
66
|
-
onNone: () => false,
|
|
67
|
-
onSome: (os) => os.toLowerCase() === "windows"
|
|
68
|
-
}));
|
|
68
|
+
const root = yield* toolCacheRoot(env, path);
|
|
69
|
+
const windows = yield* isWindowsRunner(env);
|
|
69
70
|
const cachePath = (tool, version) => ToolInstaller.cachePath({
|
|
70
71
|
root,
|
|
71
72
|
tool,
|
|
@@ -93,30 +94,22 @@ const make = Effect.gen(function* () {
|
|
|
93
94
|
* the difference between "this is not a gzip archive" and "tar is not
|
|
94
95
|
* installed". Discarding it is what makes an extraction failure unactionable.
|
|
95
96
|
*
|
|
96
|
-
* **One spawn, not two — load-bearing
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* `ChildProcessSpawner.make` derives both from `spawn`). Calling them
|
|
100
|
-
* back-to-back double-executed every extraction — harmless for idempotent
|
|
101
|
-
* `tar`/`unzip -o`, but .NET's `ZipFile.ExtractToDirectory` refuses to
|
|
102
|
-
* overwrite, so the second run failed 5/5 on real Windows runners while the
|
|
103
|
-
* captured "complaint" was the FIRST run's silent success. Output and exit
|
|
104
|
-
* code must come from the same `spawn` handle.
|
|
97
|
+
* **One spawn, not two — load-bearing**, and why is spelled once in
|
|
98
|
+
* `internal/spawn.ts`: the double run failed 5/5 on real Windows runners
|
|
99
|
+
* because .NET's `ZipFile.ExtractToDirectory` refuses to overwrite.
|
|
105
100
|
*/
|
|
106
|
-
const extractWith = (command, archive) => Effect.
|
|
107
|
-
const
|
|
101
|
+
const extractWith = (command, archive) => Effect.gen(function* () {
|
|
102
|
+
const { output, code } = yield* spawnOnce(spawner, command).pipe(Effect.mapError((cause) => new ToolInstallerError({
|
|
108
103
|
reason: "extractFailed",
|
|
109
104
|
subject: archive,
|
|
110
105
|
cause
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const output = yield* Stream.mkString(Stream.decodeText(handle.all)).pipe(Effect.mapError(extractError));
|
|
114
|
-
if ((yield* handle.exitCode.pipe(Effect.mapError(extractError))) !== 0) return yield* Effect.fail(new ToolInstallerError({
|
|
106
|
+
})));
|
|
107
|
+
if (code !== 0) return yield* Effect.fail(new ToolInstallerError({
|
|
115
108
|
reason: "extractFailed",
|
|
116
109
|
subject: archive,
|
|
117
110
|
stderr: output.trim()
|
|
118
111
|
}));
|
|
119
|
-
})
|
|
112
|
+
});
|
|
120
113
|
/**
|
|
121
114
|
* Install a staged directory into the tool cache by renaming it into place.
|
|
122
115
|
*
|
|
@@ -191,7 +184,7 @@ const make = Effect.gen(function* () {
|
|
|
191
184
|
while: (error) => error.retryable
|
|
192
185
|
}));
|
|
193
186
|
});
|
|
194
|
-
const find = (tool, version) => Effect.map(
|
|
187
|
+
const find = (tool, version) => Effect.map(typeAt(fs, cachePath(tool, version)), (type) => type === "Directory" ? Option.some(cachePath(tool, version)) : Option.none());
|
|
195
188
|
const cacheFile = Effect.fn("ToolInstaller.cacheFile")(function* (source, name, tool, version) {
|
|
196
189
|
yield* Effect.annotateCurrentSpan({
|
|
197
190
|
tool,
|
|
@@ -252,8 +245,7 @@ const make = Effect.gen(function* () {
|
|
|
252
245
|
});
|
|
253
246
|
const cached = yield* find(options.tool, options.version);
|
|
254
247
|
if (Option.isSome(cached)) {
|
|
255
|
-
|
|
256
|
-
if (present._tag === "Success" && present.success.type === "File") return {
|
|
248
|
+
if ((yield* typeAt(fs, path.join(cached.value, options.binary))) === "File") return {
|
|
257
249
|
directory: cached.value,
|
|
258
250
|
binDir: cached.value
|
|
259
251
|
};
|
|
@@ -272,9 +264,7 @@ const make = Effect.gen(function* () {
|
|
|
272
264
|
})
|
|
273
265
|
};
|
|
274
266
|
});
|
|
275
|
-
const
|
|
276
|
-
throw new Error(`ToolInstaller.makeTest: ${member}() was called but not stubbed — pass a \`${member}\` override.`);
|
|
277
|
-
};
|
|
267
|
+
const dies = unstubbed("ToolInstaller.makeTest");
|
|
278
268
|
/**
|
|
279
269
|
* Download, extract and cache a toolchain in the runner's tool cache.
|
|
280
270
|
*
|
|
@@ -323,13 +313,13 @@ var ToolInstaller = class ToolInstaller extends Context.Service()("@effected/git
|
|
|
323
313
|
static cachePath = (options) => `${options.root}/${options.tool}/${options.version}/${options.arch}`;
|
|
324
314
|
/** A test double. Unstubbed members die rather than reporting a tool that is not there. */
|
|
325
315
|
static makeTest = (overrides = {}) => ({
|
|
326
|
-
find: () =>
|
|
327
|
-
download: () =>
|
|
328
|
-
extractTar: () =>
|
|
329
|
-
extractZip: () =>
|
|
330
|
-
cacheDir: () =>
|
|
331
|
-
cacheFile: () =>
|
|
332
|
-
provisionFile: () =>
|
|
316
|
+
find: () => dies("find"),
|
|
317
|
+
download: () => dies("download"),
|
|
318
|
+
extractTar: () => dies("extractTar"),
|
|
319
|
+
extractZip: () => dies("extractZip"),
|
|
320
|
+
cacheDir: () => dies("cacheDir"),
|
|
321
|
+
cacheFile: () => dies("cacheFile"),
|
|
322
|
+
provisionFile: () => dies("provisionFile"),
|
|
333
323
|
...overrides
|
|
334
324
|
});
|
|
335
325
|
/** {@link ToolInstaller.makeTest} behind `Layer.succeed`. */
|