<div align="center">
<img src="logo.png" alt="gaji logo" width="200"/>
<h1>gaji</h1>
<p>Type-safe GitHub Actions workflows in TypeScript</p>
<p><em>GitHub Actions Justified Improvements</em></p>
<p>🍆 Named after the Korean word "가지" (gaji, eggplant) - a versatile ingredient that makes everything better!</p>
</div>
## Overview
`gaji` is a CLI tool that allows developers to write GitHub Actions workflows in TypeScript with full type safety, then compile them to YAML. It automatically fetches `action.yml` definitions and generates typed wrappers, so you get autocomplete and type checking for every action input and output.
## Features
- TypeScript-based workflow authoring with full type safety
- Automatic type generation from `action.yml` files
- Composite action and reusable workflow support
- File watching for development (`--watch`)
- Built-in QuickJS execution with `npx tsx` fallback
- GitHub Enterprise support
- Single binary distribution (Rust)
## Installation
### From npm
```bash
npm install -D gaji
```
### From cargo
```bash
cargo install gaji
```
## Quick Start
```bash
# Initialize a new project (creates workflows/ and generated/ directories)
gaji init
# Add actions and generate types
gaji add actions/checkout@v5
gaji add actions/setup-node@v4
# Run a one-time dev scan to generate types
gaji dev
# Build workflows to YAML
gaji build
```
## Usage
### Writing Workflows
Create TypeScript files in the `workflows/` directory:
```typescript
import { getAction, Job, Workflow } from "../generated/index.js";
const checkout = getAction("actions/checkout@v5");
const setupNode = getAction("actions/setup-node@v4");
const build = new Job("ubuntu-latest")
.addStep(checkout({
name: "Checkout code",
with: { "fetch-depth": 1 },
}))
.addStep(setupNode({
with: { "node-version": "22" },
}))
.addStep({ name: "Install dependencies", run: "npm ci" })
.addStep({ name: "Run tests", run: "npm test" });
const workflow = new Workflow({
name: "CI",
on: {
push: { branches: ["main"] },
pull_request: { branches: ["main"] },
},
}).addJob("build", build);
workflow.build("ci");
```
Run `gaji build` and it outputs `.github/workflows/ci.yml`.
### Recommended Development Workflow
For the best experience, follow this workflow:
1. **Start watch mode**:
```bash
gaji dev --watch
```
Leave this running in a terminal. It will automatically generate types when you add new actions.
2. **Edit your TypeScript workflows** in `workflows/*.ts`:
- Add or modify steps
- Use `getAction()` with full type safety
- Types are automatically generated for new actions
3. **Build to YAML**:
```bash
gaji build
```
4. **Review the generated YAML** in `.github/workflows/`:
- Verify commands are correct
- Check that step order is as expected
- Ensure all required fields are present
5. **Commit both TypeScript and YAML**:
```bash
git add workflows/ .github/workflows/
git commit -m "Update workflows"
```
#### Why Commit Both?
You should commit **both** the TypeScript source (`workflows/*.ts`) and the generated YAML (`.github/workflows/*.yml`):
- **TypeScript**: Source of truth for your workflows
- **YAML**: What GitHub Actions actually executes
#### ⚠️ Important: Auto-compilation in CI
While you can create a workflow that auto-compiles TypeScript to YAML on push, **this is NOT recommended**. Always compile and review workflows locally before committing.
If you're willing to handle the complexity of GitHub Actions triggers (e.g., filtering `paths`, managing PAT tokens, avoiding infinite loops), you can set up an auto-compilation workflow. See [`workflows/update-workflows.ts`](https://github.com/dodok8/gaji/blob/main/workflows/update-workflows.ts) for a working example.
### Composite Actions
Define reusable composite actions and reference them in workflows:
```typescript
import { CompositeAction, CallAction, Job, Workflow } from "../generated/index.js";
const action = new CompositeAction({
name: "Setup",
description: "Setup the project environment",
inputs: {
"node-version": { description: "Node.js version", required: false, default: "20" },
},
});
action.addStep({ name: "Install deps", run: "npm ci", shell: "bash" });
action.build("setup");
// Reference the composite action in a workflow
const job = new Job("ubuntu-latest")
.addStep(CallAction.from(action).toJSON());
const workflow = new Workflow({
name: "CI",
on: { push: {} },
}).addJob("build", job);
workflow.build("ci");
```
### Reusable Workflows
Call reusable workflows using `CallJob`:
```typescript
import { CallJob, Workflow } from "../generated/index.js";
const deploy = new CallJob("./.github/workflows/deploy.yml")
.with({ environment: "production" })
.secrets("inherit")
.needs(["build"]);
const workflow = new Workflow({
name: "Release",
on: { push: { tags: ["v*"] } },
}).addJob("deploy", deploy);
workflow.build("release");
```
### Job Options
The `Job` constructor accepts an optional second argument for additional configuration:
```typescript
const job = new Job("ubuntu-latest", {
needs: ["setup"],
env: { NODE_ENV: "test" },
"timeout-minutes": 30,
"continue-on-error": true,
permissions: { contents: "read" },
strategy: {
matrix: { node: ["18", "20", "22"] },
"fail-fast": false,
},
});
```
Builder methods are also available:
```typescript
const job = new Job("ubuntu-latest")
.addStep({ name: "Test", run: "npm test" })
.needs(["setup"])
.env({ CI: "true" })
.when("github.event_name == 'push'")
.permissions({ contents: "read" })
.outputs({ result: "${{ steps.test.outputs.result }}" })
.strategy({ matrix: { os: ["ubuntu-latest", "macos-latest"] } })
.continueOnError(true)
.timeoutMinutes(30);
```
## Commands
### `gaji init`
Initialize a new gaji project. Detects the project state (empty, existing project, or has YAML workflows) and sets up accordingly.
```
gaji init [OPTIONS]
```
| `--force` | Overwrite existing files |
| `--skip-examples` | Skip example workflow creation |
| `--migrate` | Migrate existing YAML workflows to TypeScript |
| `-i, --interactive` | Interactive mode |
### `gaji dev`
Start development mode. Scans workflow files for action references and generates types.
```
gaji dev [OPTIONS]
```
| `-d, --dir <DIR>` | Directory to scan (default: `workflows`) |
| `--watch` | Keep watching for changes after the initial scan |
### `gaji build`
Build TypeScript workflows to YAML.
```
gaji build [OPTIONS]
```
| `-i, --input <DIR>` | Input directory containing TypeScript workflows (default: `workflows`) |
| `-o, --output <DIR>` | Output directory for YAML files (default: `.github`) |
| `--dry-run` | Preview YAML output without writing files |
Output files are placed in subdirectories based on type:
- Workflows: `.github/workflows/<id>.yml`
- Composite actions: `.github/actions/<id>/action.yml`
### `gaji add <action>`
Add a new action and generate types.
```bash
gaji add actions/checkout@v5
gaji add actions/setup-node@v4
```
### `gaji clean`
Clean generated files.
```
gaji clean [OPTIONS]
```
| `--cache` | Also clean cache |
## Configuration
### `.gaji.toml`
Project-level configuration file. Created automatically by `gaji init`.
```toml
[project]
workflows_dir = "workflows" # TypeScript workflow source directory
output_dir = ".github" # Output directory for generated YAML
generated_dir = "generated" # Directory for generated type definitions
[watch]
debounce_ms = 300 # Debounce delay for file watcher
ignored_patterns = ["node_modules", ".git", "generated"]
[build]
validate = true # Validate workflow YAML (requires 'on' and 'jobs')
format = true # Format YAML output
[github]
token = "ghp_..." # GitHub token (prefer .gaji.local.toml for this)
api_url = "https://github.example.com" # GitHub Enterprise URL
```
### `.gaji.local.toml`
Local overrides for sensitive values. Add this to `.gitignore`.
```toml
[github]
token = "ghp_your_token_here"
api_url = "https://github.example.com" # for GitHub Enterprise
```
Token resolution priority: `GITHUB_TOKEN` env var > `.gaji.local.toml` > `.gaji.toml`
## Documentation
📚 **[Full Documentation](https://gaji.gaebalgom.work)** (English & 한국어)
- [Getting Started](https://gaji.gaebalgom.work/guide/getting-started)
- [Writing Workflows](https://gaji.gaebalgom.work/guide/writing-workflows)
- [CLI Reference](https://gaji.gaebalgom.work/reference/cli)
- [API Reference](https://gaji.gaebalgom.work/reference/api)
- [Examples](examples/)
## Examples
Check out the [examples/](examples/) directory for complete working examples:
- **[ts-package](examples/ts-package/)** - TypeScript package with gaji CI workflow using pnpm
## License
MIT License