# Writing Workflows

This guide explains how to write type-safe GitHub Actions workflows using gaji.

::: tip Standalone TypeScript Files
Workflow files generated by gaji are completely standalone and self-contained. You can run them directly with any TypeScript runtime (tsx, ts-node, Deno) to output the workflow JSON. This makes debugging and inspection easy!
:::

## Basic Structure

A gaji workflow consists of three main components:

1. **Actions**: Imported using `getAction()`
2. **Jobs**: Created using the `Job` class
3. **Workflows**: Created using the `Workflow` class

```typescript
import { getAction, Job, Workflow } from "../generated/index.js";

// 1. Import actions
const checkout = getAction("actions/checkout@v4");

// 2. Create jobs
const build = new Job("ubuntu-latest")
  .addStep(checkout({}));

// 3. Create workflow
const workflow = new Workflow({
  name: "CI",
  on: { push: { branches: ["main"] } },
}).addJob("build", build);

// 4. Build to YAML
workflow.build("ci");
```

## Using Actions

### Adding Actions

First, add the action and generate types:

```bash
gaji add actions/checkout@v4
```

### Importing Actions

Import actions using `getAction()`:

```typescript
const checkout = getAction("actions/checkout@v4");
const setupNode = getAction("actions/setup-node@v4");
const cache = getAction("actions/cache@v4");
```

### Using Actions with Type Safety

Actions return a function that accepts configuration:

```typescript
const step = checkout({
  name: "Checkout code",
  with: {
    // ✅ Full autocomplete for all inputs!
    repository: "owner/repo",
    ref: "main",
    token: "${{ secrets.GITHUB_TOKEN }}",
    "fetch-depth": 0,
  },
});
```

Your editor will provide:
- ✅ Autocomplete for all action inputs
- ✅ Type checking
- ✅ Documentation from action.yml
- ✅ Default values shown

## Creating Jobs

Jobs are created using the `Job` class:

```typescript
const job = new Job("ubuntu-latest");
```

### Supported Runners

```typescript
// Ubuntu
new Job("ubuntu-latest")
new Job("ubuntu-22.04")
new Job("ubuntu-20.04")

// macOS
new Job("macos-latest")
new Job("macos-13")
new Job("macos-12")

// Windows
new Job("windows-latest")
new Job("windows-2022")
new Job("windows-2019")

// Self-hosted
new Job("self-hosted")
new Job(["self-hosted", "linux", "x64"])
```

### Adding Steps

Steps can be added using `.addStep()`:

```typescript
const job = new Job("ubuntu-latest")
  // Action step
  .addStep(checkout({
    name: "Checkout",
  }))

  // Run command
  .addStep({
    name: "Build",
    run: "npm run build",
  })

  // Multi-line command
  .addStep({
    name: "Install dependencies",
    run: `
      npm ci
      npm run build
      npm test
    `.trim(),
  })

  // With environment variables
  .addStep({
    name: "Deploy",
    run: "npm run deploy",
    env: {
      NODE_ENV: "production",
      API_KEY: "${{ secrets.API_KEY }}",
    },
  })

  // Conditional step
  .addStep({
    name: "Upload artifacts",
    if: "success()",
    run: "npm run upload",
  });
```

## Creating Workflows

### Basic Workflow

```typescript
const workflow = new Workflow({
  name: "CI",
  on: {
    push: {
      branches: ["main"],
    },
  },
}).addJob("build", buildJob);

workflow.build("ci");
```

### Trigger Events

#### Push

```typescript
on: {
  push: {
    branches: ["main", "develop"],
    tags: ["v*"],
    paths: ["src/**", "tests/**"],
  },
}
```

#### Pull Request

```typescript
on: {
  pull_request: {
    branches: ["main"],
    types: ["opened", "synchronize", "reopened"],
  },
}
```

#### Schedule (Cron)

```typescript
on: {
  schedule: [
    { cron: "0 0 * * *" },  // Daily at midnight
  ],
}
```

#### Multiple Triggers

```typescript
on: {
  push: { branches: ["main"] },
  pull_request: { branches: ["main"] },
  workflow_dispatch: {},  // Manual trigger
}
```

### Multiple Jobs

```typescript
const test = new Job("ubuntu-latest")
  .addStep(checkout({}))
  .addStep({ run: "npm test" });

const build = new Job("ubuntu-latest")
  .addStep(checkout({}))
  .addStep({ run: "npm run build" });

const workflow = new Workflow({
  name: "CI",
  on: { push: { branches: ["main"] } },
})
  .addJob("test", test)
  .addJob("build", build);
```

### Job Dependencies

Use `.needs()` to create job dependencies:

```typescript
const test = new Job("ubuntu-latest")
  .addStep({ run: "npm test" });

const deploy = new Job("ubuntu-latest")
  .needs(["test"])  // Wait for test job
  .addStep({ run: "npm run deploy" });

const workflow = new Workflow({
  name: "Deploy",
  on: { push: { branches: ["main"] } },
})
  .addJob("test", test)
  .addJob("deploy", deploy);
```

## Matrix Builds

Create matrix builds for testing across multiple versions:

```typescript
const test = new Job("${{ matrix.os }}")
  .strategy({
    matrix: {
      os: ["ubuntu-latest", "macos-latest", "windows-latest"],
      node: ["18", "20", "22"],
    },
  })
  .addStep(checkout({}))
  .addStep(setupNode({
    with: {
      "node-version": "${{ matrix.node }}",
    },
  }))
  .addStep({ run: "npm test" });
```

## Composite Actions

Create reusable composite actions:

```typescript
import { CompositeAction } from "../generated/index.js";

const myAction = new CompositeAction({
  name: "My Action",
  description: "Reusable action",
  inputs: {
    version: {
      description: "Version to install",
      required: true,
    },
  },
})
  .addStep(checkout({}))
  .addStep({
    run: "echo Installing version ${{ inputs.version }}",
  });

myAction.build("my-action");
```

This generates `action.yml` in your repository.

## Composite Jobs

Create reusable job templates using `CompositeJob`:

```typescript
import { CompositeJob, getAction } from "../generated/index.js";

const checkout = getAction("actions/checkout@v4");
const setupNode = getAction("actions/setup-node@v4");

// Define a reusable job class
class NodeTestJob extends CompositeJob {
  constructor(nodeVersion: string) {
    super("ubuntu-latest");

    this
      .addStep(checkout({
        name: "Checkout code",
      }))
      .addStep(setupNode({
        name: `Setup Node.js ${nodeVersion}`,
        with: {
          "node-version": nodeVersion,
          cache: "npm",
        },
      }))
      .addStep({
        name: "Install dependencies",
        run: "npm ci",
      })
      .addStep({
        name: "Run tests",
        run: "npm test",
      });
  }
}

// Use in workflow
const workflow = new Workflow({
  name: "Test Matrix",
  on: { push: { branches: ["main"] } },
})
  .addJob("test-node-18", new NodeTestJob("18"))
  .addJob("test-node-20", new NodeTestJob("20"))
  .addJob("test-node-22", new NodeTestJob("22"));

workflow.build("test-matrix");
```

### Advanced Example: Parameterized Jobs

```typescript
class DeployJob extends CompositeJob {
  constructor(environment: "staging" | "production") {
    super("ubuntu-latest");

    this
      .env({
        ENVIRONMENT: environment,
        API_URL: environment === "production"
          ? "https://api.example.com"
          : "https://staging.api.example.com",
      })
      .addStep(checkout({}))
      .addStep(setupNode({ with: { "node-version": "20" } }))
      .addStep({
        name: "Deploy",
        run: `npm run deploy:${environment}`,
        env: {
          DEPLOY_TOKEN: "${{ secrets.DEPLOY_TOKEN }}",
        },
      });
  }
}

// Use in workflow
const workflow = new Workflow({
  name: "Deploy",
  on: { push: { tags: ["v*"] } },
})
  .addJob("deploy-staging", new DeployJob("staging"))
  .addJob("deploy-production",
    new DeployJob("production").needs(["deploy-staging"])
  );
```

**Benefits:**
- Reuse common job patterns
- Type-safe parameters
- Easier maintenance
- Consistent job structure

## Environment Variables

### Workflow-level

```typescript
const workflow = new Workflow({
  name: "CI",
  on: { push: { branches: ["main"] } },
  env: {
    NODE_ENV: "production",
  },
});
```

### Job-level

```typescript
const job = new Job("ubuntu-latest")
  .env({
    DATABASE_URL: "${{ secrets.DATABASE_URL }}",
  });
```

### Step-level

```typescript
.addStep({
  run: "npm run deploy",
  env: {
    API_KEY: "${{ secrets.API_KEY }}",
  },
})
```

## Outputs

Define and use job outputs:

```typescript
const build = new Job("ubuntu-latest")
  .outputs({
    version: "${{ steps.version.outputs.value }}",
  })
  .addStep({
    id: "version",
    run: 'echo "value=1.0.0" >> $GITHUB_OUTPUT',
  });

const deploy = new Job("ubuntu-latest")
  .needs(["build"])
  .addStep({
    run: "echo Deploying version ${{ needs.build.outputs.version }}",
  });
```

## Tips

### 1. Use Watch Mode

Always use `gaji dev --watch` during development to automatically generate types for new actions.

### 2. Review Generated YAML

Always review the generated YAML before committing to ensure correctness.

### 3. Type Safety

Take advantage of TypeScript's type checking:

```typescript
// ❌ Type error - unknown property key
setupNode({
  with: {
    "node-versoin": "20",  // Typo in key name! ❌
  },
});

// ❌ Type error - wrong type
setupNode({
  with: {
    "node-version": 20,  // Should be string! ❌
  },
});

// ✅ Correct
setupNode({
  with: {
    "node-version": "20",  // ✅ Correct key and type
    cache: "npm",
  },
});
```

**Note**: While gaji provides type safety for property keys and types, it cannot validate string values (e.g., `cache: "npn"` vs `cache: "npm"`) at compile time. Always review generated YAML to catch such typos.

## Next Steps

- Learn about [Configuration](./configuration.md)
- See [Examples](/examples/simple-ci)
- Check the [API Reference](/reference/api)
