@savvy-web/github-action-effects 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Savvy Web Strategy, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @savvy-web/github-action-effects
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@savvy-web/github-action-effects)](https://www.npmjs.com/package/@savvy-web/github-action-effects)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Build GitHub Actions with [Effect](https://effect.website) -- schema-validated
7
+ inputs, structured logging with buffered output, and type-safe outputs through
8
+ composable service layers.
9
+
10
+ ## Features
11
+
12
+ - **Schema-validated inputs and outputs** using Effect Schema
13
+ - **Three-tier action logger** (info/verbose/debug) with automatic log buffering
14
+ - **GitHub Flavored Markdown builders** for step summaries
15
+ - **Test layers for every service** -- no mocking `@actions/core` required
16
+ - **Full TypeScript** with strict mode and ESM
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @savvy-web/github-action-effects effect @actions/core
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { Effect, Layer, Schema } from "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";
40
+
41
+ const program = Effect.gen(function* () {
42
+ const inputs = yield* ActionInputs;
43
+ const outputs = yield* ActionOutputs;
44
+
45
+ const level = yield* inputs.get("log-level", LogLevelInput);
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]]));
51
+ });
52
+
53
+ const MainLive = Layer.mergeAll(
54
+ ActionInputsLive,
55
+ ActionOutputsLive,
56
+ ActionLoggerLive,
57
+ );
58
+
59
+ program.pipe(
60
+ Effect.provide(ActionLoggerLayer),
61
+ Effect.provide(MainLive),
62
+ Effect.runPromise,
63
+ );
64
+ ```
65
+
66
+ ## Documentation
67
+
68
+ For architecture, API reference, testing guides, and advanced usage, see
69
+ [docs](./docs/).
70
+
71
+ ## License
72
+
73
+ [MIT](LICENSE)
package/index.d.ts ADDED
@@ -0,0 +1,416 @@
1
+ /**
2
+ * \@savvy-web/github-action-effects
3
+ *
4
+ * Effect-based utility library for building robust, well-logged,
5
+ * and schema-validated GitHub Actions.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { AnnotationProperties } from '@actions/core';
11
+ import { Context } from 'effect';
12
+ import { Effect } from 'effect';
13
+ import { Equals } from 'effect/Types';
14
+ import { FiberRef } from 'effect';
15
+ import { Layer } from 'effect';
16
+ import { Logger } from 'effect';
17
+ import type { Option } from 'effect';
18
+ import { Schema } from 'effect';
19
+ import { YieldableError } from 'effect/Cause';
20
+
21
+ /**
22
+ * Error when a GitHub Action input is missing or fails schema validation.
23
+ */
24
+ export declare class ActionInputError extends ActionInputErrorBase<{
25
+ /** The input name from action.yml. */
26
+ readonly inputName: string;
27
+ /** Human-readable description of what went wrong. */
28
+ readonly reason: string;
29
+ /** The raw string value received, if any. */
30
+ readonly rawValue: string | undefined;
31
+ }> {
32
+ }
33
+
34
+ /**
35
+ * Base class for ActionInputError.
36
+ *
37
+ * @internal
38
+ */
39
+ export declare const ActionInputErrorBase: 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 & {
40
+ readonly _tag: "ActionInputError";
41
+ } & Readonly<A>;
42
+
43
+ /**
44
+ * Service interface for reading GitHub Action inputs with schema validation.
45
+ *
46
+ * @public
47
+ */
48
+ export declare interface ActionInputs {
49
+ /**
50
+ * Read a required input and validate it against a schema.
51
+ */
52
+ readonly get: <A, I>(name: string, schema: Schema.Schema<A, I, never>) => Effect.Effect<A, ActionInputError>;
53
+ /**
54
+ * Read an optional input. Returns `Option.none()` if empty.
55
+ */
56
+ readonly getOptional: <A, I>(name: string, schema: Schema.Schema<A, I, never>) => Effect.Effect<Option.Option<A>, ActionInputError>;
57
+ /**
58
+ * Read a required input and mask it as a secret in logs.
59
+ */
60
+ readonly getSecret: <A, I>(name: string, schema: Schema.Schema<A, I, never>) => Effect.Effect<A, ActionInputError>;
61
+ /**
62
+ * Read a required input as a JSON string, parse and validate it.
63
+ */
64
+ readonly getJson: <A, I>(name: string, schema: Schema.Schema<A, I, never>) => Effect.Effect<A, ActionInputError>;
65
+ }
66
+
67
+ /**
68
+ * ActionInputs tag for dependency injection.
69
+ *
70
+ * @public
71
+ */
72
+ export declare const ActionInputs: Context.Tag<ActionInputs, ActionInputs>;
73
+
74
+ export declare const ActionInputsLive: Layer.Layer<ActionInputs>;
75
+
76
+ /**
77
+ * Test implementation that reads from a provided record.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const layer = ActionInputsTest.layer({ "package-name": "my-pkg" });
82
+ * ```
83
+ */
84
+ export declare const ActionInputsTest: {
85
+ readonly layer: (inputs: Record<string, string>) => Layer.Layer<ActionInputs, never, never>;
86
+ };
87
+
88
+ /**
89
+ * Service interface for action-specific logging operations beyond the Effect Logger.
90
+ *
91
+ * @remarks
92
+ * The core log-level routing is handled by the Effect Logger installed
93
+ * via {@link ActionLoggerLayer}. This service provides additional
94
+ * GitHub Actions-specific operations like log groups and buffering.
95
+ *
96
+ * @public
97
+ */
98
+ export declare interface ActionLogger {
99
+ /**
100
+ * Run an effect inside a collapsible log group.
101
+ */
102
+ readonly group: <A, E, R>(name: string, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
103
+ /**
104
+ * Run an effect with buffered logging. At `info` level, verbose output
105
+ * is captured in memory. On success the buffer is discarded. On failure
106
+ * the buffer is flushed before the error is reported.
107
+ */
108
+ readonly withBuffer: <A, E, R>(label: string, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
109
+ /**
110
+ * Emit an error annotation (red, blocks PR checks).
111
+ */
112
+ readonly annotationError: (message: string, properties?: AnnotationProperties) => Effect.Effect<void>;
113
+ /**
114
+ * Emit a warning annotation (yellow, informational).
115
+ */
116
+ readonly annotationWarning: (message: string, properties?: AnnotationProperties) => Effect.Effect<void>;
117
+ /**
118
+ * Emit a notice annotation (blue, informational).
119
+ */
120
+ readonly annotationNotice: (message: string, properties?: AnnotationProperties) => Effect.Effect<void>;
121
+ }
122
+
123
+ /**
124
+ * ActionLogger tag for dependency injection.
125
+ *
126
+ * @public
127
+ */
128
+ export declare const ActionLogger: Context.Tag<ActionLogger, ActionLogger>;
129
+
130
+ /**
131
+ * Layer that installs the GitHub Actions logger as the default Effect logger.
132
+ */
133
+ export declare const ActionLoggerLayer: Layer.Layer<never>;
134
+
135
+ /**
136
+ * Live implementation of the ActionLogger service.
137
+ */
138
+ export declare const ActionLoggerLive: Layer.Layer<ActionLogger>;
139
+
140
+ /**
141
+ * Test implementation that captures log operations in memory.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const state = ActionLoggerTest.empty();
146
+ * const layer = ActionLoggerTest.layer(state);
147
+ * ```
148
+ */
149
+ export declare const ActionLoggerTest: {
150
+ /**
151
+ * Create a fresh empty test state container.
152
+ */
153
+ readonly empty: () => ActionLoggerTestState;
154
+ /**
155
+ * Create a test layer from the given state.
156
+ */
157
+ readonly layer: (state: ActionLoggerTestState) => Layer.Layer<ActionLogger, never, never>;
158
+ };
159
+
160
+ /**
161
+ * In-memory state captured by the test logger.
162
+ */
163
+ export declare interface ActionLoggerTestState {
164
+ readonly entries: Array<{
165
+ readonly level: string;
166
+ readonly message: string;
167
+ }>;
168
+ readonly groups: Array<{
169
+ readonly name: string;
170
+ readonly entries: Array<{
171
+ readonly level: string;
172
+ readonly message: string;
173
+ }>;
174
+ }>;
175
+ readonly annotations: Array<{
176
+ readonly type: TestAnnotationType;
177
+ readonly message: string;
178
+ readonly properties?: AnnotationProperties;
179
+ }>;
180
+ readonly flushedBuffers: Array<{
181
+ readonly label: string;
182
+ readonly entries: Array<string>;
183
+ }>;
184
+ }
185
+
186
+ /**
187
+ * The three log levels supported by the action logger.
188
+ *
189
+ * - `info` — Buffered. Shows only outcome summaries. Flushes verbose buffer on failure.
190
+ * - `verbose` — Unbuffered milestones. Start/finish markers for operations.
191
+ * - `debug` — Everything. Full command output, input/output values, internal state.
192
+ */
193
+ export declare const ActionLogLevel: Schema.Literal<["info", "verbose", "debug"]>;
194
+
195
+ export declare type ActionLogLevel = typeof ActionLogLevel.Type;
196
+
197
+ /**
198
+ * Error when a GitHub Action output fails schema validation or writing.
199
+ */
200
+ export declare class ActionOutputError extends ActionOutputErrorBase<{
201
+ /** The output name. */
202
+ readonly outputName: string;
203
+ /** Human-readable description of what went wrong. */
204
+ readonly reason: string;
205
+ }> {
206
+ }
207
+
208
+ /**
209
+ * Base class for ActionOutputError.
210
+ *
211
+ * @internal
212
+ */
213
+ export declare const ActionOutputErrorBase: 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 & {
214
+ readonly _tag: "ActionOutputError";
215
+ } & Readonly<A>;
216
+
217
+ /**
218
+ * Service interface for setting GitHub Action outputs with schema validation.
219
+ *
220
+ * @public
221
+ */
222
+ export declare interface ActionOutputs {
223
+ /**
224
+ * Set a string output value.
225
+ */
226
+ readonly set: (name: string, value: string) => Effect.Effect<void>;
227
+ /**
228
+ * Serialize a value as JSON and set it as an output.
229
+ * Validates against the schema before serializing.
230
+ */
231
+ readonly setJson: <A, I>(name: string, value: A, schema: Schema.Schema<A, I, never>) => Effect.Effect<void, ActionOutputError>;
232
+ /**
233
+ * Write markdown content to the step summary.
234
+ */
235
+ readonly summary: (content: string) => Effect.Effect<void, ActionOutputError>;
236
+ /**
237
+ * Export an environment variable for subsequent steps.
238
+ */
239
+ readonly exportVariable: (name: string, value: string) => Effect.Effect<void>;
240
+ /**
241
+ * Add a directory to PATH for subsequent steps.
242
+ */
243
+ readonly addPath: (path: string) => Effect.Effect<void>;
244
+ }
245
+
246
+ /**
247
+ * ActionOutputs tag for dependency injection.
248
+ *
249
+ * @public
250
+ */
251
+ export declare const ActionOutputs: Context.Tag<ActionOutputs, ActionOutputs>;
252
+
253
+ export declare const ActionOutputsLive: Layer.Layer<ActionOutputs>;
254
+
255
+ /**
256
+ * Test implementation that captures outputs in memory.
257
+ *
258
+ * @example
259
+ * ```ts
260
+ * const state = ActionOutputsTest.empty();
261
+ * const layer = ActionOutputsTest.layer(state);
262
+ * ```
263
+ */
264
+ export declare const ActionOutputsTest: {
265
+ /**
266
+ * Create a fresh empty test state container.
267
+ */
268
+ readonly empty: () => ActionOutputsTestState;
269
+ /**
270
+ * Create a test layer from the given state.
271
+ */
272
+ readonly layer: (state: ActionOutputsTestState) => Layer.Layer<ActionOutputs, never, never>;
273
+ };
274
+
275
+ /**
276
+ * In-memory state captured by the test output layer.
277
+ */
278
+ export declare interface ActionOutputsTestState {
279
+ readonly outputs: Array<CapturedOutput>;
280
+ readonly summaries: Array<string>;
281
+ readonly variables: Array<CapturedOutput>;
282
+ readonly paths: Array<string>;
283
+ }
284
+
285
+ /**
286
+ * Bold text.
287
+ */
288
+ export declare const bold: (text: string) => string;
289
+
290
+ /**
291
+ * A captured output entry.
292
+ */
293
+ export declare const CapturedOutput: Schema.Struct<{
294
+ name: typeof Schema.String;
295
+ value: typeof Schema.String;
296
+ }>;
297
+
298
+ export declare type CapturedOutput = typeof CapturedOutput.Type;
299
+
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
+ /**
309
+ * A single item in a checklist.
310
+ */
311
+ export declare const ChecklistItem: Schema.Struct<{
312
+ label: typeof Schema.String;
313
+ checked: typeof Schema.Boolean;
314
+ }>;
315
+
316
+ export declare type ChecklistItem = typeof ChecklistItem.Type;
317
+
318
+ /**
319
+ * Inline code.
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;
327
+
328
+ /**
329
+ * FiberRef that holds the current action log level for the fiber.
330
+ */
331
+ export declare const CurrentLogLevel: FiberRef.FiberRef<ActionLogLevel>;
332
+
333
+ /**
334
+ * Build a collapsible `<details>` block.
335
+ */
336
+ export declare const details: (summary: string, content: string) => string;
337
+
338
+ /**
339
+ * Build a markdown heading.
340
+ *
341
+ * @param level - Heading level 1-6, defaults to 2.
342
+ */
343
+ export declare const heading: (text: string, level?: 1 | 2 | 3 | 4 | 5 | 6) => string;
344
+
345
+ /**
346
+ * Build a markdown link.
347
+ */
348
+ export declare const link: (text: string, url: string) => string;
349
+
350
+ /**
351
+ * Build a bulleted list.
352
+ */
353
+ export declare const list: (items: readonly string[]) => string;
354
+
355
+ /**
356
+ * Log level input values accepted by the standardized `log-level` action input.
357
+ * Includes `auto` which resolves based on the GitHub Actions environment.
358
+ */
359
+ export declare const LogLevelInput: Schema.Literal<["info", "verbose", "debug", "auto"]>;
360
+
361
+ export declare type LogLevelInput = typeof LogLevelInput.Type;
362
+
363
+ /**
364
+ * Create an Effect Logger that routes to GitHub Actions log functions.
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"`.
376
+ */
377
+ export declare const resolveLogLevel: (input: "auto" | "debug" | "info" | "verbose") => "debug" | "info" | "verbose";
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>;
388
+
389
+ /**
390
+ * Status values for {@link statusIcon}.
391
+ */
392
+ export declare const Status: Schema.Literal<["pass", "fail", "skip", "warn"]>;
393
+
394
+ export declare type Status = typeof Status.Type;
395
+
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
+ /**
412
+ * Annotation type captured by the test layer.
413
+ */
414
+ export declare type TestAnnotationType = "error" | "warning" | "notice";
415
+
416
+ export { }
package/index.js ADDED
@@ -0,0 +1,295 @@
1
+ import { Context, Data, Effect, FiberRef, FiberRefs, Layer, LogLevel, Logger, Option, Schema } from "effect";
2
+ import { addPath, debug, endGroup, error as core_error, exportVariable, getInput, info, notice, setOutput, setSecret, startGroup, summary as core_summary, warning } from "@actions/core";
3
+ const ActionInputErrorBase = Data.TaggedError("ActionInputError");
4
+ class ActionInputError extends ActionInputErrorBase {
5
+ }
6
+ const ActionOutputErrorBase = Data.TaggedError("ActionOutputError");
7
+ class ActionOutputError extends ActionOutputErrorBase {
8
+ }
9
+ const ActionInputs = Context.GenericTag("ActionInputs");
10
+ const decodeInput = (name, raw, schema)=>Schema.decode(schema)(raw).pipe(Effect.mapError((parseError)=>new ActionInputError({
11
+ inputName: name,
12
+ reason: `Input "${name}" validation failed: ${parseError.message}`,
13
+ rawValue: raw
14
+ })));
15
+ const decodeJsonInput = (name, raw, schema)=>Effect["try"]({
16
+ try: ()=>JSON.parse(raw),
17
+ catch: (error)=>new ActionInputError({
18
+ inputName: name,
19
+ reason: `Input "${name}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
20
+ rawValue: raw
21
+ })
22
+ }).pipe(Effect.flatMap((parsed)=>Schema.decode(schema)(parsed).pipe(Effect.mapError((parseError)=>new ActionInputError({
23
+ inputName: name,
24
+ reason: `Input "${name}" JSON validation failed: ${parseError.message}`,
25
+ rawValue: raw
26
+ })))));
27
+ const ActionInputsLive = Layer.succeed(ActionInputs, {
28
+ get: (name, schema)=>Effect.sync(()=>getInput(name, {
29
+ required: true
30
+ })).pipe(Effect.flatMap((raw)=>decodeInput(name, raw, schema))),
31
+ getOptional: (name, schema)=>Effect.sync(()=>getInput(name, {
32
+ required: false
33
+ })).pipe(Effect.flatMap((raw)=>{
34
+ if ("" === raw) return Effect.succeed(Option.none());
35
+ return decodeInput(name, raw, schema).pipe(Effect.map((a)=>Option.some(a)));
36
+ })),
37
+ getSecret: (name, schema)=>Effect.sync(()=>{
38
+ const raw = getInput(name, {
39
+ required: true
40
+ });
41
+ setSecret(raw);
42
+ return raw;
43
+ }).pipe(Effect.flatMap((raw)=>decodeInput(name, raw, schema))),
44
+ getJson: (name, schema)=>Effect.sync(()=>getInput(name, {
45
+ required: true
46
+ })).pipe(Effect.flatMap((raw)=>decodeJsonInput(name, raw, schema)))
47
+ });
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
+ const ActionLogger = Context.GenericTag("ActionLogger");
78
+ const CurrentLogLevel = FiberRef.unsafeMake("info");
79
+ const setLogLevel = (level)=>FiberRef.set(CurrentLogLevel, level);
80
+ const formatMessage = (message)=>{
81
+ const value = Array.isArray(message) && 1 === message.length ? message[0] : message;
82
+ return "string" == typeof value ? value : JSON.stringify(value);
83
+ };
84
+ const shouldEmitUserFacing = (effectLevel, actionLevel)=>{
85
+ if ("debug" === actionLevel) return true;
86
+ if ("verbose" === actionLevel) return LogLevel.greaterThanEqual(effectLevel, LogLevel.Info);
87
+ return LogLevel.greaterThanEqual(effectLevel, LogLevel.Warning);
88
+ };
89
+ const emitToGitHub = (level, message)=>{
90
+ if (LogLevel.greaterThanEqual(level, LogLevel.Error)) core_error(message);
91
+ else if (LogLevel.greaterThanEqual(level, LogLevel.Warning)) warning(message);
92
+ else info(message);
93
+ };
94
+ const makeActionLogger = ()=>Logger.make(({ logLevel, message, context })=>{
95
+ const text = formatMessage(message);
96
+ debug(text);
97
+ const actionLevel = FiberRefs.getOrDefault(context, CurrentLogLevel);
98
+ if (shouldEmitUserFacing(logLevel, actionLevel)) emitToGitHub(logLevel, text);
99
+ });
100
+ const ActionLoggerLayer = Logger.replace(Logger.defaultLogger, makeActionLogger());
101
+ const createBuffer = ()=>({
102
+ entries: []
103
+ });
104
+ const flushBuffer = (label, buffer)=>{
105
+ if (buffer.entries.length > 0) {
106
+ info(`--- Buffered output for "${label}" ---`);
107
+ for (const entry of buffer.entries)info(entry);
108
+ info(`--- End buffered output for "${label}" ---`);
109
+ }
110
+ };
111
+ const ActionLoggerLive = Layer.succeed(ActionLogger, {
112
+ group: (name, effect)=>Effect.acquireUseRelease(Effect.sync(()=>startGroup(name)), ()=>effect, ()=>Effect.sync(()=>endGroup())),
113
+ withBuffer: (label, effect)=>FiberRef.get(CurrentLogLevel).pipe(Effect.flatMap((level)=>{
114
+ if ("info" !== level) return effect;
115
+ const buffer = createBuffer();
116
+ const bufferingLogger = Logger.make(({ logLevel, message })=>{
117
+ const text = formatMessage(message);
118
+ debug(text);
119
+ if (LogLevel.greaterThanEqual(logLevel, LogLevel.Warning)) emitToGitHub(logLevel, text);
120
+ else buffer.entries.push(text);
121
+ });
122
+ return effect.pipe(Logger.withMinimumLogLevel(LogLevel.All), Effect.provide(Logger.replace(Logger.defaultLogger, bufferingLogger)), Effect.tapErrorCause(()=>Effect.sync(()=>flushBuffer(label, buffer))));
123
+ })),
124
+ annotationError: (message, properties)=>Effect.sync(()=>{
125
+ void 0 !== properties ? core_error(message, properties) : core_error(message);
126
+ }),
127
+ annotationWarning: (message, properties)=>Effect.sync(()=>{
128
+ void 0 !== properties ? warning(message, properties) : warning(message);
129
+ }),
130
+ annotationNotice: (message, properties)=>Effect.sync(()=>{
131
+ void 0 !== properties ? notice(message, properties) : notice(message);
132
+ })
133
+ });
134
+ const makeAnnotation = (state, type)=>(message, properties)=>Effect.sync(()=>{
135
+ state.annotations.push({
136
+ type,
137
+ message,
138
+ ...void 0 !== properties ? {
139
+ properties
140
+ } : {}
141
+ });
142
+ });
143
+ const ActionLoggerTest = {
144
+ empty: ()=>({
145
+ entries: [],
146
+ groups: [],
147
+ annotations: [],
148
+ flushedBuffers: []
149
+ }),
150
+ layer: (state)=>Layer.succeed(ActionLogger, {
151
+ group: (name, effect)=>{
152
+ const groupEntries = [];
153
+ state.groups.push({
154
+ name,
155
+ entries: groupEntries
156
+ });
157
+ return effect;
158
+ },
159
+ withBuffer: (label, effect)=>FiberRef.get(CurrentLogLevel).pipe(Effect.flatMap((level)=>{
160
+ if ("info" !== level) return effect;
161
+ return effect.pipe(Effect.tapErrorCause(()=>Effect.sync(()=>{
162
+ state.flushedBuffers.push({
163
+ label,
164
+ entries: []
165
+ });
166
+ })));
167
+ })),
168
+ annotationError: makeAnnotation(state, "error"),
169
+ annotationWarning: makeAnnotation(state, "warning"),
170
+ annotationNotice: makeAnnotation(state, "notice")
171
+ })
172
+ };
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
+ const ActionOutputsTest = {
191
+ empty: ()=>({
192
+ outputs: [],
193
+ summaries: [],
194
+ variables: [],
195
+ paths: []
196
+ }),
197
+ layer: (state)=>Layer.succeed(ActionOutputs, {
198
+ set: (name, value)=>Effect.sync(()=>{
199
+ state.outputs.push({
200
+ name,
201
+ value
202
+ });
203
+ }),
204
+ setJson: (name, value, schema)=>Schema.encode(schema)(value).pipe(Effect.tap((encoded)=>Effect.sync(()=>{
205
+ state.outputs.push({
206
+ name,
207
+ value: JSON.stringify(encoded)
208
+ });
209
+ })), Effect.asVoid, Effect.mapError((parseError)=>new ActionOutputError({
210
+ outputName: name,
211
+ reason: `Output "${name}" validation failed: ${parseError.message}`
212
+ }))),
213
+ summary: (content)=>Effect.sync(()=>{
214
+ state.summaries.push(content);
215
+ }),
216
+ exportVariable: (name, value)=>Effect.sync(()=>{
217
+ state.variables.push({
218
+ name,
219
+ value
220
+ });
221
+ }),
222
+ addPath: (path)=>Effect.sync(()=>{
223
+ state.paths.push(path);
224
+ })
225
+ })
226
+ };
227
+ const Status = Schema.Literal("pass", "fail", "skip", "warn").annotations({
228
+ identifier: "Status",
229
+ title: "Check Status",
230
+ description: "Status indicator for check run outcomes"
231
+ });
232
+ const ChecklistItem = Schema.Struct({
233
+ label: Schema.String,
234
+ checked: Schema.Boolean
235
+ }).annotations({
236
+ identifier: "ChecklistItem",
237
+ title: "Checklist Item"
238
+ });
239
+ const CapturedOutput = Schema.Struct({
240
+ name: Schema.String,
241
+ value: Schema.String
242
+ }).annotations({
243
+ identifier: "CapturedOutput",
244
+ title: "Captured Output"
245
+ });
246
+ const ActionLogLevel = Schema.Literal("info", "verbose", "debug").annotations({
247
+ identifier: "ActionLogLevel",
248
+ title: "Action Log Level",
249
+ description: "Logging verbosity for GitHub Action output"
250
+ });
251
+ const LogLevelInput = Schema.Literal("info", "verbose", "debug", "auto").annotations({
252
+ identifier: "LogLevelInput",
253
+ title: "Log Level Input",
254
+ description: "Logging verbosity: info, verbose, debug, or auto",
255
+ message: ()=>({
256
+ message: 'log-level must be one of: "info", "verbose", "debug", "auto"',
257
+ override: true
258
+ })
259
+ });
260
+ const resolveLogLevel = (input)=>{
261
+ if ("auto" !== input) return input;
262
+ return "1" === process.env.RUNNER_DEBUG ? "debug" : "info";
263
+ };
264
+ const table = (headers, rows)=>{
265
+ const headerRow = `| ${headers.join(" | ")} |`;
266
+ const separator = `| ${headers.map(()=>"---").join(" | ")} |`;
267
+ const dataRows = rows.map((row)=>`| ${row.join(" | ")} |`);
268
+ return [
269
+ headerRow,
270
+ separator,
271
+ ...dataRows
272
+ ].join("\n");
273
+ };
274
+ const heading = (text, level = 2)=>`${"#".repeat(level)} ${text}`;
275
+ const details = (summary, content)=>`<details>\n<summary>${summary}</summary>\n\n${content}\n\n</details>`;
276
+ const rule = ()=>"---";
277
+ const statusIcon = (status)=>{
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
+ }
288
+ };
289
+ const GithubMarkdown_link = (text, url)=>`[${text}](${url})`;
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 };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@savvy-web/github-action-effects",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Effect-based utility library for building robust, well-logged, and schema-validated GitHub Actions.",
6
+ "homepage": "https://github.com/savvy-web/github-action-effects#readme",
7
+ "bugs": {
8
+ "url": "https://github.com/savvy-web/github-action-effects/issues"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/savvy-web/github-action-effects.git"
13
+ },
14
+ "license": "MIT",
15
+ "author": {
16
+ "name": "C. Spencer Beggs",
17
+ "email": "spencer@savvyweb.systems",
18
+ "url": "https://savvyweb.systems"
19
+ },
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./index.d.ts",
24
+ "import": "./index.js"
25
+ }
26
+ },
27
+ "peerDependencies": {
28
+ "@actions/core": "^3.0.0",
29
+ "@actions/exec": "^3.0.0",
30
+ "@actions/github": "^9.0.0",
31
+ "@effect/platform": ">=0.94.0",
32
+ "@effect/platform-node": ">=0.104.0",
33
+ "effect": "^3.19.0"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "@actions/core": {
37
+ "optional": false
38
+ },
39
+ "@actions/exec": {
40
+ "optional": true
41
+ },
42
+ "@actions/github": {
43
+ "optional": true
44
+ },
45
+ "@effect/platform": {
46
+ "optional": true
47
+ },
48
+ "@effect/platform-node": {
49
+ "optional": true
50
+ },
51
+ "effect": {
52
+ "optional": false
53
+ }
54
+ },
55
+ "files": [
56
+ "!github-action-effects.api.json",
57
+ "!tsconfig.json",
58
+ "!tsdoc.json",
59
+ "LICENSE",
60
+ "README.md",
61
+ "index.d.ts",
62
+ "index.js",
63
+ "package.json",
64
+ "tsdoc-metadata.json"
65
+ ]
66
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.57.6"
9
+ }
10
+ ]
11
+ }