@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/123.js +646 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/bin/github-action-builder.js +294 -0
- package/index.d.ts +1621 -0
- package/index.js +118 -0
- package/package.json +69 -0
- package/tsdoc-metadata.json +11 -0
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);
|