@savvy-web/github-action-effects 0.1.0 → 0.3.0
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/README.md +20 -42
- package/index.d.ts +256 -73
- package/index.js +255 -102
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,71 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@savvy-web/github-action-effects)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
inputs, structured logging with buffered output, and type-safe outputs through
|
|
8
|
-
composable service layers.
|
|
7
|
+
Composable [Effect](https://effect.website) services for building Node.js 24 GitHub Actions with schema-validated inputs, structured logging, typed outputs and multi-phase state management without the boilerplate.
|
|
9
8
|
|
|
10
9
|
## Features
|
|
11
10
|
|
|
12
|
-
- **Schema-validated inputs and
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
11
|
+
- **Schema-validated inputs** — read, parse, and validate action inputs with Effect Schema
|
|
12
|
+
- **Structured logging** — three-tier logger (info/verbose/debug) with buffer-on-failure
|
|
13
|
+
- **Typed outputs** — set outputs, export variables, and write GFM job summaries
|
|
14
|
+
- **Multi-phase state** — transfer schema-serialized state across pre/main/post phases
|
|
15
|
+
- **In-memory test layers** — test every service without mocking `@actions/core`
|
|
17
16
|
|
|
18
17
|
## Installation
|
|
19
18
|
|
|
20
19
|
```bash
|
|
21
|
-
npm install @savvy-web/github-action-effects effect @actions/core
|
|
20
|
+
npm install @savvy-web/github-action-effects effect @actions/core @effect/platform @effect/platform-node
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
## Quick Start
|
|
25
24
|
|
|
26
25
|
```typescript
|
|
27
|
-
import { Effect,
|
|
28
|
-
import {
|
|
29
|
-
ActionInputs,
|
|
30
|
-
ActionInputsLive,
|
|
31
|
-
ActionOutputs,
|
|
32
|
-
ActionOutputsLive,
|
|
33
|
-
ActionLoggerLive,
|
|
34
|
-
ActionLoggerLayer,
|
|
35
|
-
LogLevelInput,
|
|
36
|
-
resolveLogLevel,
|
|
37
|
-
setLogLevel,
|
|
38
|
-
table,
|
|
39
|
-
} from "@savvy-web/github-action-effects";
|
|
26
|
+
import { Effect, Schema } from "effect";
|
|
27
|
+
import { Action, ActionInputs, ActionOutputs } from "@savvy-web/github-action-effects";
|
|
40
28
|
|
|
41
29
|
const program = Effect.gen(function* () {
|
|
42
30
|
const inputs = yield* ActionInputs;
|
|
43
31
|
const outputs = yield* ActionOutputs;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
yield* setLogLevel(resolveLogLevel(level));
|
|
47
|
-
|
|
48
|
-
const name = yield* inputs.get("name", Schema.String);
|
|
49
|
-
yield* outputs.set("greeting", `Hello, ${name}!`);
|
|
50
|
-
yield* outputs.summary(table(["Input", "Value"], [["name", name]]));
|
|
32
|
+
const name = yield* inputs.get("package-name", Schema.String);
|
|
33
|
+
yield* outputs.set("result", `checked ${name}`);
|
|
51
34
|
});
|
|
52
35
|
|
|
53
|
-
|
|
54
|
-
ActionInputsLive,
|
|
55
|
-
ActionOutputsLive,
|
|
56
|
-
ActionLoggerLive,
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
program.pipe(
|
|
60
|
-
Effect.provide(ActionLoggerLayer),
|
|
61
|
-
Effect.provide(MainLive),
|
|
62
|
-
Effect.runPromise,
|
|
63
|
-
);
|
|
36
|
+
Action.run(program);
|
|
64
37
|
```
|
|
65
38
|
|
|
39
|
+
`Action.run` provides all core service layers (including `NodeContext.layer` for `FileSystem`, `Path`, `Terminal`, `CommandExecutor`, and `WorkerManager` from `@effect/platform`), installs the Effect logger, and catches errors with `core.setFailed` automatically.
|
|
40
|
+
|
|
41
|
+
See the [full walkthrough](./docs/example-action.md) for log level configuration, batch input reading, GFM summaries, multi-phase state, and error handling.
|
|
42
|
+
|
|
66
43
|
## Documentation
|
|
67
44
|
|
|
68
|
-
|
|
69
|
-
[
|
|
45
|
+
- [Example Action](./docs/example-action.md) — end-to-end tutorial
|
|
46
|
+
- [Architecture](./docs/architecture.md) — API reference and layer composition
|
|
47
|
+
- [Testing](./docs/testing.md) — testing with in-memory layers
|
|
70
48
|
|
|
71
49
|
## License
|
|
72
50
|
|
package/index.d.ts
CHANGED
|
@@ -13,11 +13,69 @@ import { Effect } from 'effect';
|
|
|
13
13
|
import { Equals } from 'effect/Types';
|
|
14
14
|
import { FiberRef } from 'effect';
|
|
15
15
|
import { Layer } from 'effect';
|
|
16
|
-
import { Logger } from 'effect';
|
|
16
|
+
import { Logger } from 'effect/Logger';
|
|
17
17
|
import type { Option } from 'effect';
|
|
18
|
+
import * as PlatformNode from '@effect/platform-node';
|
|
18
19
|
import { Schema } from 'effect';
|
|
19
20
|
import { YieldableError } from 'effect/Cause';
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Namespace for top-level GitHub Action helpers.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Effect } from "effect"
|
|
28
|
+
* import { Action, ActionInputs, ActionLogger } from "@savvy-web/github-action-effects"
|
|
29
|
+
*
|
|
30
|
+
* const program = Effect.gen(function* () {
|
|
31
|
+
* const inputs = yield* ActionInputs
|
|
32
|
+
* const logger = yield* ActionLogger
|
|
33
|
+
* // ... your action logic
|
|
34
|
+
* })
|
|
35
|
+
*
|
|
36
|
+
* Action.run(program)
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export declare const Action: {
|
|
42
|
+
/**
|
|
43
|
+
* Run a GitHub Action program with standard boilerplate handled.
|
|
44
|
+
*
|
|
45
|
+
* Handles:
|
|
46
|
+
* - Providing all standard Live layers (ActionInputs, ActionLogger, ActionOutputs)
|
|
47
|
+
* - Installing ActionLoggerLayer (routes Effect.log to core.info/debug)
|
|
48
|
+
* - Catching all errors and calling `core.setFailed`
|
|
49
|
+
* - Running with `Effect.runPromise`
|
|
50
|
+
*
|
|
51
|
+
* Returns a Promise that resolves when the action completes. In production
|
|
52
|
+
* the return value can be ignored (fire-and-forget). In tests, await it
|
|
53
|
+
* to avoid timing issues.
|
|
54
|
+
*/
|
|
55
|
+
readonly run: {
|
|
56
|
+
<E>(program: Effect.Effect<void, E, CoreServices>): Promise<void>;
|
|
57
|
+
<E, R>(program: Effect.Effect<void, E, R | CoreServices>, layer: Layer.Layer<R, never, never>): Promise<void>;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Read and validate all inputs at once, with optional cross-validation.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const inputs = yield* Action.parseInputs({
|
|
65
|
+
* "app-id": { schema: Schema.NumberFromString, required: true },
|
|
66
|
+
* "branch": { schema: Schema.String, default: "main" },
|
|
67
|
+
* })
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
readonly parseInputs: <T extends Record<string, InputConfig<Schema.Schema.AnyNoContext>>>(config: T, crossValidate?: ((parsed: ParsedInputs<T>) => Effect.Effect<ParsedInputs<T>, ActionInputError, never>) | undefined) => Effect.Effect<ParsedInputs<T>, ActionInputError, ActionInputs>;
|
|
71
|
+
/** Create an Effect Logger that routes to GitHub Actions log functions. */
|
|
72
|
+
readonly makeLogger: () => Logger<unknown, void>;
|
|
73
|
+
/** Set the action log level for the current scope. */
|
|
74
|
+
readonly setLogLevel: (level: "debug" | "info" | "verbose") => Effect.Effect<void, never, never>;
|
|
75
|
+
/** Resolve a LogLevelInput to a concrete ActionLogLevel. */
|
|
76
|
+
readonly resolveLogLevel: (input: "auto" | "debug" | "info" | "verbose") => "debug" | "info" | "verbose";
|
|
77
|
+
};
|
|
78
|
+
|
|
21
79
|
/**
|
|
22
80
|
* Error when a GitHub Action input is missing or fails schema validation.
|
|
23
81
|
*/
|
|
@@ -62,6 +120,21 @@ export declare interface ActionInputs {
|
|
|
62
120
|
* Read a required input as a JSON string, parse and validate it.
|
|
63
121
|
*/
|
|
64
122
|
readonly getJson: <A, I>(name: string, schema: Schema.Schema<A, I, never>) => Effect.Effect<A, ActionInputError>;
|
|
123
|
+
/**
|
|
124
|
+
* Read a multiline input (newline-delimited list).
|
|
125
|
+
* Splits on newlines, trims each line, filters blank lines and comment lines (starting with #).
|
|
126
|
+
* Each remaining item is validated against the schema.
|
|
127
|
+
*/
|
|
128
|
+
readonly getMultiline: <A, I>(name: string, itemSchema: Schema.Schema<A, I, never>) => Effect.Effect<Array<A>, ActionInputError>;
|
|
129
|
+
/**
|
|
130
|
+
* Read a boolean input. Accepts "true"/"false" (case-insensitive).
|
|
131
|
+
*/
|
|
132
|
+
readonly getBoolean: (name: string) => Effect.Effect<boolean, ActionInputError>;
|
|
133
|
+
/**
|
|
134
|
+
* Read an optional boolean input with a default value.
|
|
135
|
+
* Returns the default if the input is not provided.
|
|
136
|
+
*/
|
|
137
|
+
readonly getBooleanOptional: (name: string, defaultValue: boolean) => Effect.Effect<boolean, ActionInputError>;
|
|
65
138
|
}
|
|
66
139
|
|
|
67
140
|
/**
|
|
@@ -241,6 +314,16 @@ export declare interface ActionOutputs {
|
|
|
241
314
|
* Add a directory to PATH for subsequent steps.
|
|
242
315
|
*/
|
|
243
316
|
readonly addPath: (path: string) => Effect.Effect<void>;
|
|
317
|
+
/**
|
|
318
|
+
* Mark the action as failed with a message.
|
|
319
|
+
* This is the standard way to signal action failure.
|
|
320
|
+
*/
|
|
321
|
+
readonly setFailed: (message: string) => Effect.Effect<void>;
|
|
322
|
+
/**
|
|
323
|
+
* Register a value as a secret so it is masked in logs.
|
|
324
|
+
* Use for values not read through ActionInputs (e.g., generated tokens).
|
|
325
|
+
*/
|
|
326
|
+
readonly setSecret: (value: string) => Effect.Effect<void>;
|
|
244
327
|
}
|
|
245
328
|
|
|
246
329
|
/**
|
|
@@ -280,12 +363,93 @@ export declare interface ActionOutputsTestState {
|
|
|
280
363
|
readonly summaries: Array<string>;
|
|
281
364
|
readonly variables: Array<CapturedOutput>;
|
|
282
365
|
readonly paths: Array<string>;
|
|
366
|
+
readonly secrets: Array<string>;
|
|
367
|
+
readonly failed: Array<string>;
|
|
283
368
|
}
|
|
284
369
|
|
|
285
370
|
/**
|
|
286
|
-
*
|
|
371
|
+
* Service interface for reading and writing GitHub Action state
|
|
372
|
+
* with schema-based serialization across action phases (pre/main/post).
|
|
373
|
+
*
|
|
374
|
+
* @public
|
|
287
375
|
*/
|
|
288
|
-
export declare
|
|
376
|
+
export declare interface ActionState {
|
|
377
|
+
/**
|
|
378
|
+
* Save a value to action state. Uses Schema.encode to serialize
|
|
379
|
+
* complex objects to JSON strings for storage.
|
|
380
|
+
*/
|
|
381
|
+
readonly save: <A, I>(key: string, value: A, schema: Schema.Schema<A, I, never>) => Effect.Effect<void, ActionStateError>;
|
|
382
|
+
/**
|
|
383
|
+
* Read a required state value. Uses Schema.decode to deserialize
|
|
384
|
+
* and validate the stored JSON string.
|
|
385
|
+
*/
|
|
386
|
+
readonly get: <A, I>(key: string, schema: Schema.Schema<A, I, never>) => Effect.Effect<A, ActionStateError>;
|
|
387
|
+
/**
|
|
388
|
+
* Read an optional state value. Returns Option.none() if the key
|
|
389
|
+
* has no stored value.
|
|
390
|
+
*/
|
|
391
|
+
readonly getOptional: <A, I>(key: string, schema: Schema.Schema<A, I, never>) => Effect.Effect<Option.Option<A>, ActionStateError>;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* ActionState tag for dependency injection.
|
|
396
|
+
*
|
|
397
|
+
* @public
|
|
398
|
+
*/
|
|
399
|
+
export declare const ActionState: Context.Tag<ActionState, ActionState>;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Error when GitHub Action state reading/writing fails.
|
|
403
|
+
*/
|
|
404
|
+
export declare class ActionStateError extends ActionStateErrorBase<{
|
|
405
|
+
/** The state key name. */
|
|
406
|
+
readonly key: string;
|
|
407
|
+
/** Human-readable description of what went wrong. */
|
|
408
|
+
readonly reason: string;
|
|
409
|
+
/** The raw string value received, if any. */
|
|
410
|
+
readonly rawValue: string | undefined;
|
|
411
|
+
}> {
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Base class for ActionStateError.
|
|
416
|
+
*
|
|
417
|
+
* @internal
|
|
418
|
+
*/
|
|
419
|
+
export declare const ActionStateErrorBase: new <A extends Record<string, any> = {}>(args: Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }) => YieldableError & {
|
|
420
|
+
readonly _tag: "ActionStateError";
|
|
421
|
+
} & Readonly<A>;
|
|
422
|
+
|
|
423
|
+
export declare const ActionStateLive: Layer.Layer<ActionState>;
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Test implementation that captures state in memory.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```ts
|
|
430
|
+
* const state = ActionStateTest.empty();
|
|
431
|
+
* const layer = ActionStateTest.layer(state);
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
export declare const ActionStateTest: {
|
|
435
|
+
/**
|
|
436
|
+
* Create a fresh empty test state container.
|
|
437
|
+
*/
|
|
438
|
+
readonly empty: () => ActionStateTestState;
|
|
439
|
+
/**
|
|
440
|
+
* Create a test layer from the given state.
|
|
441
|
+
* Pre-populate entries to simulate state from a previous phase.
|
|
442
|
+
*/
|
|
443
|
+
readonly layer: (state: ActionStateTestState) => Layer.Layer<ActionState, never, never>;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* In-memory state captured by the test state layer.
|
|
448
|
+
*/
|
|
449
|
+
export declare interface ActionStateTestState {
|
|
450
|
+
/** Stored state entries (key to JSON string). */
|
|
451
|
+
readonly entries: Map<string, string>;
|
|
452
|
+
}
|
|
289
453
|
|
|
290
454
|
/**
|
|
291
455
|
* A captured output entry.
|
|
@@ -297,14 +461,6 @@ export declare const CapturedOutput: Schema.Struct<{
|
|
|
297
461
|
|
|
298
462
|
export declare type CapturedOutput = typeof CapturedOutput.Type;
|
|
299
463
|
|
|
300
|
-
/**
|
|
301
|
-
* Build a checkbox checklist.
|
|
302
|
-
*/
|
|
303
|
-
export declare const checklist: (items: readonly {
|
|
304
|
-
readonly label: string;
|
|
305
|
-
readonly checked: boolean;
|
|
306
|
-
}[]) => string;
|
|
307
|
-
|
|
308
464
|
/**
|
|
309
465
|
* A single item in a checklist.
|
|
310
466
|
*/
|
|
@@ -315,15 +471,8 @@ export declare const ChecklistItem: Schema.Struct<{
|
|
|
315
471
|
|
|
316
472
|
export declare type ChecklistItem = typeof ChecklistItem.Type;
|
|
317
473
|
|
|
318
|
-
/**
|
|
319
|
-
|
|
320
|
-
*/
|
|
321
|
-
export declare const code: (text: string) => string;
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Fenced code block.
|
|
325
|
-
*/
|
|
326
|
-
export declare const codeBlock: (content: string, language?: string) => string;
|
|
474
|
+
/** Core services provided automatically by {@link Action.run}. */
|
|
475
|
+
export declare type CoreServices = ActionInputs | ActionLogger | ActionOutputs | PlatformNode.NodeContext.NodeContext;
|
|
327
476
|
|
|
328
477
|
/**
|
|
329
478
|
* FiberRef that holds the current action log level for the fiber.
|
|
@@ -331,26 +480,94 @@ export declare const codeBlock: (content: string, language?: string) => string;
|
|
|
331
480
|
export declare const CurrentLogLevel: FiberRef.FiberRef<ActionLogLevel>;
|
|
332
481
|
|
|
333
482
|
/**
|
|
334
|
-
*
|
|
335
|
-
*/
|
|
336
|
-
export declare const details: (summary: string, content: string) => string;
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Build a markdown heading.
|
|
483
|
+
* Namespace for GitHub-Flavored Markdown builder functions.
|
|
340
484
|
*
|
|
341
|
-
* @
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
*
|
|
485
|
+
* @example
|
|
486
|
+
* ```ts
|
|
487
|
+
* import { GithubMarkdown } from "@savvy-web/github-action-effects"
|
|
488
|
+
*
|
|
489
|
+
* GithubMarkdown.table(["Name", "Status"], [["build", "pass"]])
|
|
490
|
+
* GithubMarkdown.bold("hello")
|
|
491
|
+
* ```
|
|
492
|
+
*
|
|
493
|
+
* @public
|
|
347
494
|
*/
|
|
348
|
-
export declare const
|
|
495
|
+
export declare const GithubMarkdown: {
|
|
496
|
+
/**
|
|
497
|
+
* Build a GFM table from headers and rows.
|
|
498
|
+
*/
|
|
499
|
+
readonly table: (headers: readonly string[], rows: readonly (readonly string[])[]) => string;
|
|
500
|
+
/**
|
|
501
|
+
* Build a markdown heading.
|
|
502
|
+
*
|
|
503
|
+
* @param level - Heading level 1-6, defaults to 2.
|
|
504
|
+
*/
|
|
505
|
+
readonly heading: (text: string, level?: 1 | 2 | 3 | 4 | 5 | 6) => string;
|
|
506
|
+
/**
|
|
507
|
+
* Build a collapsible `<details>` block.
|
|
508
|
+
*/
|
|
509
|
+
readonly details: (summary: string, content: string) => string;
|
|
510
|
+
/**
|
|
511
|
+
* Horizontal rule.
|
|
512
|
+
*/
|
|
513
|
+
readonly rule: () => string;
|
|
514
|
+
/**
|
|
515
|
+
* Map a {@link Status} to its emoji indicator.
|
|
516
|
+
*/
|
|
517
|
+
readonly statusIcon: (status: "fail" | "pass" | "skip" | "warn") => string;
|
|
518
|
+
/**
|
|
519
|
+
* Build a markdown link.
|
|
520
|
+
*/
|
|
521
|
+
readonly link: (text: string, url: string) => string;
|
|
522
|
+
/**
|
|
523
|
+
* Build a bulleted list.
|
|
524
|
+
*/
|
|
525
|
+
readonly list: (items: readonly string[]) => string;
|
|
526
|
+
/**
|
|
527
|
+
* Build a checkbox checklist.
|
|
528
|
+
*/
|
|
529
|
+
readonly checklist: (items: readonly {
|
|
530
|
+
readonly label: string;
|
|
531
|
+
readonly checked: boolean;
|
|
532
|
+
}[]) => string;
|
|
533
|
+
/**
|
|
534
|
+
* Bold text.
|
|
535
|
+
*/
|
|
536
|
+
readonly bold: (text: string) => string;
|
|
537
|
+
/**
|
|
538
|
+
* Inline code.
|
|
539
|
+
*/
|
|
540
|
+
readonly code: (text: string) => string;
|
|
541
|
+
/**
|
|
542
|
+
* Fenced code block.
|
|
543
|
+
*/
|
|
544
|
+
readonly codeBlock: (content: string, language?: string) => string;
|
|
545
|
+
};
|
|
349
546
|
|
|
350
547
|
/**
|
|
351
|
-
*
|
|
548
|
+
* Configuration for a single input in {@link Action.parseInputs}.
|
|
549
|
+
*
|
|
550
|
+
* Precedence rules for how an input is read:
|
|
551
|
+
* 1. `json: true` — reads as JSON string, parses and validates (always required)
|
|
552
|
+
* 2. `multiline: true` — reads as newline-delimited list (always required)
|
|
553
|
+
* 3. `secret: true` — reads and masks the value (always required)
|
|
554
|
+
* 4. `default` is set — reads as optional, falls back to default if missing
|
|
555
|
+
* 5. `required: false` — reads as optional, returns undefined if missing
|
|
556
|
+
* 6. Otherwise — reads as required (default behavior)
|
|
557
|
+
*
|
|
558
|
+
* When `json`, `multiline`, or `secret` is set, the input is always
|
|
559
|
+
* treated as required regardless of `required` or `default` values.
|
|
560
|
+
*
|
|
561
|
+
* @public
|
|
352
562
|
*/
|
|
353
|
-
export declare
|
|
563
|
+
export declare interface InputConfig<S extends Schema.Schema.AnyNoContext = Schema.Schema.AnyNoContext> {
|
|
564
|
+
readonly schema: S;
|
|
565
|
+
readonly required?: boolean;
|
|
566
|
+
readonly default?: Schema.Schema.Type<S>;
|
|
567
|
+
readonly multiline?: boolean;
|
|
568
|
+
readonly secret?: boolean;
|
|
569
|
+
readonly json?: boolean;
|
|
570
|
+
}
|
|
354
571
|
|
|
355
572
|
/**
|
|
356
573
|
* Log level input values accepted by the standardized `log-level` action input.
|
|
@@ -361,30 +578,11 @@ export declare const LogLevelInput: Schema.Literal<["info", "verbose", "debug",
|
|
|
361
578
|
export declare type LogLevelInput = typeof LogLevelInput.Type;
|
|
362
579
|
|
|
363
580
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* - Always writes to `core.debug()` (GitHub-gated shadow channel).
|
|
367
|
-
* - Writes to user-facing output based on the action log level.
|
|
368
|
-
*/
|
|
369
|
-
export declare const makeActionLogger: () => Logger.Logger<unknown, void>;
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Resolve a {@link LogLevelInput} to a concrete {@link ActionLogLevel}.
|
|
373
|
-
*
|
|
374
|
-
* `"auto"` resolves to `"info"` unless `RUNNER_DEBUG` is `"1"`,
|
|
375
|
-
* in which case it resolves to `"debug"`.
|
|
581
|
+
* Infer the output type from an input config record.
|
|
376
582
|
*/
|
|
377
|
-
export declare
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
* Horizontal rule.
|
|
381
|
-
*/
|
|
382
|
-
export declare const rule: () => string;
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Set the action log level for the current scope.
|
|
386
|
-
*/
|
|
387
|
-
export declare const setLogLevel: (level: "debug" | "info" | "verbose") => Effect.Effect<void, never, never>;
|
|
583
|
+
export declare type ParsedInputs<T extends Record<string, InputConfig>> = {
|
|
584
|
+
readonly [K in keyof T]: T[K] extends InputConfig<infer S> ? Schema.Schema.Type<S> : never;
|
|
585
|
+
};
|
|
388
586
|
|
|
389
587
|
/**
|
|
390
588
|
* Status values for {@link statusIcon}.
|
|
@@ -393,21 +591,6 @@ export declare const Status: Schema.Literal<["pass", "fail", "skip", "warn"]>;
|
|
|
393
591
|
|
|
394
592
|
export declare type Status = typeof Status.Type;
|
|
395
593
|
|
|
396
|
-
/**
|
|
397
|
-
* Map a {@link Status} to its emoji indicator.
|
|
398
|
-
*/
|
|
399
|
-
export declare const statusIcon: (status: "fail" | "pass" | "skip" | "warn") => string;
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Build a GFM table from headers and rows.
|
|
403
|
-
*
|
|
404
|
-
* @example
|
|
405
|
-
* ```ts
|
|
406
|
-
* table(["Name", "Status"], [["build", "pass"], ["test", "fail"]])
|
|
407
|
-
* ```
|
|
408
|
-
*/
|
|
409
|
-
export declare const table: (headers: readonly string[], rows: readonly (readonly string[])[]) => string;
|
|
410
|
-
|
|
411
594
|
/**
|
|
412
595
|
* Annotation type captured by the test layer.
|
|
413
596
|
*/
|
package/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { addPath, debug, endGroup, error as core_error, exportVariable, getInput, getMultilineInput, getState, info, notice, saveState, setFailed, setOutput, setSecret, startGroup, summary as core_summary, warning } from "@actions/core";
|
|
2
|
+
import { NodeContext } from "@effect/platform-node";
|
|
3
|
+
import { Cause, Context, Data, Effect, FiberRef, FiberRefs, Layer, LogLevel, Logger, Option, Schema } from "effect";
|
|
3
4
|
const ActionInputErrorBase = Data.TaggedError("ActionInputError");
|
|
4
5
|
class ActionInputError extends ActionInputErrorBase {
|
|
5
6
|
}
|
|
6
|
-
const ActionOutputErrorBase = Data.TaggedError("ActionOutputError");
|
|
7
|
-
class ActionOutputError extends ActionOutputErrorBase {
|
|
8
|
-
}
|
|
9
7
|
const ActionInputs = Context.GenericTag("ActionInputs");
|
|
10
8
|
const decodeInput = (name, raw, schema)=>Schema.decode(schema)(raw).pipe(Effect.mapError((parseError)=>new ActionInputError({
|
|
11
9
|
inputName: name,
|
|
@@ -24,6 +22,16 @@ const decodeJsonInput = (name, raw, schema)=>Effect["try"]({
|
|
|
24
22
|
reason: `Input "${name}" JSON validation failed: ${parseError.message}`,
|
|
25
23
|
rawValue: raw
|
|
26
24
|
})))));
|
|
25
|
+
const parseBoolean = (name, raw)=>{
|
|
26
|
+
const lower = raw.toLowerCase().trim();
|
|
27
|
+
if ("true" === lower) return Effect.succeed(true);
|
|
28
|
+
if ("false" === lower) return Effect.succeed(false);
|
|
29
|
+
return Effect.fail(new ActionInputError({
|
|
30
|
+
inputName: name,
|
|
31
|
+
reason: `Input "${name}" is not a valid boolean: expected "true" or "false", got "${raw}"`,
|
|
32
|
+
rawValue: raw
|
|
33
|
+
}));
|
|
34
|
+
};
|
|
27
35
|
const ActionInputsLive = Layer.succeed(ActionInputs, {
|
|
28
36
|
get: (name, schema)=>Effect.sync(()=>getInput(name, {
|
|
29
37
|
required: true
|
|
@@ -43,37 +51,20 @@ const ActionInputsLive = Layer.succeed(ActionInputs, {
|
|
|
43
51
|
}).pipe(Effect.flatMap((raw)=>decodeInput(name, raw, schema))),
|
|
44
52
|
getJson: (name, schema)=>Effect.sync(()=>getInput(name, {
|
|
45
53
|
required: true
|
|
46
|
-
})).pipe(Effect.flatMap((raw)=>decodeJsonInput(name, raw, schema)))
|
|
54
|
+
})).pipe(Effect.flatMap((raw)=>decodeJsonInput(name, raw, schema))),
|
|
55
|
+
getMultiline: (name, itemSchema)=>Effect.sync(()=>getMultilineInput(name, {
|
|
56
|
+
required: true
|
|
57
|
+
})).pipe(Effect.map((lines)=>lines.map((l)=>l.trim()).filter((l)=>l.length > 0 && !l.startsWith("#"))), Effect.flatMap((lines)=>Effect.forEach(lines, (line)=>decodeInput(name, line, itemSchema)))),
|
|
58
|
+
getBoolean: (name)=>Effect.sync(()=>getInput(name, {
|
|
59
|
+
required: true
|
|
60
|
+
})).pipe(Effect.flatMap((raw)=>parseBoolean(name, raw))),
|
|
61
|
+
getBooleanOptional: (name, defaultValue)=>Effect.sync(()=>getInput(name, {
|
|
62
|
+
required: false
|
|
63
|
+
})).pipe(Effect.flatMap((raw)=>{
|
|
64
|
+
if ("" === raw) return Effect.succeed(defaultValue);
|
|
65
|
+
return parseBoolean(name, raw);
|
|
66
|
+
}))
|
|
47
67
|
});
|
|
48
|
-
const missingInput = (name)=>new ActionInputError({
|
|
49
|
-
inputName: name,
|
|
50
|
-
reason: `Input "${name}" is required but not provided`,
|
|
51
|
-
rawValue: void 0
|
|
52
|
-
});
|
|
53
|
-
const ActionInputsTest = {
|
|
54
|
-
layer: (inputs)=>Layer.succeed(ActionInputs, {
|
|
55
|
-
get: (name, schema)=>{
|
|
56
|
-
const raw = inputs[name];
|
|
57
|
-
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
58
|
-
return decodeInput(name, raw, schema);
|
|
59
|
-
},
|
|
60
|
-
getOptional: (name, schema)=>{
|
|
61
|
-
const raw = inputs[name];
|
|
62
|
-
if (void 0 === raw || "" === raw) return Effect.succeed(Option.none());
|
|
63
|
-
return decodeInput(name, raw, schema).pipe(Effect.map((a)=>Option.some(a)));
|
|
64
|
-
},
|
|
65
|
-
getSecret: (name, schema)=>{
|
|
66
|
-
const raw = inputs[name];
|
|
67
|
-
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
68
|
-
return decodeInput(name, raw, schema);
|
|
69
|
-
},
|
|
70
|
-
getJson: (name, schema)=>{
|
|
71
|
-
const raw = inputs[name];
|
|
72
|
-
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
73
|
-
return decodeJsonInput(name, raw, schema);
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
};
|
|
77
68
|
const ActionLogger = Context.GenericTag("ActionLogger");
|
|
78
69
|
const CurrentLogLevel = FiberRef.unsafeMake("info");
|
|
79
70
|
const setLogLevel = (level)=>FiberRef.set(CurrentLogLevel, level);
|
|
@@ -131,6 +122,136 @@ const ActionLoggerLive = Layer.succeed(ActionLogger, {
|
|
|
131
122
|
void 0 !== properties ? notice(message, properties) : notice(message);
|
|
132
123
|
})
|
|
133
124
|
});
|
|
125
|
+
const ActionOutputErrorBase = Data.TaggedError("ActionOutputError");
|
|
126
|
+
class ActionOutputError extends ActionOutputErrorBase {
|
|
127
|
+
}
|
|
128
|
+
const ActionOutputs = Context.GenericTag("ActionOutputs");
|
|
129
|
+
const ActionOutputsLive = Layer.succeed(ActionOutputs, {
|
|
130
|
+
set: (name, value)=>Effect.sync(()=>setOutput(name, value)),
|
|
131
|
+
setJson: (name, value, schema)=>Schema.encode(schema)(value).pipe(Effect.tap((encoded)=>Effect.sync(()=>setOutput(name, JSON.stringify(encoded)))), Effect.asVoid, Effect.mapError((parseError)=>new ActionOutputError({
|
|
132
|
+
outputName: name,
|
|
133
|
+
reason: `Output "${name}" validation failed: ${parseError.message}`
|
|
134
|
+
}))),
|
|
135
|
+
summary: (content)=>Effect.tryPromise({
|
|
136
|
+
try: ()=>core_summary.addRaw(content).write(),
|
|
137
|
+
catch: (error)=>new ActionOutputError({
|
|
138
|
+
outputName: "summary",
|
|
139
|
+
reason: `Failed to write step summary: ${error instanceof Error ? error.message : String(error)}`
|
|
140
|
+
})
|
|
141
|
+
}).pipe(Effect.asVoid),
|
|
142
|
+
exportVariable: (name, value)=>Effect.sync(()=>exportVariable(name, value)),
|
|
143
|
+
addPath: (path)=>Effect.sync(()=>addPath(path)),
|
|
144
|
+
setFailed: (message)=>Effect.sync(()=>setFailed(message)),
|
|
145
|
+
setSecret: (value)=>Effect.sync(()=>setSecret(value))
|
|
146
|
+
});
|
|
147
|
+
const ActionLogLevel = Schema.Literal("info", "verbose", "debug").annotations({
|
|
148
|
+
identifier: "ActionLogLevel",
|
|
149
|
+
title: "Action Log Level",
|
|
150
|
+
description: "Logging verbosity for GitHub Action output"
|
|
151
|
+
});
|
|
152
|
+
const LogLevelInput = Schema.Literal("info", "verbose", "debug", "auto").annotations({
|
|
153
|
+
identifier: "LogLevelInput",
|
|
154
|
+
title: "Log Level Input",
|
|
155
|
+
description: "Logging verbosity: info, verbose, debug, or auto",
|
|
156
|
+
message: ()=>({
|
|
157
|
+
message: 'log-level must be one of: "info", "verbose", "debug", "auto"',
|
|
158
|
+
override: true
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
const resolveLogLevel = (input)=>{
|
|
162
|
+
if ("auto" !== input) return input;
|
|
163
|
+
return "1" === process.env.RUNNER_DEBUG ? "debug" : "info";
|
|
164
|
+
};
|
|
165
|
+
const CoreLive = Layer.mergeAll(ActionInputsLive, ActionLoggerLive, ActionOutputsLive, NodeContext.layer);
|
|
166
|
+
const Action = {
|
|
167
|
+
run: (program, layer)=>{
|
|
168
|
+
const fullLayer = layer ? Layer.mergeAll(CoreLive, layer) : CoreLive;
|
|
169
|
+
const runnable = program.pipe(Effect.provide(fullLayer), Effect.provide(ActionLoggerLayer), Effect.catchAllCause((cause)=>{
|
|
170
|
+
const message = Cause.pretty(cause);
|
|
171
|
+
return Effect.sync(()=>setFailed(`Action failed: ${message}`));
|
|
172
|
+
}));
|
|
173
|
+
return Effect.runPromise(runnable).catch(()=>{
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
parseInputs: (config, crossValidate)=>Effect.flatMap(ActionInputs, (svc)=>{
|
|
178
|
+
const entries = Object.entries(config);
|
|
179
|
+
return Effect.forEach(entries, ([name, cfg])=>{
|
|
180
|
+
const { schema, json, multiline, secret } = cfg;
|
|
181
|
+
const isOptional = false === cfg.required || void 0 !== cfg.default;
|
|
182
|
+
let readEffect;
|
|
183
|
+
readEffect = json ? svc.getJson(name, schema) : multiline ? svc.getMultiline(name, schema) : secret ? svc.getSecret(name, schema) : isOptional ? svc.getOptional(name, schema).pipe(Effect.map((opt)=>{
|
|
184
|
+
if ("None" === opt._tag) return cfg.default;
|
|
185
|
+
return opt.value;
|
|
186
|
+
})) : svc.get(name, schema);
|
|
187
|
+
return Effect.map(readEffect, (value)=>[
|
|
188
|
+
name,
|
|
189
|
+
value
|
|
190
|
+
]);
|
|
191
|
+
}).pipe(Effect.map((pairs)=>Object.fromEntries(pairs)), Effect.flatMap((parsed)=>crossValidate ? crossValidate(parsed) : Effect.succeed(parsed)));
|
|
192
|
+
}),
|
|
193
|
+
makeLogger: makeActionLogger,
|
|
194
|
+
setLogLevel: setLogLevel,
|
|
195
|
+
resolveLogLevel: resolveLogLevel
|
|
196
|
+
};
|
|
197
|
+
const ActionStateErrorBase = Data.TaggedError("ActionStateError");
|
|
198
|
+
class ActionStateError extends ActionStateErrorBase {
|
|
199
|
+
}
|
|
200
|
+
const missingInput = (name)=>new ActionInputError({
|
|
201
|
+
inputName: name,
|
|
202
|
+
reason: `Input "${name}" is required but not provided`,
|
|
203
|
+
rawValue: void 0
|
|
204
|
+
});
|
|
205
|
+
const ActionInputsTest_parseBoolean = (name, raw)=>{
|
|
206
|
+
const lower = raw.toLowerCase().trim();
|
|
207
|
+
if ("true" === lower) return Effect.succeed(true);
|
|
208
|
+
if ("false" === lower) return Effect.succeed(false);
|
|
209
|
+
return Effect.fail(new ActionInputError({
|
|
210
|
+
inputName: name,
|
|
211
|
+
reason: `Input "${name}" is not a valid boolean: expected "true" or "false", got "${raw}"`,
|
|
212
|
+
rawValue: raw
|
|
213
|
+
}));
|
|
214
|
+
};
|
|
215
|
+
const ActionInputsTest = {
|
|
216
|
+
layer: (inputs)=>Layer.succeed(ActionInputs, {
|
|
217
|
+
get: (name, schema)=>{
|
|
218
|
+
const raw = inputs[name];
|
|
219
|
+
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
220
|
+
return decodeInput(name, raw, schema);
|
|
221
|
+
},
|
|
222
|
+
getOptional: (name, schema)=>{
|
|
223
|
+
const raw = inputs[name];
|
|
224
|
+
if (void 0 === raw || "" === raw) return Effect.succeed(Option.none());
|
|
225
|
+
return decodeInput(name, raw, schema).pipe(Effect.map((a)=>Option.some(a)));
|
|
226
|
+
},
|
|
227
|
+
getSecret: (name, schema)=>{
|
|
228
|
+
const raw = inputs[name];
|
|
229
|
+
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
230
|
+
return decodeInput(name, raw, schema);
|
|
231
|
+
},
|
|
232
|
+
getJson: (name, schema)=>{
|
|
233
|
+
const raw = inputs[name];
|
|
234
|
+
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
235
|
+
return decodeJsonInput(name, raw, schema);
|
|
236
|
+
},
|
|
237
|
+
getMultiline: (name, itemSchema)=>{
|
|
238
|
+
const raw = inputs[name];
|
|
239
|
+
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
240
|
+
const lines = raw.split("\n").map((l)=>l.trim()).filter((l)=>l.length > 0 && !l.startsWith("#"));
|
|
241
|
+
return Effect.forEach(lines, (line)=>decodeInput(name, line, itemSchema));
|
|
242
|
+
},
|
|
243
|
+
getBoolean: (name)=>{
|
|
244
|
+
const raw = inputs[name];
|
|
245
|
+
if (void 0 === raw) return Effect.fail(missingInput(name));
|
|
246
|
+
return ActionInputsTest_parseBoolean(name, raw);
|
|
247
|
+
},
|
|
248
|
+
getBooleanOptional: (name, defaultValue)=>{
|
|
249
|
+
const raw = inputs[name];
|
|
250
|
+
if (void 0 === raw || "" === raw) return Effect.succeed(defaultValue);
|
|
251
|
+
return ActionInputsTest_parseBoolean(name, raw);
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
};
|
|
134
255
|
const makeAnnotation = (state, type)=>(message, properties)=>Effect.sync(()=>{
|
|
135
256
|
state.annotations.push({
|
|
136
257
|
type,
|
|
@@ -170,29 +291,14 @@ const ActionLoggerTest = {
|
|
|
170
291
|
annotationNotice: makeAnnotation(state, "notice")
|
|
171
292
|
})
|
|
172
293
|
};
|
|
173
|
-
const ActionOutputs = Context.GenericTag("ActionOutputs");
|
|
174
|
-
const ActionOutputsLive = Layer.succeed(ActionOutputs, {
|
|
175
|
-
set: (name, value)=>Effect.sync(()=>setOutput(name, value)),
|
|
176
|
-
setJson: (name, value, schema)=>Schema.encode(schema)(value).pipe(Effect.tap((encoded)=>Effect.sync(()=>setOutput(name, JSON.stringify(encoded)))), Effect.asVoid, Effect.mapError((parseError)=>new ActionOutputError({
|
|
177
|
-
outputName: name,
|
|
178
|
-
reason: `Output "${name}" validation failed: ${parseError.message}`
|
|
179
|
-
}))),
|
|
180
|
-
summary: (content)=>Effect.tryPromise({
|
|
181
|
-
try: ()=>core_summary.addRaw(content).write(),
|
|
182
|
-
catch: (error)=>new ActionOutputError({
|
|
183
|
-
outputName: "summary",
|
|
184
|
-
reason: `Failed to write step summary: ${error instanceof Error ? error.message : String(error)}`
|
|
185
|
-
})
|
|
186
|
-
}).pipe(Effect.asVoid),
|
|
187
|
-
exportVariable: (name, value)=>Effect.sync(()=>exportVariable(name, value)),
|
|
188
|
-
addPath: (path)=>Effect.sync(()=>addPath(path))
|
|
189
|
-
});
|
|
190
294
|
const ActionOutputsTest = {
|
|
191
295
|
empty: ()=>({
|
|
192
296
|
outputs: [],
|
|
193
297
|
summaries: [],
|
|
194
298
|
variables: [],
|
|
195
|
-
paths: []
|
|
299
|
+
paths: [],
|
|
300
|
+
secrets: [],
|
|
301
|
+
failed: []
|
|
196
302
|
}),
|
|
197
303
|
layer: (state)=>Layer.succeed(ActionOutputs, {
|
|
198
304
|
set: (name, value)=>Effect.sync(()=>{
|
|
@@ -221,9 +327,72 @@ const ActionOutputsTest = {
|
|
|
221
327
|
}),
|
|
222
328
|
addPath: (path)=>Effect.sync(()=>{
|
|
223
329
|
state.paths.push(path);
|
|
330
|
+
}),
|
|
331
|
+
setFailed: (message)=>Effect.sync(()=>{
|
|
332
|
+
state.failed.push(message);
|
|
333
|
+
}),
|
|
334
|
+
setSecret: (value)=>Effect.sync(()=>{
|
|
335
|
+
state.secrets.push(value);
|
|
224
336
|
})
|
|
225
337
|
})
|
|
226
338
|
};
|
|
339
|
+
const ActionState = Context.GenericTag("ActionState");
|
|
340
|
+
const encodeState = (key, value, schema)=>Schema.encode(schema)(value).pipe(Effect.map((encoded)=>JSON.stringify(encoded)), Effect.mapError((error)=>new ActionStateError({
|
|
341
|
+
key,
|
|
342
|
+
reason: `State "${key}" encode failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
343
|
+
rawValue: void 0
|
|
344
|
+
})));
|
|
345
|
+
const decodeState = (key, raw, schema)=>Effect["try"]({
|
|
346
|
+
try: ()=>JSON.parse(raw),
|
|
347
|
+
catch: (error)=>new ActionStateError({
|
|
348
|
+
key,
|
|
349
|
+
reason: `State "${key}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
350
|
+
rawValue: raw
|
|
351
|
+
})
|
|
352
|
+
}).pipe(Effect.flatMap((parsed)=>Schema.decode(schema)(parsed).pipe(Effect.mapError((parseError)=>new ActionStateError({
|
|
353
|
+
key,
|
|
354
|
+
reason: `State "${key}" decode failed: ${parseError.message}`,
|
|
355
|
+
rawValue: raw
|
|
356
|
+
})))));
|
|
357
|
+
const ActionStateLive = Layer.succeed(ActionState, {
|
|
358
|
+
save: (key, value, schema)=>encodeState(key, value, schema).pipe(Effect.tap((json)=>Effect.sync(()=>saveState(key, json))), Effect.asVoid),
|
|
359
|
+
get: (key, schema)=>Effect.sync(()=>getState(key)).pipe(Effect.flatMap((raw)=>{
|
|
360
|
+
if ("" === raw) return Effect.fail(new ActionStateError({
|
|
361
|
+
key,
|
|
362
|
+
reason: `State "${key}" is not set (phase ordering issue?)`,
|
|
363
|
+
rawValue: void 0
|
|
364
|
+
}));
|
|
365
|
+
return decodeState(key, raw, schema);
|
|
366
|
+
})),
|
|
367
|
+
getOptional: (key, schema)=>Effect.sync(()=>getState(key)).pipe(Effect.flatMap((raw)=>{
|
|
368
|
+
if ("" === raw) return Effect.succeed(Option.none());
|
|
369
|
+
return decodeState(key, raw, schema).pipe(Effect.map((a)=>Option.some(a)));
|
|
370
|
+
}))
|
|
371
|
+
});
|
|
372
|
+
const ActionStateTest = {
|
|
373
|
+
empty: ()=>({
|
|
374
|
+
entries: new Map()
|
|
375
|
+
}),
|
|
376
|
+
layer: (state)=>Layer.succeed(ActionState, {
|
|
377
|
+
save: (key, value, schema)=>encodeState(key, value, schema).pipe(Effect.tap((json)=>Effect.sync(()=>{
|
|
378
|
+
state.entries.set(key, json);
|
|
379
|
+
})), Effect.asVoid),
|
|
380
|
+
get: (key, schema)=>{
|
|
381
|
+
const raw = state.entries.get(key);
|
|
382
|
+
if (void 0 === raw) return Effect.fail(new ActionStateError({
|
|
383
|
+
key,
|
|
384
|
+
reason: `State "${key}" is not set (phase ordering issue?)`,
|
|
385
|
+
rawValue: void 0
|
|
386
|
+
}));
|
|
387
|
+
return decodeState(key, raw, schema);
|
|
388
|
+
},
|
|
389
|
+
getOptional: (key, schema)=>{
|
|
390
|
+
const raw = state.entries.get(key);
|
|
391
|
+
if (void 0 === raw) return Effect.succeed(Option.none());
|
|
392
|
+
return decodeState(key, raw, schema).pipe(Effect.map((a)=>Option.some(a)));
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
};
|
|
227
396
|
const Status = Schema.Literal("pass", "fail", "skip", "warn").annotations({
|
|
228
397
|
identifier: "Status",
|
|
229
398
|
title: "Check Status",
|
|
@@ -243,53 +412,37 @@ const CapturedOutput = Schema.Struct({
|
|
|
243
412
|
identifier: "CapturedOutput",
|
|
244
413
|
title: "Captured Output"
|
|
245
414
|
});
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
]
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
switch(status){
|
|
279
|
-
case "pass":
|
|
280
|
-
return "\u2705";
|
|
281
|
-
case "fail":
|
|
282
|
-
return "\u274C";
|
|
283
|
-
case "skip":
|
|
284
|
-
return "\uD83D\uDDC3\uFE0F";
|
|
285
|
-
case "warn":
|
|
286
|
-
return "\u26A0\uFE0F";
|
|
287
|
-
}
|
|
415
|
+
const GithubMarkdown = {
|
|
416
|
+
table: (headers, rows)=>{
|
|
417
|
+
const headerRow = `| ${headers.join(" | ")} |`;
|
|
418
|
+
const separator = `| ${headers.map(()=>"---").join(" | ")} |`;
|
|
419
|
+
const dataRows = rows.map((row)=>`| ${row.join(" | ")} |`);
|
|
420
|
+
return [
|
|
421
|
+
headerRow,
|
|
422
|
+
separator,
|
|
423
|
+
...dataRows
|
|
424
|
+
].join("\n");
|
|
425
|
+
},
|
|
426
|
+
heading: (text, level = 2)=>`${"#".repeat(level)} ${text}`,
|
|
427
|
+
details: (summary, content)=>`<details>\n<summary>${summary}</summary>\n\n${content}\n\n</details>`,
|
|
428
|
+
rule: ()=>"---",
|
|
429
|
+
statusIcon: (status)=>{
|
|
430
|
+
switch(status){
|
|
431
|
+
case "pass":
|
|
432
|
+
return "\u2705";
|
|
433
|
+
case "fail":
|
|
434
|
+
return "\u274C";
|
|
435
|
+
case "skip":
|
|
436
|
+
return "\uD83D\uDDC3\uFE0F";
|
|
437
|
+
case "warn":
|
|
438
|
+
return "\u26A0\uFE0F";
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
link: (text, url)=>`[${text}](${url})`,
|
|
442
|
+
list: (items)=>items.map((item)=>`- ${item}`).join("\n"),
|
|
443
|
+
checklist: (items)=>items.map((item)=>`- [${item.checked ? "x" : " "}] ${item.label}`).join("\n"),
|
|
444
|
+
bold: (text)=>`**${text}**`,
|
|
445
|
+
code: (text)=>`\`${text}\``,
|
|
446
|
+
codeBlock: (content, language = "")=>`\`\`\`${language}\n${content}\n\`\`\``
|
|
288
447
|
};
|
|
289
|
-
|
|
290
|
-
const list = (items)=>items.map((item)=>`- ${item}`).join("\n");
|
|
291
|
-
const checklist = (items)=>items.map((item)=>`- [${item.checked ? "x" : " "}] ${item.label}`).join("\n");
|
|
292
|
-
const bold = (text)=>`**${text}**`;
|
|
293
|
-
const code = (text)=>`\`${text}\``;
|
|
294
|
-
const codeBlock = (content, language = "")=>`\`\`\`${language}\n${content}\n\`\`\``;
|
|
295
|
-
export { ActionInputError, ActionInputErrorBase, ActionInputs, ActionInputsLive, ActionInputsTest, ActionLogLevel, ActionLogger, ActionLoggerLayer, ActionLoggerLive, ActionLoggerTest, ActionOutputError, ActionOutputErrorBase, ActionOutputs, ActionOutputsLive, ActionOutputsTest, CapturedOutput, ChecklistItem, CurrentLogLevel, GithubMarkdown_link as link, LogLevelInput, Status, bold, checklist, code, codeBlock, details, heading, list, makeActionLogger, resolveLogLevel, rule, setLogLevel, statusIcon, table };
|
|
448
|
+
export { Action, ActionInputError, ActionInputErrorBase, ActionInputs, ActionInputsLive, ActionInputsTest, ActionLogLevel, ActionLogger, ActionLoggerLayer, ActionLoggerLive, ActionLoggerTest, ActionOutputError, ActionOutputErrorBase, ActionOutputs, ActionOutputsLive, ActionOutputsTest, ActionState, ActionStateError, ActionStateErrorBase, ActionStateLive, ActionStateTest, CapturedOutput, ChecklistItem, CurrentLogLevel, GithubMarkdown, LogLevelInput, Status };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@savvy-web/github-action-effects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Effect-based utility library for building robust, well-logged, and schema-validated GitHub Actions.",
|
|
6
6
|
"homepage": "https://github.com/savvy-web/github-action-effects#readme",
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"optional": true
|
|
44
44
|
},
|
|
45
45
|
"@effect/platform": {
|
|
46
|
-
"optional":
|
|
46
|
+
"optional": false
|
|
47
47
|
},
|
|
48
48
|
"@effect/platform-node": {
|
|
49
|
-
"optional":
|
|
49
|
+
"optional": false
|
|
50
50
|
},
|
|
51
51
|
"effect": {
|
|
52
52
|
"optional": false
|