Example: Composite Action
Create reusable composite actions and jobs.
Composite Action
Create a reusable action that can be shared across workflows.
Creating the Action
Create actions/setup-env/action.ts:
typescript
import { CompositeAction, getAction } from "../../generated/index.js";
const checkout = getAction("actions/checkout@v4");
const setupNode = getAction("actions/setup-node@v4");
const cache = getAction("actions/cache@v4");
const setupEnv = new CompositeAction({
name: "Setup Environment",
description: "Setup Node.js and install dependencies",
inputs: {
"node-version": {
description: "Node.js version to use",
required: false,
default: "20",
},
"cache-dependency-path": {
description: "Path to dependency file",
required: false,
default: "package-lock.json",
},
},
outputs: {
"node-version": {
description: "Installed Node.js version",
value: "${{ steps.setup-node.outputs.node-version }}",
},
},
})
.addStep(checkout({
name: "Checkout code",
}))
.addStep(setupNode({
id: "setup-node",
name: "Setup Node.js",
with: {
"node-version": "${{ inputs.node-version }}",
cache: "npm",
"cache-dependency-path": "${{ inputs.cache-dependency-path }}",
},
}))
.addStep({
name: "Install dependencies",
run: "npm ci",
});
setupEnv.build("setup-env");Build it:
bash
gaji buildThis generates actions/setup-env/action.yml.
Using the Composite Action
In your workflow:
typescript
import { getAction, Job, Workflow } from "../../generated/index.js";
// Reference local composite action
const setupEnv = getAction("./actions/setup-env");
const test = new Job("ubuntu-latest")
.addStep(setupEnv({
name: "Setup environment",
with: {
"node-version": "20",
},
}))
.addStep({
name: "Run tests",
run: "npm test",
});
const workflow = new Workflow({
name: "CI",
on: { push: { branches: ["main"] } },
}).addJob("test", test);
workflow.build("ci");Composite Job
Create reusable job templates with CompositeJob.
Basic Example
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 Deploy Job
typescript
import { CompositeJob, getAction } from "../../generated/index.js";
const checkout = getAction("actions/checkout@v4");
const setupNode = getAction("actions/setup-node@v4");
class DeployJob extends CompositeJob {
constructor(
environment: "staging" | "production",
region: string = "us-east-1"
) {
super("ubuntu-latest");
this
.env({
ENVIRONMENT: environment,
REGION: region,
API_URL: environment === "production"
? "https://api.example.com"
: "https://staging.api.example.com",
})
.addStep(checkout({
name: "Checkout code",
}))
.addStep(setupNode({
name: "Setup Node.js",
with: {
"node-version": "20",
cache: "npm",
},
}))
.addStep({
name: "Install dependencies",
run: "npm ci",
})
.addStep({
name: "Build",
run: `npm run build:${environment}`,
})
.addStep({
name: `Deploy to ${environment}`,
run: `npm run deploy`,
env: {
DEPLOY_TOKEN: "${{ secrets.DEPLOY_TOKEN }}",
AWS_REGION: region,
},
});
}
}
// Use in workflow
const workflow = new Workflow({
name: "Deploy",
on: {
push: {
tags: ["v*"],
},
},
})
.addJob("deploy-staging-us", new DeployJob("staging", "us-east-1"))
.addJob("deploy-staging-eu", new DeployJob("staging", "eu-west-1"))
.addJob("deploy-production",
new DeployJob("production", "us-east-1")
.needs(["deploy-staging-us", "deploy-staging-eu"])
);
workflow.build("deploy");Complex Example: Reusable Test Suite
typescript
import { CompositeJob, getAction } from "../../generated/index.js";
const checkout = getAction("actions/checkout@v4");
const setupNode = getAction("actions/setup-node@v4");
const uploadArtifact = getAction("actions/upload-artifact@v4");
const uploadCodecov = getAction("codecov/codecov-action@v4");
interface TestOptions {
nodeVersion: string;
coverage?: boolean;
uploadArtifacts?: boolean;
additionalTests?: string[];
}
class TestSuiteJob extends CompositeJob {
constructor(options: TestOptions) {
super("ubuntu-latest");
// Setup
this
.addStep(checkout({}))
.addStep(setupNode({
with: {
"node-version": options.nodeVersion,
cache: "npm",
},
}))
.addStep({ run: "npm ci" });
// Lint
this.addStep({
name: "Run linter",
run: "npm run lint",
});
// Tests
if (options.coverage) {
this.addStep({
name: "Run tests with coverage",
run: "npm run test:coverage",
});
this.addStep(uploadCodecov({
name: "Upload coverage",
with: {
files: "./coverage/lcov.info",
},
}));
} else {
this.addStep({
name: "Run tests",
run: "npm test",
});
}
// Additional tests
if (options.additionalTests) {
for (const test of options.additionalTests) {
this.addStep({
name: `Run ${test}`,
run: `npm run test:${test}`,
});
}
}
// Upload artifacts
if (options.uploadArtifacts) {
this.addStep(uploadArtifact({
name: "Upload test results",
with: {
name: "test-results",
path: "test-results/",
},
}));
}
}
}
// Use in workflow
const workflow = new Workflow({
name: "Full Test Suite",
on: {
push: { branches: ["main"] },
pull_request: { branches: ["main"] },
},
})
.addJob("test-basic", new TestSuiteJob({
nodeVersion: "20",
}))
.addJob("test-full", new TestSuiteJob({
nodeVersion: "20",
coverage: true,
uploadArtifacts: true,
additionalTests: ["integration", "e2e"],
}));
workflow.build("full-test");Benefits
Code Reuse
- Define common patterns once
- Reuse across multiple workflows
- Maintain consistency
Type Safety
- Parameters are type-checked
- Refactoring is safer
- Autocomplete works
Easier Maintenance
- Update logic in one place
- Changes propagate automatically
- Reduce duplication
Next Steps
- See API Reference
- Learn about Matrix Builds
