@computis/effect-github 0.0.0-stage → 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.
Files changed (109) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +23 -0
  3. package/README.md +79 -2
  4. package/dist/action-client.d.ts +38 -0
  5. package/dist/action-client.d.ts.map +1 -0
  6. package/dist/action-client.js +96 -0
  7. package/dist/action-client.js.map +1 -0
  8. package/dist/action-context.d.ts +51 -0
  9. package/dist/action-context.d.ts.map +1 -0
  10. package/dist/action-context.js +61 -0
  11. package/dist/action-context.js.map +1 -0
  12. package/dist/action-runner.d.ts +57 -0
  13. package/dist/action-runner.d.ts.map +1 -0
  14. package/dist/action-runner.js +100 -0
  15. package/dist/action-runner.js.map +1 -0
  16. package/dist/action-summary.d.ts +45 -0
  17. package/dist/action-summary.d.ts.map +1 -0
  18. package/dist/action-summary.js +92 -0
  19. package/dist/action-summary.js.map +1 -0
  20. package/dist/action.d.ts +16 -0
  21. package/dist/action.d.ts.map +1 -0
  22. package/dist/action.js +37 -0
  23. package/dist/action.js.map +1 -0
  24. package/dist/errors/action-api-error.d.ts +13 -0
  25. package/dist/errors/action-api-error.d.ts.map +1 -0
  26. package/dist/errors/action-api-error.js +17 -0
  27. package/dist/errors/action-api-error.js.map +1 -0
  28. package/dist/errors/action-context-error.d.ts +16 -0
  29. package/dist/errors/action-context-error.d.ts.map +1 -0
  30. package/dist/errors/action-context-error.js +16 -0
  31. package/dist/errors/action-context-error.js.map +1 -0
  32. package/dist/errors/action-failed.d.ts +10 -0
  33. package/dist/errors/action-failed.d.ts.map +1 -0
  34. package/dist/errors/action-failed.js +10 -0
  35. package/dist/errors/action-failed.js.map +1 -0
  36. package/dist/errors/action-failure.d.ts +15 -0
  37. package/dist/errors/action-failure.d.ts.map +1 -0
  38. package/dist/errors/action-failure.js +15 -0
  39. package/dist/errors/action-failure.js.map +1 -0
  40. package/dist/errors/action-input-error.d.ts +17 -0
  41. package/dist/errors/action-input-error.d.ts.map +1 -0
  42. package/dist/errors/action-input-error.js +22 -0
  43. package/dist/errors/action-input-error.js.map +1 -0
  44. package/dist/errors/action-oidc-error.d.ts +15 -0
  45. package/dist/errors/action-oidc-error.d.ts.map +1 -0
  46. package/dist/errors/action-oidc-error.js +18 -0
  47. package/dist/errors/action-oidc-error.js.map +1 -0
  48. package/dist/errors/action-summary-error.d.ts +15 -0
  49. package/dist/errors/action-summary-error.d.ts.map +1 -0
  50. package/dist/errors/action-summary-error.js +18 -0
  51. package/dist/errors/action-summary-error.js.map +1 -0
  52. package/dist/errors/input-validation-failure.d.ts +19 -0
  53. package/dist/errors/input-validation-failure.d.ts.map +1 -0
  54. package/dist/errors/input-validation-failure.js +19 -0
  55. package/dist/errors/input-validation-failure.js.map +1 -0
  56. package/dist/errors/platform-missing.d.ts +9 -0
  57. package/dist/errors/platform-missing.d.ts.map +1 -0
  58. package/dist/errors/platform-missing.js +9 -0
  59. package/dist/errors/platform-missing.js.map +1 -0
  60. package/dist/input.d.ts +14 -0
  61. package/dist/input.d.ts.map +1 -0
  62. package/dist/input.js +30 -0
  63. package/dist/input.js.map +1 -0
  64. package/dist/object.d.ts +2 -0
  65. package/dist/object.d.ts.map +1 -0
  66. package/dist/object.js +2 -0
  67. package/dist/object.js.map +1 -0
  68. package/dist/testing/action-client-test.d.ts +7 -0
  69. package/dist/testing/action-client-test.d.ts.map +1 -0
  70. package/dist/testing/action-client-test.js +10 -0
  71. package/dist/testing/action-client-test.js.map +1 -0
  72. package/dist/testing/action-context-test.d.ts +10 -0
  73. package/dist/testing/action-context-test.d.ts.map +1 -0
  74. package/dist/testing/action-context-test.js +27 -0
  75. package/dist/testing/action-context-test.js.map +1 -0
  76. package/dist/testing/action-runner-test.d.ts +26 -0
  77. package/dist/testing/action-runner-test.d.ts.map +1 -0
  78. package/dist/testing/action-runner-test.js +92 -0
  79. package/dist/testing/action-runner-test.js.map +1 -0
  80. package/dist/testing/action-summary-test.d.ts +11 -0
  81. package/dist/testing/action-summary-test.d.ts.map +1 -0
  82. package/dist/testing/action-summary-test.js +62 -0
  83. package/dist/testing/action-summary-test.js.map +1 -0
  84. package/dist/thrown-message.d.ts +2 -0
  85. package/dist/thrown-message.d.ts.map +1 -0
  86. package/dist/thrown-message.js +7 -0
  87. package/dist/thrown-message.js.map +1 -0
  88. package/package.json +64 -3
  89. package/src/action-client.ts +238 -0
  90. package/src/action-context.ts +121 -0
  91. package/src/action-runner.ts +221 -0
  92. package/src/action-summary.ts +143 -0
  93. package/src/action.ts +81 -0
  94. package/src/errors/action-api-error.ts +18 -0
  95. package/src/errors/action-context-error.ts +20 -0
  96. package/src/errors/action-failed.ts +10 -0
  97. package/src/errors/action-failure.ts +36 -0
  98. package/src/errors/action-input-error.ts +24 -0
  99. package/src/errors/action-oidc-error.ts +19 -0
  100. package/src/errors/action-summary-error.ts +22 -0
  101. package/src/errors/input-validation-failure.ts +23 -0
  102. package/src/errors/platform-missing.ts +9 -0
  103. package/src/input.ts +60 -0
  104. package/src/object.ts +4 -0
  105. package/src/testing/action-client-test.ts +15 -0
  106. package/src/testing/action-context-test.ts +36 -0
  107. package/src/testing/action-runner-test.ts +138 -0
  108. package/src/testing/action-summary-test.ts +82 -0
  109. package/src/thrown-message.ts +9 -0
@@ -0,0 +1,143 @@
1
+ import { summary } from "@actions/core";
2
+ import { whenDefined } from "./object.ts";
3
+ import { thrownMessage } from "./thrown-message.ts";
4
+ import { Context, Effect, Layer } from "effect";
5
+ import { ActionSummaryError, ActionSummaryErrorReason } from "./errors/action-summary-error.ts";
6
+
7
+ export interface SummaryTableCell {
8
+ readonly data: string;
9
+ readonly header?: boolean;
10
+ readonly colspan?: number;
11
+ readonly rowspan?: number;
12
+ }
13
+
14
+ export interface SummaryImageOptions {
15
+ readonly width?: string;
16
+ readonly height?: string;
17
+ }
18
+
19
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
20
+
21
+ export interface ActionSummaryApi {
22
+ readonly addRaw: (text: string, addEOL?: boolean) => ActionSummaryApi;
23
+ readonly addEOL: () => ActionSummaryApi;
24
+ readonly addHeading: (text: string, level?: HeadingLevel) => ActionSummaryApi;
25
+ readonly addCodeBlock: (code: string, lang?: string) => ActionSummaryApi;
26
+ readonly addList: (items: readonly string[], ordered?: boolean) => ActionSummaryApi;
27
+ readonly addTable: (rows: readonly (readonly SummaryTableCell[])[]) => ActionSummaryApi;
28
+ readonly addDetails: (label: string, content: string) => ActionSummaryApi;
29
+ readonly addImage: (src: string, alt: string, options?: SummaryImageOptions) => ActionSummaryApi;
30
+ readonly addLink: (text: string, href: string) => ActionSummaryApi;
31
+ readonly addSeparator: () => ActionSummaryApi;
32
+ readonly addBreak: () => ActionSummaryApi;
33
+ readonly addQuote: (text: string, cite?: string) => ActionSummaryApi;
34
+ readonly stringify: () => string;
35
+ readonly isEmptyBuffer: () => boolean;
36
+ readonly emptyBuffer: () => ActionSummaryApi;
37
+ readonly write: (options?: {
38
+ readonly overwrite?: boolean;
39
+ }) => Effect.Effect<ActionSummaryApi, ActionSummaryError>;
40
+ readonly clear: () => Effect.Effect<ActionSummaryApi, ActionSummaryError>;
41
+ }
42
+
43
+ const writeFailed = (cause: unknown) =>
44
+ new ActionSummaryError({
45
+ cause,
46
+ description: thrownMessage(cause, "GITHUB_STEP_SUMMARY could not be written"),
47
+ reason: ActionSummaryErrorReason.WriteFailed,
48
+ });
49
+
50
+ export class ActionSummary extends Context.Service<ActionSummary, ActionSummaryApi>()(
51
+ "@computis/effect-github/ActionSummary",
52
+ ) {
53
+ static readonly layer = Layer.sync(ActionSummary, () => {
54
+ const self: ActionSummaryApi = {
55
+ addBreak: () => {
56
+ summary.addBreak();
57
+ return self;
58
+ },
59
+ addCodeBlock: (code, lang) => {
60
+ summary.addCodeBlock(code, lang);
61
+ return self;
62
+ },
63
+ addDetails: (label, content) => {
64
+ summary.addDetails(label, content);
65
+ return self;
66
+ },
67
+ addEOL: () => {
68
+ summary.addEOL();
69
+ return self;
70
+ },
71
+ addHeading: (text, level) => {
72
+ summary.addHeading(text, level);
73
+ return self;
74
+ },
75
+ addImage: (src, alt, options) => {
76
+ summary.addImage(src, alt, options);
77
+ return self;
78
+ },
79
+ addLink: (text, href) => {
80
+ summary.addLink(text, href);
81
+ return self;
82
+ },
83
+ addList: (items, ordered) => {
84
+ summary.addList([...items], ordered);
85
+ return self;
86
+ },
87
+ addQuote: (text, cite) => {
88
+ summary.addQuote(text, cite);
89
+ return self;
90
+ },
91
+ addRaw: (text, addEOL) => {
92
+ summary.addRaw(text, addEOL);
93
+ return self;
94
+ },
95
+ addSeparator: () => {
96
+ summary.addSeparator();
97
+ return self;
98
+ },
99
+ addTable: (rows) => {
100
+ summary.addTable(
101
+ rows.map((row) =>
102
+ row.map((cell) => ({
103
+ data: cell.data,
104
+ ...whenDefined(cell.header, (header) => ({ header })),
105
+ ...whenDefined(cell.colspan, (colspan) => ({ colspan: String(colspan) })),
106
+ ...whenDefined(cell.rowspan, (rowspan) => ({ rowspan: String(rowspan) })),
107
+ })),
108
+ ),
109
+ );
110
+ return self;
111
+ },
112
+ clear: Effect.fn("ActionSummary.clear")(function* clear() {
113
+ yield* Effect.tryPromise({ catch: writeFailed, try: async () => await summary.clear() });
114
+ return self;
115
+ }),
116
+ emptyBuffer: () => {
117
+ summary.emptyBuffer();
118
+ return self;
119
+ },
120
+ isEmptyBuffer: () => summary.isEmptyBuffer(),
121
+ stringify: () => summary.stringify(),
122
+ write: Effect.fn("ActionSummary.write")(function* write(options?: {
123
+ readonly overwrite?: boolean;
124
+ }) {
125
+ yield* Effect.tryPromise({
126
+ catch: writeFailed,
127
+ try: async () => await summary.write(options),
128
+ });
129
+ return self;
130
+ }),
131
+ };
132
+ return self;
133
+ });
134
+ }
135
+
136
+ const jobSummary = Effect.service(ActionSummary);
137
+
138
+ export const write = (options?: { readonly overwrite?: boolean }) =>
139
+ jobSummary.pipe(Effect.flatMap((service) => service.write(options)));
140
+
141
+ export const clear = jobSummary.pipe(Effect.flatMap((service) => service.clear()));
142
+
143
+ export const { layer } = ActionSummary;
package/src/action.ts ADDED
@@ -0,0 +1,81 @@
1
+ import { setFailed } from "@actions/core";
2
+ import { thrownMessage } from "./thrown-message.ts";
3
+ import { Cause, Effect, Layer, Logger, ManagedRuntime, Option } from "effect";
4
+ import type { Redacted } from "effect";
5
+ import { ActionClient } from "./action-client.ts";
6
+ import { ActionContext } from "./action-context.ts";
7
+ import { ActionRunner } from "./action-runner.ts";
8
+ import { ActionSummary } from "./action-summary.ts";
9
+ import { isActionFailure } from "./errors/action-failure.ts";
10
+ import { PlatformMissing } from "./errors/platform-missing.ts";
11
+
12
+ export type ActionRequirements = ActionRunner | ActionContext | ActionClient | ActionSummary;
13
+
14
+ export const layer = (token: Redacted.Redacted): Layer.Layer<ActionRequirements> =>
15
+ Layer.mergeAll(
16
+ ActionRunner.layer,
17
+ ActionContext.layer,
18
+ ActionClient.layer(token),
19
+ ActionSummary.layer,
20
+ );
21
+
22
+ export const layerConfig = Layer.mergeAll(
23
+ ActionRunner.layer,
24
+ ActionContext.layer,
25
+ ActionClient.layerConfig,
26
+ ActionSummary.layer,
27
+ );
28
+
29
+ export const runMain = <A, E>(
30
+ effect: Effect.Effect<A, E, ActionRequirements>,
31
+ options: {
32
+ readonly disableErrorReporting?: boolean;
33
+ readonly disablePrettyLogger?: boolean;
34
+ readonly token?: Redacted.Redacted;
35
+ } = {},
36
+ ) => {
37
+ const program = effect.pipe(
38
+ Effect.provide(options.token === undefined ? layerConfig : layer(options.token)),
39
+ Effect.tapCause(
40
+ Effect.fn("Action.reportFailure")(function* reportFailure<F>(cause: Cause.Cause<F>) {
41
+ if (Cause.hasInterruptsOnly(cause)) {
42
+ return;
43
+ }
44
+ if (options.disableErrorReporting !== true) {
45
+ yield* Effect.logError(cause);
46
+ }
47
+ setFailed(
48
+ Option.match(Cause.findErrorOption(cause), {
49
+ onNone: () => Cause.pretty(cause),
50
+ onSome: (error) =>
51
+ isActionFailure(error)
52
+ ? error.displayMessage
53
+ : thrownMessage(error, Cause.pretty(cause)),
54
+ }),
55
+ );
56
+ }),
57
+ ),
58
+ Effect.provide(
59
+ options.disablePrettyLogger === true ? Layer.empty : Logger.layer([Logger.consolePretty()]),
60
+ ),
61
+ );
62
+ Effect.runFork(
63
+ Effect.tryPromise({
64
+ catch: () => new PlatformMissing({ platform: "@effect/platform-node" }),
65
+ try: async () => await import("@effect/platform-node/NodeRuntime"),
66
+ }).pipe(
67
+ Effect.flatMap((NodeRuntime) =>
68
+ Effect.sync(() => {
69
+ NodeRuntime.runMain(program, { disableErrorReporting: true });
70
+ }),
71
+ ),
72
+ Effect.catchTag("PlatformMissing", (missing) =>
73
+ Effect.sync(() => {
74
+ setFailed(missing.message);
75
+ }),
76
+ ),
77
+ ),
78
+ );
79
+ };
80
+
81
+ export const makeRuntime = (token: Redacted.Redacted) => ManagedRuntime.make(layer(token));
@@ -0,0 +1,18 @@
1
+ import { Schema } from "effect";
2
+
3
+ export class ActionApiError extends Schema.TaggedError<ActionApiError>()("ActionApiError", {
4
+ cause: Schema.optionalKey(Schema.Defect()),
5
+ description: Schema.optionalKey(Schema.String),
6
+ method: Schema.String,
7
+ status: Schema.optionalKey(Schema.Number),
8
+ }) {
9
+ override get message() {
10
+ const status = this.status === undefined ? "" : ` (${this.status})`;
11
+ const description = this.description === undefined ? "" : ` - ${this.description}`;
12
+ return `GitHub API error${status}: ${this.method}${description}`;
13
+ }
14
+
15
+ get isRateLimited() {
16
+ return this.status === 403 || this.status === 429;
17
+ }
18
+ }
@@ -0,0 +1,20 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const ActionContextErrorReason = {
4
+ InvalidPayload: "InvalidPayload",
5
+ InvalidRepo: "InvalidRepo",
6
+ MissingEnv: "MissingEnv",
7
+ } as const;
8
+
9
+ export class ActionContextError extends Schema.TaggedError<ActionContextError>()(
10
+ "ActionContextError",
11
+ {
12
+ cause: Schema.optionalKey(Schema.Defect()),
13
+ description: Schema.optionalKey(Schema.String),
14
+ reason: Schema.Literals(Object.values(ActionContextErrorReason)),
15
+ },
16
+ ) {
17
+ override get message() {
18
+ return this.description ?? `Context error: ${this.reason}`;
19
+ }
20
+ }
@@ -0,0 +1,10 @@
1
+ import { Schema } from "effect";
2
+
3
+ export class ActionFailed extends Schema.TaggedError<ActionFailed>()("ActionFailed", {
4
+ cause: Schema.optionalKey(Schema.Defect()),
5
+ message: Schema.String,
6
+ }) {
7
+ get displayMessage() {
8
+ return this.message;
9
+ }
10
+ }
@@ -0,0 +1,36 @@
1
+ import { ActionApiError } from "./action-api-error.ts";
2
+ import { ActionContextError } from "./action-context-error.ts";
3
+ import { ActionFailed } from "./action-failed.ts";
4
+ import { ActionInputError } from "./action-input-error.ts";
5
+ import { ActionOIDCError } from "./action-oidc-error.ts";
6
+ import { ActionSummaryError } from "./action-summary-error.ts";
7
+ import { InputValidationFailure } from "./input-validation-failure.ts";
8
+
9
+ export type ActionFailure = InputValidationFailure | ActionFailed;
10
+
11
+ type NarrowedMember<Member, Target> = Member extends Target
12
+ ? Member
13
+ : [Extract<Target, Member>] extends [never]
14
+ ? Member & Target
15
+ : Extract<Target, Member>;
16
+
17
+ export type Narrowed<E, Target> = E extends unknown ? NarrowedMember<E, Target> : never;
18
+
19
+ export const isActionFailure = <E>(error: E): error is Narrowed<E, ActionFailure> =>
20
+ error instanceof InputValidationFailure || error instanceof ActionFailed;
21
+
22
+ export type ActionError =
23
+ | ActionInputError
24
+ | ActionContextError
25
+ | ActionApiError
26
+ | ActionOIDCError
27
+ | ActionSummaryError
28
+ | ActionFailure;
29
+
30
+ export const isActionError = <E>(error: E): error is Narrowed<E, ActionError> =>
31
+ isActionFailure(error) ||
32
+ error instanceof ActionInputError ||
33
+ error instanceof ActionContextError ||
34
+ error instanceof ActionApiError ||
35
+ error instanceof ActionOIDCError ||
36
+ error instanceof ActionSummaryError;
@@ -0,0 +1,24 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const ActionInputErrorReason = {
4
+ InvalidType: "InvalidType",
5
+ Missing: "Missing",
6
+ ParseError: "ParseError",
7
+ } as const;
8
+
9
+ const messages = {
10
+ [ActionInputErrorReason.InvalidType]: (name: string) => `Input "${name}" has invalid type`,
11
+ [ActionInputErrorReason.Missing]: (name: string) => `Input required and not supplied: ${name}`,
12
+ [ActionInputErrorReason.ParseError]: (name: string) => `Failed to parse input "${name}"`,
13
+ };
14
+
15
+ export class ActionInputError extends Schema.TaggedError<ActionInputError>()("ActionInputError", {
16
+ cause: Schema.optionalKey(Schema.Defect()),
17
+ name: Schema.String,
18
+ reason: Schema.Literals(Object.values(ActionInputErrorReason)),
19
+ value: Schema.optionalKey(Schema.Unknown),
20
+ }) {
21
+ override get message() {
22
+ return messages[this.reason](this.name);
23
+ }
24
+ }
@@ -0,0 +1,19 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const ActionOIDCErrorReason = {
4
+ NotAvailable: "NotAvailable",
5
+ RequestFailed: "RequestFailed",
6
+ } as const;
7
+
8
+ export class ActionOIDCError extends Schema.TaggedError<ActionOIDCError>()("ActionOIDCError", {
9
+ cause: Schema.optionalKey(Schema.Defect()),
10
+ description: Schema.optionalKey(Schema.String),
11
+ reason: Schema.Literals(Object.values(ActionOIDCErrorReason)),
12
+ }) {
13
+ override get message() {
14
+ if (this.reason === ActionOIDCErrorReason.NotAvailable) {
15
+ return "OIDC token is not available in this environment";
16
+ }
17
+ return `OIDC token request failed${this.description === undefined ? "" : `: ${this.description}`}`;
18
+ }
19
+ }
@@ -0,0 +1,22 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const ActionSummaryErrorReason = {
4
+ NotAvailable: "NotAvailable",
5
+ WriteFailed: "WriteFailed",
6
+ } as const;
7
+
8
+ export class ActionSummaryError extends Schema.TaggedError<ActionSummaryError>()(
9
+ "ActionSummaryError",
10
+ {
11
+ cause: Schema.optionalKey(Schema.Defect()),
12
+ description: Schema.optionalKey(Schema.String),
13
+ reason: Schema.Literals(Object.values(ActionSummaryErrorReason)),
14
+ },
15
+ ) {
16
+ override get message() {
17
+ if (this.reason === ActionSummaryErrorReason.NotAvailable) {
18
+ return "Job summary is not available in this environment";
19
+ }
20
+ return `Failed to write job summary${this.description === undefined ? "" : `: ${this.description}`}`;
21
+ }
22
+ }
@@ -0,0 +1,23 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const InputValidationFailureReason = {
4
+ InvalidJson: "InvalidJson",
5
+ InvalidType: "InvalidType",
6
+ MissingRequired: "MissingRequired",
7
+ SchemaValidation: "SchemaValidation",
8
+ } as const;
9
+
10
+ export class InputValidationFailure extends Schema.TaggedError<InputValidationFailure>()(
11
+ "InputValidationFailure",
12
+ {
13
+ cause: Schema.optionalKey(Schema.Defect()),
14
+ input: Schema.String,
15
+ message: Schema.String,
16
+ reason: Schema.Literals(Object.values(InputValidationFailureReason)),
17
+ value: Schema.String,
18
+ },
19
+ ) {
20
+ get displayMessage() {
21
+ return `Input '${this.input}' is invalid: ${this.message}`;
22
+ }
23
+ }
@@ -0,0 +1,9 @@
1
+ import { Schema } from "effect";
2
+
3
+ export class PlatformMissing extends Schema.TaggedError<PlatformMissing>()("PlatformMissing", {
4
+ platform: Schema.String,
5
+ }) {
6
+ override get message() {
7
+ return `runMain runs on an Effect platform package: install ${this.platform} next to @computis/effect-github.`;
8
+ }
9
+ }
package/src/input.ts ADDED
@@ -0,0 +1,60 @@
1
+ import { Effect, Schema, SchemaGetter, SchemaTransformation } from "effect";
2
+ import { ActionRunner } from "./action-runner.ts";
3
+ import {
4
+ InputValidationFailure,
5
+ InputValidationFailureReason,
6
+ } from "./errors/input-validation-failure.ts";
7
+
8
+ const yamlTrue: ReadonlySet<string> = new Set(["true", "yes", "on", "1"]);
9
+
10
+ export const YamlBoolean: Schema.Codec<boolean, string> = Schema.String.pipe(
11
+ Schema.decodeTo(Schema.String, SchemaTransformation.toLowerCase()),
12
+ Schema.decodeTo(
13
+ Schema.Literals(["true", "yes", "on", "1", "false", "no", "off", "0", ""]),
14
+ SchemaTransformation.passthroughSupertype(),
15
+ ),
16
+ Schema.decodeTo(Schema.Boolean, {
17
+ decode: SchemaGetter.transform((literal) => yamlTrue.has(literal)),
18
+ encode: SchemaGetter.transform((value) => (value ? "true" : "false")),
19
+ }),
20
+ );
21
+
22
+ export const raw = Effect.fn("Input.raw")(function* raw(name: string) {
23
+ const runner = yield* ActionRunner;
24
+ return yield* runner.getInput(name).pipe(Effect.orDie);
25
+ });
26
+
27
+ export const parse = Effect.fn("Input.parse")(function* parse<S extends Schema.Top>(
28
+ name: string,
29
+ schema: S,
30
+ ) {
31
+ const value = yield* raw(name);
32
+ return yield* Schema.decodeUnknownEffect(schema)(value).pipe(
33
+ Effect.mapError(
34
+ (cause) =>
35
+ new InputValidationFailure({
36
+ cause,
37
+ input: name,
38
+ message: cause.message,
39
+ reason: InputValidationFailureReason.SchemaValidation,
40
+ value,
41
+ }),
42
+ ),
43
+ );
44
+ });
45
+
46
+ export const string = (name: string) => parse(name, Schema.String);
47
+
48
+ export const nonEmptyString = (name: string) => parse(name, Schema.NonEmptyString);
49
+
50
+ export const integer = (name: string) => parse(name, Schema.NumberFromString.check(Schema.isInt()));
51
+
52
+ export const number = (name: string) => parse(name, Schema.NumberFromString);
53
+
54
+ export const boolean = (name: string) => parse(name, YamlBoolean);
55
+
56
+ export const secret = (name: string) =>
57
+ parse(name, Schema.RedactedFromValue(Schema.NonEmptyString));
58
+
59
+ export const json = <S extends Schema.Top>(name: string, schema: S) =>
60
+ parse(name, Schema.fromJsonString(schema));
package/src/object.ts ADDED
@@ -0,0 +1,4 @@
1
+ export const whenDefined = <T, U extends object>(
2
+ value: T,
3
+ factory: (value: NonNullable<T>) => U,
4
+ ) => (value === undefined || value === null ? undefined : factory(value));
@@ -0,0 +1,15 @@
1
+ import { Redacted } from "effect";
2
+ import type { Layer } from "effect";
3
+ import { ActionClient } from "../action-client.ts";
4
+
5
+ export const make = (options: { readonly fetch?: typeof globalThis.fetch } = {}) =>
6
+ ActionClient.layer(Redacted.make("test-token"), {
7
+ request: {
8
+ fetch:
9
+ options.fetch ??
10
+ (async () =>
11
+ await Promise.resolve(Response.json({ message: "Not Found" }, { status: 404 }))),
12
+ },
13
+ });
14
+
15
+ export const layer: Layer.Layer<ActionClient> = make();
@@ -0,0 +1,36 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { ActionContext } from "../action-context.ts";
3
+ import type { ActionContextApi, IssueContext, RepoContext } from "../action-context.ts";
4
+ import { ActionContextError } from "../errors/action-context-error.ts";
5
+
6
+ export const make = (
7
+ options: Partial<
8
+ Omit<ActionContextApi, "repo" | "issue"> & {
9
+ readonly repo: RepoContext | ActionContextError;
10
+ readonly issue: IssueContext | ActionContextError;
11
+ }
12
+ > = {},
13
+ ) => {
14
+ const repo = options.repo ?? { owner: "octocat", repo: "hello-world" };
15
+ const issue = options.issue ?? { number: 1, owner: "octocat", repo: "hello-world" };
16
+ return Layer.succeed(ActionContext, {
17
+ action: options.action ?? "run",
18
+ actor: options.actor ?? "octocat",
19
+ apiUrl: options.apiUrl ?? "https://api.github.com",
20
+ eventName: options.eventName ?? "push",
21
+ graphqlUrl: options.graphqlUrl ?? "https://api.github.com/graphql",
22
+ issue: issue instanceof ActionContextError ? Effect.fail(issue) : Effect.succeed(issue),
23
+ job: options.job ?? "build",
24
+ payload: options.payload ?? {},
25
+ ref: options.ref ?? "refs/heads/main",
26
+ repo: repo instanceof ActionContextError ? Effect.fail(repo) : Effect.succeed(repo),
27
+ runAttempt: options.runAttempt ?? 1,
28
+ runId: options.runId ?? 12_345,
29
+ runNumber: options.runNumber ?? 1,
30
+ serverUrl: options.serverUrl ?? "https://github.com",
31
+ sha: options.sha ?? "abc123def456",
32
+ workflow: options.workflow ?? "CI",
33
+ });
34
+ };
35
+
36
+ export const layer = make();
@@ -0,0 +1,138 @@
1
+ import { Effect, Layer, Predicate, Redacted } from "effect";
2
+ import type { Schema } from "effect";
3
+ import { ActionRunner } from "../action-runner.ts";
4
+ import type { InputOptions } from "../action-runner.ts";
5
+ import { ActionInputError, ActionInputErrorReason } from "../errors/action-input-error.ts";
6
+
7
+ export interface LogEntry {
8
+ readonly level: "debug" | "info" | "warning" | "error" | "notice";
9
+ readonly message: string;
10
+ }
11
+
12
+ const trueInputs = new Set(["true", "True", "TRUE"]);
13
+ const falseInputs = new Set(["false", "False", "FALSE"]);
14
+
15
+ const messageText = (message: string | Error) =>
16
+ Predicate.isString(message) ? message : message.message;
17
+
18
+ export const make = (
19
+ options: {
20
+ readonly inputs?: Readonly<Record<string, string>>;
21
+ readonly state?: Readonly<Record<string, string>>;
22
+ readonly idToken?: string;
23
+ } = {},
24
+ ) => {
25
+ const inputs = new Map(Object.entries(options.inputs ?? {}));
26
+ const recording = {
27
+ failures: Array.of<string>(),
28
+ groups: Array.of<string>(),
29
+ logs: Array.of<LogEntry>(),
30
+ outputs: new Map<string, Schema.Json>(),
31
+ paths: Array.of<string>(),
32
+ secrets: Array.of<string>(),
33
+ state: new Map(Object.entries(options.state ?? {})),
34
+ variables: new Map<string, string>(),
35
+ };
36
+ const readInput = Effect.fn("ActionRunnerTest.readInput")(function* readInput(
37
+ name: string,
38
+ inputOptions: InputOptions = {},
39
+ ) {
40
+ const value = inputs.get(name) ?? "";
41
+ if (inputOptions.required === true && value === "") {
42
+ return yield* new ActionInputError({ name, reason: ActionInputErrorReason.Missing });
43
+ }
44
+ return inputOptions.trimWhitespace === false ? value : value.trim();
45
+ });
46
+ const log = (level: LogEntry["level"]) => (message: string | Error) =>
47
+ Effect.sync(() => {
48
+ recording.logs.push({ level, message: messageText(message) });
49
+ });
50
+ const layer = Layer.succeed(ActionRunner, {
51
+ addPath: (path) =>
52
+ Effect.sync(() => {
53
+ recording.paths.push(path);
54
+ }),
55
+ debug: log("debug"),
56
+ endGroup: () =>
57
+ Effect.sync(() => {
58
+ recording.groups.pop();
59
+ }),
60
+ error: log("error"),
61
+ exportVariable: (name, value) =>
62
+ Effect.sync(() => {
63
+ recording.variables.set(name, value);
64
+ }),
65
+ getBooleanInput: Effect.fn("TestActionRunner.getBooleanInput")(function* getBooleanInput(
66
+ name: string,
67
+ inputOptions?: InputOptions,
68
+ ) {
69
+ const value = yield* readInput(name, inputOptions).pipe(
70
+ Effect.mapError(
71
+ (cause) =>
72
+ new ActionInputError({ cause, name, reason: ActionInputErrorReason.InvalidType }),
73
+ ),
74
+ );
75
+ if (trueInputs.has(value)) {
76
+ return true;
77
+ }
78
+ if (falseInputs.has(value)) {
79
+ return false;
80
+ }
81
+ return yield* new ActionInputError({
82
+ name,
83
+ reason: ActionInputErrorReason.InvalidType,
84
+ value,
85
+ });
86
+ }),
87
+ getIDToken: () => Effect.succeed(Redacted.make(options.idToken ?? "test-oidc-token")),
88
+ getInput: readInput,
89
+ getMultilineInput: Effect.fn("ActionRunnerTest.getMultilineInput")(function* getMultilineInput(
90
+ name: string,
91
+ inputOptions?: InputOptions,
92
+ ) {
93
+ const value = yield* readInput(name, { ...inputOptions, trimWhitespace: false });
94
+ return value
95
+ .split("\n")
96
+ .filter((line) => line !== "")
97
+ .map((line) => (inputOptions?.trimWhitespace === false ? line : line.trim()));
98
+ }),
99
+ getState: (name) => Effect.sync(() => recording.state.get(name) ?? ""),
100
+ group: <A, E, R>(name: string, fn: () => Effect.Effect<A, E, R>) =>
101
+ Effect.acquireUseRelease(
102
+ Effect.sync(() => {
103
+ recording.groups.push(name);
104
+ }),
105
+ fn,
106
+ () =>
107
+ Effect.sync(() => {
108
+ recording.groups.pop();
109
+ }),
110
+ ).pipe(Effect.withSpan("ActionRunnerTest.group", { attributes: { group: name } })),
111
+ info: log("info"),
112
+ notice: log("notice"),
113
+ saveState: (name, value) =>
114
+ Effect.sync(() => {
115
+ recording.state.set(name, Predicate.isString(value) ? value : JSON.stringify(value));
116
+ }),
117
+ setFailed: (message) =>
118
+ Effect.sync(() => {
119
+ recording.failures.push(messageText(message));
120
+ }),
121
+ setOutput: (name, value) =>
122
+ Effect.sync(() => {
123
+ recording.outputs.set(name, value);
124
+ }),
125
+ setSecret: (secret) =>
126
+ Effect.sync(() => {
127
+ recording.secrets.push(secret);
128
+ }),
129
+ startGroup: (name) =>
130
+ Effect.sync(() => {
131
+ recording.groups.push(name);
132
+ }),
133
+ warning: log("warning"),
134
+ });
135
+ return { layer, recording };
136
+ };
137
+
138
+ export const { layer } = make();