<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 vegetable</p>
<p>
<a href="https://crates.io/crates/gaji"><img src="https://img.shields.io/crates/v/gaji" alt="crates.io"></a>
<a href="https://www.npmjs.com/package/gaji"><img src="https://img.shields.io/npm/v/gaji" alt="npm"></a>
</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, providing autocomplete and type checking for action inputs and outputs.
## 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");
new Workflow({
name: "CI",
on: {
push: { branches: ["main"] },
pull_request: { branches: ["main"] },
},
}).jobs(j => j
.add("build",
new Job("ubuntu-latest")
.steps(s => s
.add(checkout({ name: "Checkout code", with: { "fetch-depth": 1 } }))
.add(setupNode({ with: { "node-version": "22" } }))
.add({ name: "Install dependencies", run: "npm ci" })
.add({ name: "Run tests", run: "npm test" })
)
)
).build("ci");
```
Run `gaji build` and it outputs `.github/workflows/ci.yml`.
### Recommended Development 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 { Action, ActionRef, Job, Workflow } from "../generated/index.js";
const action = new Action({
name: "Setup",
description: "Setup the project environment",
inputs: {
"node-version": { description: "Node.js version", required: false, default: "20" },
},
})
.steps(s => s
.add({ name: "Install deps", run: "npm ci", shell: "bash" })
);
action.build("setup");
// Reference the composite action in a workflow
new Workflow({
name: "CI",
on: { push: {} },
}).jobs(j => j
.add("build",
new Job("ubuntu-latest")
.steps(s => s
.add(ActionRef.from(action).toJSON())
)
)
).build("ci");
```
### Reusable Workflows
Call reusable workflows using `WorkflowCall`:
```typescript
import { WorkflowCall, Workflow } from "../generated/index.js";
new Workflow({
name: "Release",
on: { push: { tags: ["v*"] } },
}).jobs(j => j
.add("deploy",
new WorkflowCall("./.github/workflows/deploy.yml", {
with: { environment: "production" },
secrets: "inherit",
needs: ["build"],
})
)
).build("release");
```
### Job Options
The `Job` constructor accepts an optional second argument for 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,
},
})
.steps(s => s
.add({ name: "Test", run: "npm test" })
);
```
Steps are added via the `steps()` callback, and job-level settings are passed through the constructor. This keeps configuration separate from step definitions.
## 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]
```
| `-i, --input <PATH>...` | Workflow directories or individual `.ts` files (falls back to `workflows_dir` in config) |
| `--watch` | Keep watching for changes after the initial scan |
### `gaji build`
Build TypeScript workflows to YAML.
```
gaji build [OPTIONS]
```
| `-i, --input <PATH>...` | Workflow directories or individual `.ts` files (falls back to `workflows_dir` in config) |
| `-o, --output <DIR>` | Output directory for YAML files (falls back to `output_dir` in config) |
| `--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.config.ts`
Project-level configuration file. Created automatically by `gaji init`.
```typescript
import { defineConfig } from "./generated/index.js";
export default defineConfig({
workflows: "workflows", // TypeScript workflow source directory
output: ".github", // Output directory for generated YAML
generated: "generated", // Directory for generated type definitions
watch: {
debounce: 300, // Debounce delay for file watcher (ms)
ignore: ["node_modules", ".git", "generated"],
},
build: {
validate: true, // Validate workflow YAML (requires 'on' and 'jobs')
format: true, // Format YAML output
},
});
```
### `gaji.config.local.ts`
Local overrides for sensitive values. Add this to `.gitignore`.
```typescript
import { defineConfig } from "./generated/index.js";
export default defineConfig({
github: {
token: "ghp_your_token_here",
apiUrl: "https://github.example.com", // for GitHub Enterprise
},
});
```
Token resolution priority: `GITHUB_TOKEN` env var > `gaji.config.local.ts` > `gaji.config.ts`
gaji also reads `.gaji.toml` / `.gaji.local.toml` as a fallback for existing projects.
## 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
## Special Thanks
### gaji Brand
- Name suggestions: [kiwiyou](https://github.com/kiwiyou), [RanolP](https://github.com/ranolp)
- Logo design: [sij411](https://github.com/sij411)
### Inspiration
- Client Devops Team@Toss: Without the experience on this team, I would never have thought deeply about YAML and GitHub Actions. The product below was also introduced to me through a teammate.
- [emmanuelnk/github-actions-workflow-ts](https://github.com/emmanuelnk/github-actions-workflow-ts): The idea of writing GitHub Actions in TypeScript came from here.