An open API service indexing awesome lists of open source software.

https://github.com/dodok8/gaji

Type-safe GitHub Actions workflows in TypeScript, GitHub Actions Justified Improvements
https://github.com/dodok8/gaji

cd ci cli github-actions rust typescript

Last synced: 4 months ago
JSON representation

Type-safe GitHub Actions workflows in TypeScript, GitHub Actions Justified Improvements

Awesome Lists containing this project

README

          


gaji logo

gaji


Type-safe GitHub Actions workflows in TypeScript


GitHub Actions Justified Improvements


🍆 Named after the Korean word "가지" (gaji, eggplant) - a versatile vegetable



crates.io
npm


## 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");

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

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]
```

| Option | Description |
|---|---|
| `--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]
```

| Option | Description |
|---|---|
| `-i, --input ...` | Workflow directories or individual `.ts` files (default: `workflows`) |
| `--watch` | Keep watching for changes after the initial scan |

### `gaji build`

Build TypeScript workflows to YAML.

```
gaji build [OPTIONS]
```

| Option | Description |
|---|---|
| `-i, --input ...` | Workflow directories or individual `.ts` files (default: `workflows`) |
| `-o, --output ` | 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/.yml`
- Composite actions: `.github/actions//action.yml`

### `gaji add `

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]
```

| Option | Description |
|---|---|
| `--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

## 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.