@savvy-web/github-action-builder 0.7.4 → 0.7.6

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.
@@ -0,0 +1,247 @@
1
+ import { Console, Effect } from "effect";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { Args, Command, Options } from "@effect/cli";
5
+
6
+ //#region src/cli/commands/init.ts
7
+ /* v8 ignore start - CLI commands require integration testing */
8
+ /**
9
+ * Init command for GitHub Action Builder CLI.
10
+ */
11
+ /**
12
+ * Action name positional argument.
13
+ */
14
+ const actionNameArg = Args.text({ name: "action-name" }).pipe(Args.withDescription("Name of the GitHub Action (also the output directory)"));
15
+ /**
16
+ * Force overwrite option.
17
+ */
18
+ const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite existing files"), Options.withDefault(false));
19
+ /**
20
+ * Get current package version (replaced at build time).
21
+ */
22
+ const getPackageVersion = () => {
23
+ return process.env.__PACKAGE_VERSION__ ?? "0.0.0";
24
+ };
25
+ /**
26
+ * Generate package.json content.
27
+ */
28
+ const generatePackageJson = (name) => {
29
+ const pkg = {
30
+ name,
31
+ version: "0.0.0",
32
+ private: true,
33
+ type: "module",
34
+ scripts: {
35
+ build: "github-action-builder build",
36
+ validate: "github-action-builder validate",
37
+ typecheck: "tsc --noEmit"
38
+ },
39
+ devDependencies: {
40
+ "@savvy-web/github-action-builder": `^${getPackageVersion()}`,
41
+ typescript: "^5.9.3"
42
+ },
43
+ dependencies: {
44
+ "@actions/core": "^1.11.1",
45
+ "@actions/github": "^6.0.0"
46
+ }
47
+ };
48
+ return `${JSON.stringify(pkg, null, 2)}\n`;
49
+ };
50
+ /**
51
+ * Generate tsconfig.json content.
52
+ */
53
+ const generateTsConfig = () => {
54
+ return `${JSON.stringify({
55
+ compilerOptions: {
56
+ target: "ES2022",
57
+ module: "NodeNext",
58
+ moduleResolution: "NodeNext",
59
+ strict: true,
60
+ esModuleInterop: true,
61
+ skipLibCheck: true,
62
+ forceConsistentCasingInFileNames: true,
63
+ declaration: false,
64
+ outDir: "dist",
65
+ rootDir: "src"
66
+ },
67
+ include: ["src/**/*.ts", "action.config.ts"],
68
+ exclude: ["node_modules", "dist"]
69
+ }, null, 2)}\n`;
70
+ };
71
+ /**
72
+ * Generate action.yml content.
73
+ */
74
+ const generateActionYml = (name) => {
75
+ return `name: "${name}"
76
+ description: "A GitHub Action built with @savvy-web/github-action-builder"
77
+ author: ""
78
+
79
+ inputs:
80
+ example-input:
81
+ description: "An example input"
82
+ required: false
83
+ default: "hello"
84
+
85
+ outputs:
86
+ example-output:
87
+ description: "An example output"
88
+
89
+ runs:
90
+ using: "node24"
91
+ main: "dist/main.js"
92
+ pre: "dist/pre.js"
93
+ post: "dist/post.js"
94
+
95
+ branding:
96
+ icon: "zap"
97
+ color: "blue"
98
+ `;
99
+ };
100
+ /**
101
+ * Default configuration file content.
102
+ */
103
+ const defaultConfig = `import { GitHubAction } from "@savvy-web/github-action-builder";
104
+
105
+ export default GitHubAction.create({
106
+ // Entry points are auto-detected from src/main.ts, src/pre.ts, src/post.ts
107
+ // Uncomment to customize:
108
+ // entries: {
109
+ // main: "src/main.ts",
110
+ // pre: "src/pre.ts",
111
+ // post: "src/post.ts",
112
+ // },
113
+
114
+ // Build options
115
+ // build: {
116
+ // minify: true,
117
+ // sourceMap: false,
118
+ // },
119
+
120
+ // Validation options
121
+ // validation: {
122
+ // strict: undefined, // Auto-detects CI environment
123
+ // },
124
+ });
125
+ `;
126
+ /**
127
+ * Generate src/main.ts content.
128
+ */
129
+ const mainTemplate = `import * as core from "@actions/core";
130
+
131
+ async function run(): Promise<void> {
132
+ try {
133
+ const input = core.getInput("example-input");
134
+ core.info(\`Running main action with input: \${input}\`);
135
+
136
+ // Your main action logic goes here
137
+
138
+ core.setOutput("example-output", "success");
139
+ } catch (error) {
140
+ if (error instanceof Error) {
141
+ core.setFailed(error.message);
142
+ } else {
143
+ core.setFailed("An unexpected error occurred");
144
+ }
145
+ }
146
+ }
147
+
148
+ run();
149
+ `;
150
+ /**
151
+ * Generate src/pre.ts content.
152
+ */
153
+ const preTemplate = `import * as core from "@actions/core";
154
+
155
+ async function run(): Promise<void> {
156
+ try {
157
+ core.info("Running pre action...");
158
+
159
+ // Your pre-action setup logic goes here
160
+ // This runs before the main action
161
+ } catch (error) {
162
+ if (error instanceof Error) {
163
+ core.setFailed(error.message);
164
+ } else {
165
+ core.setFailed("An unexpected error occurred");
166
+ }
167
+ }
168
+ }
169
+
170
+ run();
171
+ `;
172
+ /**
173
+ * Generate src/post.ts content.
174
+ */
175
+ const postTemplate = `import * as core from "@actions/core";
176
+
177
+ async function run(): Promise<void> {
178
+ try {
179
+ core.info("Running post action...");
180
+
181
+ // Your post-action cleanup logic goes here
182
+ // This runs after the main action, even if it fails
183
+ } catch (error) {
184
+ if (error instanceof Error) {
185
+ core.warning(error.message);
186
+ } else {
187
+ core.warning("An unexpected error occurred during cleanup");
188
+ }
189
+ }
190
+ }
191
+
192
+ run();
193
+ `;
194
+ /**
195
+ * Write a file if it doesn't exist or force is enabled.
196
+ */
197
+ const writeFile = (path, content, force, createdFiles, skippedFiles) => {
198
+ if (existsSync(path) && !force) {
199
+ skippedFiles.push(path);
200
+ return;
201
+ }
202
+ writeFileSync(path, content, "utf-8");
203
+ createdFiles.push(path);
204
+ };
205
+ /**
206
+ * Init command handler.
207
+ */
208
+ const initHandler = ({ actionName, force }) => Effect.gen(function* () {
209
+ const projectDir = resolve(process.cwd(), actionName);
210
+ const createdFiles = [];
211
+ const skippedFiles = [];
212
+ if (existsSync(projectDir) && !force) {
213
+ yield* Console.error(`Directory already exists: ${actionName}`);
214
+ yield* Console.error("Use --force to overwrite existing files.");
215
+ return yield* Effect.fail(/* @__PURE__ */ new Error("Directory exists"));
216
+ }
217
+ if (!existsSync(projectDir)) mkdirSync(projectDir, { recursive: true });
218
+ const srcDir = resolve(projectDir, "src");
219
+ if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
220
+ writeFile(resolve(projectDir, "package.json"), generatePackageJson(actionName), force, createdFiles, skippedFiles);
221
+ writeFile(resolve(projectDir, "tsconfig.json"), generateTsConfig(), force, createdFiles, skippedFiles);
222
+ writeFile(resolve(projectDir, "action.yml"), generateActionYml(actionName), force, createdFiles, skippedFiles);
223
+ writeFile(resolve(projectDir, "action.config.ts"), defaultConfig, force, createdFiles, skippedFiles);
224
+ writeFile(resolve(srcDir, "main.ts"), mainTemplate, force, createdFiles, skippedFiles);
225
+ writeFile(resolve(srcDir, "pre.ts"), preTemplate, force, createdFiles, skippedFiles);
226
+ writeFile(resolve(srcDir, "post.ts"), postTemplate, force, createdFiles, skippedFiles);
227
+ yield* Console.log(`Created ${actionName}/`);
228
+ if (createdFiles.length > 0) for (const file of createdFiles) yield* Console.log(` ${file.replace(`${projectDir}/`, "")}`);
229
+ if (skippedFiles.length > 0) {
230
+ yield* Console.log("\nSkipped existing files (use --force to overwrite):");
231
+ for (const file of skippedFiles) yield* Console.log(` ${file.replace(`${projectDir}/`, "")}`);
232
+ }
233
+ yield* Console.log("\nNext steps:");
234
+ yield* Console.log(` cd ${actionName}`);
235
+ yield* Console.log(" npm install");
236
+ yield* Console.log(" npm run build");
237
+ });
238
+ /**
239
+ * Init command - creates a new GitHub Action project.
240
+ */
241
+ const initCommand = Command.make("init", {
242
+ actionName: actionNameArg,
243
+ force: forceOption
244
+ }, initHandler);
245
+
246
+ //#endregion
247
+ export { initCommand };
@@ -0,0 +1,42 @@
1
+ import { ConfigService } from "../../services/config.js";
2
+ import { ValidationService } from "../../services/validation.js";
3
+ import { configOption, quietOption } from "./build.js";
4
+ import { Console, Effect, Option } from "effect";
5
+ import { Command } from "@effect/cli";
6
+
7
+ //#region src/cli/commands/validate.ts
8
+ /* v8 ignore start - CLI commands require integration testing */
9
+ /**
10
+ * Validate command for GitHub Action Builder CLI.
11
+ */
12
+ /**
13
+ * Validate command handler using Effect services.
14
+ */
15
+ const validateHandler = ({ config, quiet }) => Effect.gen(function* () {
16
+ const configService = yield* ConfigService;
17
+ const validationService = yield* ValidationService;
18
+ const cwd = process.cwd();
19
+ if (!quiet) yield* Console.log("Loading configuration...");
20
+ const loadOptions = Option.isSome(config) ? {
21
+ cwd,
22
+ configPath: config.value
23
+ } : { cwd };
24
+ const configResult = yield* configService.load(loadOptions);
25
+ if (!quiet) if (configResult.usingDefaults) yield* Console.log(" Using default configuration");
26
+ else yield* Console.log(` Found ${configResult.configPath}`);
27
+ if (!quiet) yield* Console.log("\nValidating...");
28
+ const validationResult = yield* validationService.validate(configResult.config, { cwd });
29
+ yield* Console.log(`\n${validationService.formatResult(validationResult)}`);
30
+ if (!validationResult.valid) return yield* Effect.fail(/* @__PURE__ */ new Error("Validation failed"));
31
+ if (!quiet) yield* Console.log("\nValidation completed successfully!");
32
+ });
33
+ /**
34
+ * Validate command - checks action.yml and configuration.
35
+ */
36
+ const validateCommand = Command.make("validate", {
37
+ config: configOption,
38
+ quiet: quietOption
39
+ }, validateHandler);
40
+
41
+ //#endregion
42
+ export { validateCommand };
package/errors.js ADDED
@@ -0,0 +1,284 @@
1
+ import { Data } from "effect";
2
+
3
+ //#region src/errors.ts
4
+ /**
5
+ * Typed error classes for GitHub Action Builder.
6
+ *
7
+ * @remarks
8
+ * Uses Effect's Data.TaggedError pattern for type-safe error handling
9
+ * with pattern matching support via the `_tag` discriminant.
10
+ *
11
+ * @example Pattern matching on errors
12
+ * ```typescript
13
+ * import { ConfigNotFound, ConfigInvalid } from "@savvy-web/github-action-builder";
14
+ *
15
+ * function handleError(error: ConfigError): string {
16
+ * switch (error._tag) {
17
+ * case "ConfigNotFound":
18
+ * return `Config not found at ${error.path}`;
19
+ * case "ConfigInvalid":
20
+ * return `Invalid config: ${error.errors.join(", ")}`;
21
+ * case "ConfigLoadFailed":
22
+ * return `Failed to load config: ${error.cause}`;
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ /**
28
+ * Base class for ConfigNotFound error.
29
+ *
30
+ * @privateRemarks
31
+ * This export is required for api-extractor documentation generation.
32
+ * Effect's Data.TaggedError creates an anonymous base class that must be
33
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
34
+ *
35
+ * @internal
36
+ */
37
+ const ConfigNotFoundBase = Data.TaggedError("ConfigNotFound");
38
+ /**
39
+ * Error when configuration file is not found.
40
+ *
41
+ * @public
42
+ */
43
+ var ConfigNotFound = class extends ConfigNotFoundBase {};
44
+ /**
45
+ * Base class for ConfigInvalid error.
46
+ *
47
+ * @privateRemarks
48
+ * This export is required for api-extractor documentation generation.
49
+ * Effect's Data.TaggedError creates an anonymous base class that must be
50
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
51
+ *
52
+ * @internal
53
+ */
54
+ const ConfigInvalidBase = Data.TaggedError("ConfigInvalid");
55
+ /**
56
+ * Error when configuration file exists but contains invalid content.
57
+ *
58
+ * @public
59
+ */
60
+ var ConfigInvalid = class extends ConfigInvalidBase {};
61
+ /**
62
+ * Base class for ConfigLoadFailed error.
63
+ *
64
+ * @privateRemarks
65
+ * This export is required for api-extractor documentation generation.
66
+ * Effect's Data.TaggedError creates an anonymous base class that must be
67
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
68
+ *
69
+ * @internal
70
+ */
71
+ const ConfigLoadFailedBase = Data.TaggedError("ConfigLoadFailed");
72
+ /**
73
+ * Error when configuration file fails to load (import error, syntax error, etc.).
74
+ *
75
+ * @public
76
+ */
77
+ var ConfigLoadFailed = class extends ConfigLoadFailedBase {};
78
+ /**
79
+ * Base class for MainEntryMissing error.
80
+ *
81
+ * @privateRemarks
82
+ * This export is required for api-extractor documentation generation.
83
+ * Effect's Data.TaggedError creates an anonymous base class that must be
84
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
85
+ *
86
+ * @internal
87
+ */
88
+ const MainEntryMissingBase = Data.TaggedError("MainEntryMissing");
89
+ /**
90
+ * Error when the required main entry point is missing.
91
+ *
92
+ * @public
93
+ */
94
+ var MainEntryMissing = class extends MainEntryMissingBase {};
95
+ /**
96
+ * Base class for EntryFileMissing error.
97
+ *
98
+ * @privateRemarks
99
+ * This export is required for api-extractor documentation generation.
100
+ * Effect's Data.TaggedError creates an anonymous base class that must be
101
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
102
+ *
103
+ * @internal
104
+ */
105
+ const EntryFileMissingBase = Data.TaggedError("EntryFileMissing");
106
+ /**
107
+ * Error when an explicitly specified entry file is missing.
108
+ *
109
+ * @public
110
+ */
111
+ var EntryFileMissing = class extends EntryFileMissingBase {};
112
+ /**
113
+ * Base class for ActionYmlMissing error.
114
+ *
115
+ * @privateRemarks
116
+ * This export is required for api-extractor documentation generation.
117
+ * Effect's Data.TaggedError creates an anonymous base class that must be
118
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
119
+ *
120
+ * @internal
121
+ */
122
+ const ActionYmlMissingBase = Data.TaggedError("ActionYmlMissing");
123
+ /**
124
+ * Error when action.yml file is missing.
125
+ *
126
+ * @public
127
+ */
128
+ var ActionYmlMissing = class extends ActionYmlMissingBase {};
129
+ /**
130
+ * Base class for ActionYmlSyntaxError error.
131
+ *
132
+ * @privateRemarks
133
+ * This export is required for api-extractor documentation generation.
134
+ * Effect's Data.TaggedError creates an anonymous base class that must be
135
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
136
+ *
137
+ * @internal
138
+ */
139
+ const ActionYmlSyntaxErrorBase = Data.TaggedError("ActionYmlSyntaxError");
140
+ /**
141
+ * Error when action.yml has invalid YAML syntax.
142
+ *
143
+ * @public
144
+ */
145
+ var ActionYmlSyntaxError = class extends ActionYmlSyntaxErrorBase {};
146
+ /**
147
+ * Base class for ActionYmlSchemaError error.
148
+ *
149
+ * @privateRemarks
150
+ * This export is required for api-extractor documentation generation.
151
+ * Effect's Data.TaggedError creates an anonymous base class that must be
152
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
153
+ *
154
+ * @internal
155
+ */
156
+ const ActionYmlSchemaErrorBase = Data.TaggedError("ActionYmlSchemaError");
157
+ /**
158
+ * Error when action.yml fails schema validation.
159
+ *
160
+ * @public
161
+ */
162
+ var ActionYmlSchemaError = class extends ActionYmlSchemaErrorBase {};
163
+ /**
164
+ * Base class for ValidationFailed error.
165
+ *
166
+ * @privateRemarks
167
+ * This export is required for api-extractor documentation generation.
168
+ * Effect's Data.TaggedError creates an anonymous base class that must be
169
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
170
+ *
171
+ * @internal
172
+ */
173
+ const ValidationFailedBase = Data.TaggedError("ValidationFailed");
174
+ /**
175
+ * Error when validation fails in strict mode (CI environment).
176
+ *
177
+ * @public
178
+ */
179
+ var ValidationFailed = class extends ValidationFailedBase {};
180
+ /**
181
+ * Base class for BundleFailed error.
182
+ *
183
+ * @privateRemarks
184
+ * This export is required for api-extractor documentation generation.
185
+ * Effect's Data.TaggedError creates an anonymous base class that must be
186
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
187
+ *
188
+ * @internal
189
+ */
190
+ const BundleFailedBase = Data.TaggedError("BundleFailed");
191
+ /**
192
+ * Error when bundling with rsbuild fails.
193
+ *
194
+ * @public
195
+ */
196
+ var BundleFailed = class extends BundleFailedBase {};
197
+ /**
198
+ * Base class for WriteError error.
199
+ *
200
+ * @privateRemarks
201
+ * This export is required for api-extractor documentation generation.
202
+ * Effect's Data.TaggedError creates an anonymous base class that must be
203
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
204
+ *
205
+ * @internal
206
+ */
207
+ const WriteErrorBase = Data.TaggedError("WriteError");
208
+ /**
209
+ * Error when writing output files fails.
210
+ *
211
+ * @public
212
+ */
213
+ var WriteError = class extends WriteErrorBase {};
214
+ /**
215
+ * Base class for CleanError error.
216
+ *
217
+ * @privateRemarks
218
+ * This export is required for api-extractor documentation generation.
219
+ * Effect's Data.TaggedError creates an anonymous base class that must be
220
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
221
+ *
222
+ * @internal
223
+ */
224
+ const CleanErrorBase = Data.TaggedError("CleanError");
225
+ /**
226
+ * Error when cleaning the output directory fails.
227
+ *
228
+ * @public
229
+ */
230
+ var CleanError = class extends CleanErrorBase {};
231
+ /**
232
+ * Base class for BuildFailed error.
233
+ *
234
+ * @privateRemarks
235
+ * This export is required for api-extractor documentation generation.
236
+ * Effect's Data.TaggedError creates an anonymous base class that must be
237
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
238
+ *
239
+ * @internal
240
+ */
241
+ const BuildFailedBase = Data.TaggedError("BuildFailed");
242
+ /**
243
+ * Error when the build process fails overall.
244
+ *
245
+ * @public
246
+ */
247
+ var BuildFailed = class extends BuildFailedBase {};
248
+ /**
249
+ * Base class for PersistLocalError error.
250
+ *
251
+ * @privateRemarks
252
+ * This export is required for api-extractor documentation generation.
253
+ * Effect's Data.TaggedError creates an anonymous base class that must be
254
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
255
+ *
256
+ * @internal
257
+ */
258
+ const PersistLocalErrorBase = Data.TaggedError("PersistLocalError");
259
+ /**
260
+ * Error when persisting build output to local action directory fails.
261
+ *
262
+ * @public
263
+ */
264
+ var PersistLocalError = class extends PersistLocalErrorBase {};
265
+ /**
266
+ * Base class for ActionYmlPathError error.
267
+ *
268
+ * @privateRemarks
269
+ * This export is required for api-extractor documentation generation.
270
+ * Effect's Data.TaggedError creates an anonymous base class that must be
271
+ * explicitly exported to avoid "forgotten export" warnings. Do not delete.
272
+ *
273
+ * @internal
274
+ */
275
+ const ActionYmlPathErrorBase = Data.TaggedError("ActionYmlPathError");
276
+ /**
277
+ * Error when action.yml runs paths don't resolve correctly in destination.
278
+ *
279
+ * @public
280
+ */
281
+ var ActionYmlPathError = class extends ActionYmlPathErrorBase {};
282
+
283
+ //#endregion
284
+ export { ActionYmlMissing, ActionYmlMissingBase, ActionYmlPathError, ActionYmlPathErrorBase, ActionYmlSchemaError, ActionYmlSchemaErrorBase, ActionYmlSyntaxError, ActionYmlSyntaxErrorBase, BuildFailed, BuildFailedBase, BundleFailed, BundleFailedBase, CleanError, CleanErrorBase, ConfigInvalid, ConfigInvalidBase, ConfigLoadFailed, ConfigLoadFailedBase, ConfigNotFound, ConfigNotFoundBase, EntryFileMissing, EntryFileMissingBase, MainEntryMissing, MainEntryMissingBase, PersistLocalError, PersistLocalErrorBase, ValidationFailed, ValidationFailedBase, WriteError, WriteErrorBase };