@savvy-web/github-action-builder 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/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # @savvy-web/github-action-builder
2
+
3
+ A zero-config build tool for creating GitHub Actions from TypeScript source
4
+ code. Bundles your action with [@vercel/ncc](https://github.com/vercel/ncc),
5
+ validates `action.yml` against GitHub's official schema, and outputs
6
+ production-ready Node.js 24 actions.
7
+
8
+ ## Features
9
+
10
+ - **Zero-config** - Auto-detects entry points from `src/main.ts`, `src/pre.ts`,
11
+ `src/post.ts`
12
+ - **Node.js 24** - Builds modern ESM actions for the latest GitHub Actions
13
+ runtime
14
+ - **Schema validation** - Validates `action.yml` against GitHub's official
15
+ metadata specification
16
+ - **Single-file bundles** - All dependencies inlined using @vercel/ncc
17
+ - **CI-aware** - Strict validation in CI, warnings-only locally
18
+
19
+ ## Quick Start
20
+
21
+ Create a new GitHub Action project with a single command:
22
+
23
+ ```bash
24
+ npx @savvy-web/github-action-builder init my-action
25
+ cd my-action
26
+ npm install
27
+ npm run build
28
+ ```
29
+
30
+ That's it! Your action is built and ready. The `init` command generates a
31
+ complete project:
32
+
33
+ ```text
34
+ my-action/
35
+ ├── src/
36
+ │ ├── main.ts # Main action entry point
37
+ │ ├── pre.ts # Pre-action hook
38
+ │ └── post.ts # Post-action cleanup
39
+ ├── action.yml # GitHub Action metadata
40
+ ├── action.config.ts # Build configuration
41
+ ├── package.json # Dependencies and scripts
42
+ └── tsconfig.json # TypeScript configuration
43
+ ```
44
+
45
+ Edit `src/main.ts` with your action logic, then rebuild with `npm run build`.
46
+ Your bundled action is in `dist/main.js`, ready to commit and use.
47
+
48
+ ## Basic Usage
49
+
50
+ ### Initialize
51
+
52
+ Create a new GitHub Action project:
53
+
54
+ ```bash
55
+ npx @savvy-web/github-action-builder init my-action
56
+ ```
57
+
58
+ ### Build
59
+
60
+ Bundle all entry points into `dist/`:
61
+
62
+ ```bash
63
+ npm run build
64
+ # or directly:
65
+ npx @savvy-web/github-action-builder build
66
+ ```
67
+
68
+ ### Validate
69
+
70
+ Check your `action.yml` and configuration without building:
71
+
72
+ ```bash
73
+ npm run validate
74
+ # or directly:
75
+ npx @savvy-web/github-action-builder validate
76
+ ```
77
+
78
+ ## Project Structure
79
+
80
+ The builder expects this structure:
81
+
82
+ ```text
83
+ my-action/
84
+ ├── src/
85
+ │ ├── main.ts # Required - main action entry point
86
+ │ ├── pre.ts # Optional - runs before main
87
+ │ └── post.ts # Optional - runs after main (cleanup)
88
+ ├── action.yml # GitHub Action metadata (runs.using: "node24")
89
+ ├── action.config.ts # Optional configuration
90
+ └── package.json
91
+ ```
92
+
93
+ ## Configuration
94
+
95
+ Customize `action.config.ts` for your project:
96
+
97
+ ```typescript
98
+ import { GitHubAction } from "@savvy-web/github-action-builder";
99
+
100
+ export default GitHubAction.create({
101
+ entries: {
102
+ main: "src/main.ts",
103
+ post: "src/cleanup.ts",
104
+ },
105
+ build: {
106
+ minify: true,
107
+ sourceMap: false,
108
+ },
109
+ });
110
+ ```
111
+
112
+ ## action.yml Requirements
113
+
114
+ Your `action.yml` must use Node.js 24:
115
+
116
+ ```yaml
117
+ name: "My Action"
118
+ description: "Does something useful"
119
+ runs:
120
+ using: "node24"
121
+ main: "dist/main.js"
122
+ post: "dist/post.js" # Optional
123
+ ```
124
+
125
+ ## Documentation
126
+
127
+ - [Getting Started](./docs/getting-started.md) - Installation and first build
128
+ - [Configuration](./docs/configuration.md) - All configuration options
129
+ - [CLI Reference](./docs/cli-reference.md) - Complete command reference
130
+ - [Architecture](./docs/architecture.md) - How it works internally
131
+ - [Troubleshooting](./docs/troubleshooting.md) - Common issues and solutions
132
+
133
+ ## Programmatic API
134
+
135
+ Use the builder programmatically in your scripts:
136
+
137
+ ```typescript
138
+ import { GitHubAction } from "@savvy-web/github-action-builder";
139
+
140
+ const action = GitHubAction.create();
141
+ const result = await action.build();
142
+
143
+ if (result.success) {
144
+ console.log(`Built ${result.build?.entries.length} entry points`);
145
+ }
146
+ ```
147
+
148
+ ## Requirements
149
+
150
+ - Node.js 24+
151
+ - TypeScript source files
152
+ - `action.yml` with `runs.using: "node24"`
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,294 @@
1
+ #!/usr/bin/env node
2
+ import { Args, Command, Options } from "@effect/cli";
3
+ import { NodeContext, NodeRuntime } from "@effect/platform-node";
4
+ import { Layer, AppLayer, BuildService, ValidationService, ConfigService, Effect, existsSync, resolve, mkdirSync, writeFileSync, Console, Option } from "../123.js";
5
+ const configOption = Options.file("config").pipe(Options.withAlias("c"), Options.withDescription("Path to configuration file"), Options.optional);
6
+ const quietOption = Options.boolean("quiet").pipe(Options.withAlias("q"), Options.withDescription("Suppress non-error output"), Options.withDefault(false));
7
+ const noValidateOption = Options.boolean("no-validate").pipe(Options.withDescription("Skip validation step"), Options.withDefault(false));
8
+ const buildHandler = ({ config, quiet, noValidate })=>Effect.gen(function*() {
9
+ const configService = yield* ConfigService;
10
+ const validationService = yield* ValidationService;
11
+ const buildService = yield* BuildService;
12
+ const cwd = process.cwd();
13
+ if (!quiet) yield* Console.log("Loading configuration...");
14
+ const loadOptions = Option.isSome(config) ? {
15
+ cwd,
16
+ configPath: config.value
17
+ } : {
18
+ cwd
19
+ };
20
+ const configResult = yield* configService.load(loadOptions);
21
+ if (!quiet) if (configResult.usingDefaults) yield* Console.log(" Using default configuration");
22
+ else yield* Console.log(` Found ${configResult.configPath}`);
23
+ if (!noValidate) {
24
+ if (!quiet) yield* Console.log("\nValidating...");
25
+ const validationResult = yield* validationService.validate(configResult.config, {
26
+ cwd
27
+ });
28
+ if (!validationResult.valid) {
29
+ yield* Console.error(`\n${validationService.formatResult(validationResult)}`);
30
+ return yield* Effect.fail(new Error("Validation failed"));
31
+ }
32
+ if (!quiet && validationResult.warnings.length > 0) yield* Console.log(validationService.formatResult(validationResult));
33
+ else if (!quiet) yield* Console.log(" All checks passed");
34
+ }
35
+ if (!quiet) yield* Console.log("\nBuilding...");
36
+ const buildResult = yield* buildService.build(configResult.config, {
37
+ cwd
38
+ });
39
+ if (!buildResult.success) {
40
+ yield* Console.error(`\nBuild failed: ${buildResult.error}`);
41
+ return yield* Effect.fail(new Error("Build failed"));
42
+ }
43
+ if (!quiet) {
44
+ yield* Console.log(`\n${buildService.formatResult(buildResult)}`);
45
+ yield* Console.log("\nBuild completed successfully!");
46
+ }
47
+ });
48
+ const buildCommand = Command.make("build", {
49
+ config: configOption,
50
+ quiet: quietOption,
51
+ noValidate: noValidateOption
52
+ }, buildHandler);
53
+ const actionNameArg = Args.text({
54
+ name: "action-name"
55
+ }).pipe(Args.withDescription("Name of the GitHub Action (also the output directory)"));
56
+ const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite existing files"), Options.withDefault(false));
57
+ const getPackageVersion = ()=>"0.1.0";
58
+ const generatePackageJson = (name)=>{
59
+ const version = getPackageVersion();
60
+ const pkg = {
61
+ name,
62
+ version: "0.0.0",
63
+ private: true,
64
+ type: "module",
65
+ scripts: {
66
+ build: "github-action-builder build",
67
+ validate: "github-action-builder validate",
68
+ typecheck: "tsc --noEmit"
69
+ },
70
+ devDependencies: {
71
+ "@savvy-web/github-action-builder": `^${version}`,
72
+ typescript: "^5.9.3"
73
+ },
74
+ dependencies: {
75
+ "@actions/core": "^1.11.1",
76
+ "@actions/github": "^6.0.0"
77
+ }
78
+ };
79
+ return `${JSON.stringify(pkg, null, 2)}\n`;
80
+ };
81
+ const generateTsConfig = ()=>{
82
+ const config = {
83
+ compilerOptions: {
84
+ target: "ES2022",
85
+ module: "NodeNext",
86
+ moduleResolution: "NodeNext",
87
+ strict: true,
88
+ esModuleInterop: true,
89
+ skipLibCheck: true,
90
+ forceConsistentCasingInFileNames: true,
91
+ declaration: false,
92
+ outDir: "dist",
93
+ rootDir: "src"
94
+ },
95
+ include: [
96
+ "src/**/*.ts",
97
+ "action.config.ts"
98
+ ],
99
+ exclude: [
100
+ "node_modules",
101
+ "dist"
102
+ ]
103
+ };
104
+ return `${JSON.stringify(config, null, 2)}\n`;
105
+ };
106
+ const generateActionYml = (name)=>`name: "${name}"
107
+ description: "A GitHub Action built with @savvy-web/github-action-builder"
108
+ author: ""
109
+
110
+ inputs:
111
+ example-input:
112
+ description: "An example input"
113
+ required: false
114
+ default: "hello"
115
+
116
+ outputs:
117
+ example-output:
118
+ description: "An example output"
119
+
120
+ runs:
121
+ using: "node24"
122
+ main: "dist/main.js"
123
+ pre: "dist/pre.js"
124
+ post: "dist/post.js"
125
+
126
+ branding:
127
+ icon: "zap"
128
+ color: "blue"
129
+ `;
130
+ const defaultConfig = `import { GitHubAction } from "@savvy-web/github-action-builder";
131
+
132
+ export default GitHubAction.create({
133
+ // Entry points are auto-detected from src/main.ts, src/pre.ts, src/post.ts
134
+ // Uncomment to customize:
135
+ // entries: {
136
+ // main: "src/main.ts",
137
+ // pre: "src/pre.ts",
138
+ // post: "src/post.ts",
139
+ // },
140
+
141
+ // Build options
142
+ // build: {
143
+ // minify: true,
144
+ // sourceMap: false,
145
+ // target: "es2022",
146
+ // },
147
+
148
+ // Validation options
149
+ // validation: {
150
+ // strict: undefined, // Auto-detects CI environment
151
+ // },
152
+ });
153
+ `;
154
+ const mainTemplate = `import * as core from "@actions/core";
155
+
156
+ async function run(): Promise<void> {
157
+ try {
158
+ const input = core.getInput("example-input");
159
+ core.info(\`Running main action with input: \${input}\`);
160
+
161
+ // Your main action logic goes here
162
+
163
+ core.setOutput("example-output", "success");
164
+ } catch (error) {
165
+ if (error instanceof Error) {
166
+ core.setFailed(error.message);
167
+ } else {
168
+ core.setFailed("An unexpected error occurred");
169
+ }
170
+ }
171
+ }
172
+
173
+ run();
174
+ `;
175
+ const preTemplate = `import * as core from "@actions/core";
176
+
177
+ async function run(): Promise<void> {
178
+ try {
179
+ core.info("Running pre action...");
180
+
181
+ // Your pre-action setup logic goes here
182
+ // This runs before the main action
183
+ } catch (error) {
184
+ if (error instanceof Error) {
185
+ core.setFailed(error.message);
186
+ } else {
187
+ core.setFailed("An unexpected error occurred");
188
+ }
189
+ }
190
+ }
191
+
192
+ run();
193
+ `;
194
+ const postTemplate = `import * as core from "@actions/core";
195
+
196
+ async function run(): Promise<void> {
197
+ try {
198
+ core.info("Running post action...");
199
+
200
+ // Your post-action cleanup logic goes here
201
+ // This runs after the main action, even if it fails
202
+ } catch (error) {
203
+ if (error instanceof Error) {
204
+ core.warning(error.message);
205
+ } else {
206
+ core.warning("An unexpected error occurred during cleanup");
207
+ }
208
+ }
209
+ }
210
+
211
+ run();
212
+ `;
213
+ const writeFile = (path, content, force, createdFiles, skippedFiles)=>{
214
+ if (existsSync(path) && !force) return void skippedFiles.push(path);
215
+ writeFileSync(path, content, "utf-8");
216
+ createdFiles.push(path);
217
+ };
218
+ const initHandler = ({ actionName, force })=>Effect.gen(function*() {
219
+ const cwd = process.cwd();
220
+ const projectDir = resolve(cwd, actionName);
221
+ const createdFiles = [];
222
+ const skippedFiles = [];
223
+ if (existsSync(projectDir) && !force) {
224
+ yield* Console.error(`Directory already exists: ${actionName}`);
225
+ yield* Console.error("Use --force to overwrite existing files.");
226
+ return yield* Effect.fail(new Error("Directory exists"));
227
+ }
228
+ if (!existsSync(projectDir)) mkdirSync(projectDir, {
229
+ recursive: true
230
+ });
231
+ const srcDir = resolve(projectDir, "src");
232
+ if (!existsSync(srcDir)) mkdirSync(srcDir, {
233
+ recursive: true
234
+ });
235
+ writeFile(resolve(projectDir, "package.json"), generatePackageJson(actionName), force, createdFiles, skippedFiles);
236
+ writeFile(resolve(projectDir, "tsconfig.json"), generateTsConfig(), force, createdFiles, skippedFiles);
237
+ writeFile(resolve(projectDir, "action.yml"), generateActionYml(actionName), force, createdFiles, skippedFiles);
238
+ writeFile(resolve(projectDir, "action.config.ts"), defaultConfig, force, createdFiles, skippedFiles);
239
+ writeFile(resolve(srcDir, "main.ts"), mainTemplate, force, createdFiles, skippedFiles);
240
+ writeFile(resolve(srcDir, "pre.ts"), preTemplate, force, createdFiles, skippedFiles);
241
+ writeFile(resolve(srcDir, "post.ts"), postTemplate, force, createdFiles, skippedFiles);
242
+ yield* Console.log(`Created ${actionName}/`);
243
+ if (createdFiles.length > 0) for (const file of createdFiles)yield* Console.log(` ${file.replace(`${projectDir}/`, "")}`);
244
+ if (skippedFiles.length > 0) {
245
+ yield* Console.log("\nSkipped existing files (use --force to overwrite):");
246
+ for (const file of skippedFiles)yield* Console.log(` ${file.replace(`${projectDir}/`, "")}`);
247
+ }
248
+ yield* Console.log("\nNext steps:");
249
+ yield* Console.log(` cd ${actionName}`);
250
+ yield* Console.log(" npm install");
251
+ yield* Console.log(" npm run build");
252
+ });
253
+ const initCommand = Command.make("init", {
254
+ actionName: actionNameArg,
255
+ force: forceOption
256
+ }, initHandler);
257
+ const validateHandler = ({ config, quiet })=>Effect.gen(function*() {
258
+ const configService = yield* ConfigService;
259
+ const validationService = yield* ValidationService;
260
+ const cwd = process.cwd();
261
+ if (!quiet) yield* Console.log("Loading configuration...");
262
+ const loadOptions = Option.isSome(config) ? {
263
+ cwd,
264
+ configPath: config.value
265
+ } : {
266
+ cwd
267
+ };
268
+ const configResult = yield* configService.load(loadOptions);
269
+ if (!quiet) if (configResult.usingDefaults) yield* Console.log(" Using default configuration");
270
+ else yield* Console.log(` Found ${configResult.configPath}`);
271
+ if (!quiet) yield* Console.log("\nValidating...");
272
+ const validationResult = yield* validationService.validate(configResult.config, {
273
+ cwd
274
+ });
275
+ yield* Console.log(`\n${validationService.formatResult(validationResult)}`);
276
+ if (!validationResult.valid) return yield* Effect.fail(new Error("Validation failed"));
277
+ if (!quiet) yield* Console.log("\nValidation completed successfully!");
278
+ });
279
+ const validateCommand = Command.make("validate", {
280
+ config: configOption,
281
+ quiet: quietOption
282
+ }, validateHandler);
283
+ const rootCommand = Command.make("github-action-builder").pipe(Command.withSubcommands([
284
+ buildCommand,
285
+ validateCommand,
286
+ initCommand
287
+ ]));
288
+ const cli = Command.run(rootCommand, {
289
+ name: "github-action-builder",
290
+ version: "0.1.0"
291
+ });
292
+ const CliLayer = Layer.merge(AppLayer, NodeContext.layer);
293
+ const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(CliLayer));
294
+ NodeRuntime.runMain(main);