Migration
This guide helps you migrate existing YAML workflows to TypeScript with gaji.
Automatic Migration
gaji can automatically convert existing YAML workflows to TypeScript.
gaji init --migrateThe process follows this order:
- Detect existing YAML workflows in
.github/workflows/ - Convert them to TypeScript in
workflows/ - Backup original YAML files (
.yml.backup) - Generate types for all actions used
Action Migration
gaji also migrates existing local actions (.github/actions/*/action.yml) to TypeScript.
gaji init --migrateThis detects both workflows and actions automatically. Actions are converted to Action, NodeAction, or DockerAction classes depending on the runs.using field.
Composite Action
Before (.github/actions/setup-env/action.yml):
name: Setup Environment
description: Setup Node.js and install dependencies
inputs:
node-version:
description: Node.js version to use
required: false
default: "20"
outputs:
cache-hit:
description: Whether cache was hit
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: composite
steps:
- uses: actions/checkout@v5
- name: Install dependencies
run: npm ci
shell: bash
- name: Cache node_modules
id: cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}After (workflows/action-setup-env.ts):
import { getAction, Action } from "../generated/index.js";
const checkout = getAction("actions/checkout@v5");
const cache = getAction("actions/cache@v4");
const action = new Action({
name: "Setup Environment",
description: "Setup Node.js and install dependencies",
inputs: {
"node-version": {
description: "Node.js version to use",
required: false,
default: "20",
},
},
outputs: {
"cache-hit": {
description: "Whether cache was hit",
value: "${{ steps.cache.outputs.cache-hit }}",
},
},
});
action
.steps(s => s
.add(checkout({}))
.add({
name: "Install dependencies",
run: "npm ci",
shell: "bash",
})
.add(cache({
id: "cache",
name: "Cache node_modules",
with: {
path: "node_modules",
key: "${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}",
},
}))
)
.build("setup-env");JavaScript Action
Before (.github/actions/notify/action.yml):
name: Send Notification
description: Send a Slack notification
inputs:
webhook-url:
description: Slack webhook URL
required: true
message:
description: Message to send
required: true
runs:
using: node20
main: dist/index.js
post: dist/cleanup.jsAfter (workflows/action-notify.ts):
import { NodeAction } from "../generated/index.js";
const action = new NodeAction(
{
name: "Send Notification",
description: "Send a Slack notification",
inputs: {
"webhook-url": {
description: "Slack webhook URL",
required: true,
},
message: {
description: "Message to send",
required: true,
},
},
},
{
using: "node20",
main: "dist/index.js",
post: "dist/cleanup.js",
},
);
action.build("notify");Docker Action
Before (.github/actions/lint/action.yml):
name: Lint
description: Run linter in Docker
inputs:
config:
description: Config file path
required: false
default: ".lintrc"
runs:
using: docker
image: Dockerfile
entrypoint: entrypoint.sh
args:
- --config
- ${{ inputs.config }}After (workflows/action-lint.ts):
import { DockerAction } from "../generated/index.js";
const action = new DockerAction(
{
name: "Lint",
description: "Run linter in Docker",
inputs: {
config: {
description: "Config file path",
required: false,
default: ".lintrc",
},
},
},
{
using: "docker",
image: "Dockerfile",
entrypoint: "entrypoint.sh",
args: ["--config", "${{ inputs.config }}"],
},
);
action.build("lint");Supported Action Types
| Type | runs.using | Migrated To |
|---|---|---|
| Composite | composite | Action |
| JavaScript | node12, node16, node20 | NodeAction |
| Docker | docker | DockerAction |
Manual Migration
If you prefer to migrate manually, follow this process.
Step 1: Analyze Your YAML
Here is a simple YAML workflow as an example.
name: CI
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm testStep 2: Add Required Actions
gaji add actions/checkout@v5
gaji add actions/setup-node@v4Step 3: Convert to TypeScript
Create workflows/ci.ts:
import { getAction, Job, Workflow } from "../generated/index.js";
// Import actions
const checkout = getAction("actions/checkout@v5");
const setupNode = getAction("actions/setup-node@v4");
// Create workflow with jobs and steps
new Workflow({
name: "CI",
on: {
push: {
branches: ["main"],
},
},
})
.jobs(j => j
.add("build",
new Job("ubuntu-latest")
.steps(s => s
.add(checkout({}))
.add(setupNode({
with: { "node-version": "20" },
}))
.add({ run: "npm ci" })
.add({ run: "npm test" })
)
)
)
.build("ci");Step 4: Build and Verify
# Build TypeScript to YAML
gaji buildStep 5: Clean Up
Once verified, remove the backup:
rm .github/workflows/ci.yml.backupCommon Migration Patterns
Multiple Jobs
YAML:
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
steps:
- run: npm run buildTypeScript:
const test = new Job("ubuntu-latest")
.steps(s => s
.add({ run: "npm test" })
);
const build = new Job("ubuntu-latest", {
needs: ["test"],
})
.steps(s => s
.add({ run: "npm run build" })
);
new Workflow({
name: "CI",
on: { push: { branches: ["main"] } },
})
.jobs(j => j
.add("test", test)
.add("build", build)
)
.build("ci");Matrix Strategy
YAML:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node: [18, 20, 22]TypeScript:
const test = new Job("${{ matrix.os }}", {
strategy: {
matrix: {
os: ["ubuntu-latest", "macos-latest"],
node: ["18", "20", "22"],
},
},
})
.steps(s => s
.add(checkout({}))
);Environment Variables
YAML:
env:
NODE_ENV: production
jobs:
deploy:
runs-on: ubuntu-latest
env:
API_KEY: ${{ secrets.API_KEY }}TypeScript:
new Workflow({
name: "Deploy",
on: { push: { branches: ["main"] } },
env: {
NODE_ENV: "production",
},
})
.jobs(j => j
.add("deploy",
new Job("ubuntu-latest", {
env: {
API_KEY: "${{ secrets.API_KEY }}",
},
})
)
)
.build("deploy");Conditional Steps
YAML:
steps:
- name: Deploy
if: github.ref == 'refs/heads/main'
run: npm run deployTypeScript:
.add({
name: "Deploy",
if: "github.ref == 'refs/heads/main'",
run: "npm run deploy",
})Job Outputs
YAML:
jobs:
build:
outputs:
version: ${{ steps.version.outputs.value }}
steps:
- id: version
run: echo "value=1.0.0" >> $GITHUB_OUTPUTTypeScript:
const build = new Job("ubuntu-latest")
.steps(s => s
.add({
id: "version",
run: 'echo "value=1.0.0" >> $GITHUB_OUTPUT',
})
)
.outputs({
version: "${{ steps.version.outputs.value }}",
});Configuration Migration
If your project uses the older .gaji.toml configuration file, gaji can migrate it to gaji.config.ts automatically.
Automatic Migration
When you run gaji init in a project that has a .gaji.toml, gaji detects the file and offers to migrate it:
gaji init
# Detected .gaji.toml configuration file.
# Migrate to gaji.config.ts? [y/N]If you confirm, gaji will:
- Read
.gaji.tomland generate agaji.config.tsfile withdefineConfig() - If
.gaji.local.tomlexists, generate a correspondinggaji.config.local.ts - Remove the old TOML files after successful migration
Manual Migration
To migrate manually, create gaji.config.ts at your project root:
Before (.gaji.toml):
workflows = "src/workflows"
output = ".github"
[build]
cache_ttl_days = 14
[github]
token = "ghp_xxx"After (gaji.config.ts):
import { defineConfig } from "./generated/index.js";
export default defineConfig({
workflows: "src/workflows",
build: {
cacheTtlDays: 14,
},
});Note that TOML keys use snake_case while TypeScript config uses camelCase. Secrets like github.token should go in gaji.config.local.ts (which should be added to .gitignore):
// gaji.config.local.ts
import { defineConfig } from "./generated/index.js";
export default defineConfig({
github: {
token: "ghp_xxx",
},
});Migration Checklist
- Install gaji
- Initialize project
- Add all actions used in your workflows
- Convert YAML to TypeScript
- Build and verify generated YAML
- Test workflows in a branch
- Remove backup files
- Update documentation
Tips
1. Start Small
Migrate one workflow at a time, starting with the simplest one.
2. Use Automatic Migration
For complex workflows, let gaji do the initial conversion:
gaji init --migrateThen refine the generated TypeScript.
3. Test in a Branch
Always test migrated workflows in a feature branch before merging.
4. Keep Both During Transition
During migration, you can keep both YAML and TypeScript versions:
- Backup old workflows with
.backupextension (e.g.,ci.yml.backup) - New workflows: Generated from
workflows/*.tsto.github/workflows/*.yml
Troubleshooting
Types Not Generated
Make sure you've added all actions:
gaji add actions/checkout@v5
gaji devBuild Fails
Check for TypeScript errors:
npx tsc --noEmitYAML Differs from Original
Minor formatting differences are normal. Verify functionality, not formatting.
Next Steps
- Read the CLI Reference
- See Examples
