This guide covers how to define and run CI/CD workflows using TypeScript with HapperCI.
- Overview
- Quick Start
- Installation
- Writing Workflows
- API Reference
- Examples
- Architecture
- Troubleshooting
HapperCI allows you to define CI/CD workflows using TypeScript instead of YAML. This provides several advantages:
- Type Safety: Catch configuration errors at compile time
- IDE Support: Full autocomplete, go-to-definition, and inline documentation
- Programmability: Use loops, conditionals, and functions to generate workflows
- Ecosystem: Import and use any npm package in your workflow definitions
- Testing: Write unit tests for your workflow logic
Create a workflow file workflow.ts:
import { step } from '@happerci/sdk';
step('install', 'npm install');
step('build', 'npm run build');
step('test', 'npm test');Run it with HapperCI:
happerci run workflow.tsOutput (streaming in real-time):
HapperCI - Running workflow
Step: install
$ npm install
added 150 packages in 3s
✓ PASSED
Step: build
$ npm run build
Building project...
Build completed successfully
✓ PASSED
Step: test
$ npm test
Running 42 tests...
All tests passed
✓ PASSED
Build: PASSED (3/3 steps)
- Go 1.21 or later
- Node.js 18 or later
- npm or yarn
# Clone the repository
git clone https://github.com/pythonandchips/happerci.git
cd happerci
# Build the CLI
go build -o bin/happerci ./cmd/happerci
# Optionally, add to PATH
export PATH="$PATH:$(pwd)/bin"In your project directory:
# Initialize package.json if needed
npm init -y
# Install the HapperCI SDK
npm install @happerci/sdk
# Or use a local development version
npm install ../path/to/happerci/sdk/typescriptCreate a tsconfig.json if you don't have one:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"esModuleInterop": true,
"strict": true
}
}The step() function is the primary API for defining workflow steps:
import { step } from '@happerci/sdk';
// Each step has a name and a command
step('step-name', 'shell command to run');Steps are executed sequentially in the order they are defined.
Commands are executed via sh -c, so you can use shell features:
// Chained commands
step('check', 'npm run lint && npm run typecheck');
// Pipes
step('coverage', 'npm test -- --coverage | tee coverage.txt');
// Redirections
step('build-log', 'npm run build > build.log 2>&1');
// Environment variables
step('deploy', 'DEPLOY_ENV=production ./deploy.sh');Use TypeScript control flow for conditional logic:
import { step } from '@happerci/sdk';
step('install', 'npm ci');
step('build', 'npm run build');
// Only run on main branch
if (process.env.BRANCH === 'main') {
step('deploy', 'npm run deploy');
}
// Skip tests in draft PRs
if (process.env.DRAFT !== 'true') {
step('test', 'npm test');
}Generate steps programmatically:
import { step } from '@happerci/sdk';
// Test against multiple Node versions
const nodeVersions = ['16', '18', '20'];
for (const version of nodeVersions) {
step(`test-node-${version}`, `nvm use ${version} && npm test`);
}Access environment variables for dynamic configuration:
import { step } from '@happerci/sdk';
const env = process.env.NODE_ENV || 'development';
const registry = process.env.NPM_REGISTRY || 'https://registry.npmjs.org';
step('install', `npm install --registry=${registry}`);
step('build', `NODE_ENV=${env} npm run build`);Use npm packages for complex logic:
import { step } from '@happerci/sdk';
import * as fs from 'fs';
import * as path from 'path';
// Read package.json to determine what to build
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
step('install', 'npm ci');
// Only build if there's a build script
if (pkg.scripts?.build) {
step('build', 'npm run build');
}
// Run tests for each workspace
if (pkg.workspaces) {
for (const workspace of pkg.workspaces) {
const wsName = path.basename(workspace);
step(`test-${wsName}`, `npm test --workspace=${workspace}`);
}
}Registers a workflow step to be executed.
Parameters:
| Parameter | Type | Description |
|---|---|---|
name |
string | Human-readable name displayed in output |
command |
string | Shell command to execute (via sh -c) |
Example:
step('test', 'npm test');Type-safe helper for defining complete workflow configurations.
Parameters:
| Parameter | Type | Description |
|---|---|---|
config |
Workflow | Workflow configuration |
Returns: The same configuration object (for type inference).
Type-safe helper for defining job configurations.
Type-safe helper for defining step configurations.
interface Step {
name: string; // Step name
run: string; // Shell command
}
interface Job {
name: string; // Job name
steps: Step[]; // Steps in this job
}
interface Workflow {
name: string; // Workflow name
jobs: Job[]; // Jobs in this workflow
}import { step } from '@happerci/sdk';
// Standard Node.js CI pipeline
step('install', 'npm ci');
step('lint', 'npm run lint');
step('typecheck', 'npm run typecheck');
step('test', 'npm test');
step('build', 'npm run build');import { step } from '@happerci/sdk';
step('deps', 'go mod download');
step('vet', 'go vet ./...');
step('test', 'go test -v ./...');
step('build', 'go build -o bin/app ./cmd/app');import { step } from '@happerci/sdk';
const tag = process.env.VERSION || 'latest';
const registry = process.env.REGISTRY || 'docker.io';
step('lint', 'hadolint Dockerfile');
step('build', `docker build -t ${registry}/myapp:${tag} .`);
if (process.env.PUSH === 'true') {
step('push', `docker push ${registry}/myapp:${tag}`);
}import { step } from '@happerci/sdk';
import * as fs from 'fs';
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const workspaces: string[] = pkg.workspaces || [];
step('install', 'npm ci');
// Build all workspaces
for (const ws of workspaces) {
step(`build-${ws}`, `npm run build -w ${ws}`);
}
// Test all workspaces
for (const ws of workspaces) {
step(`test-${ws}`, `npm test -w ${ws}`);
}import { step } from '@happerci/sdk';
const nodeVersions = ['16', '18', '20'];
const osList = ['ubuntu', 'macos'];
for (const os of osList) {
for (const node of nodeVersions) {
step(
`test-${os}-node${node}`,
`echo "Testing on ${os} with Node ${node}"`
);
}
}HapperCI uses a host-guest architecture:
┌─────────────────────────────────────────────────────────────┐
│ HapperCI CLI (Go) │
│ │
│ 1. Start gRPC server on random port │
│ 2. Set HAPPERCI_ENGINE_ADDR environment variable │
│ 3. Spawn: npx ts-node workflow.ts │
│ 4. Receive step registrations via gRPC │
│ 5. Execute steps when registration complete │
│ 6. Report results │
└─────────────────────────┬───────────────────────────────────┘
│ gRPC
│
┌─────────────────────────┴───────────────────────────────────┐
│ TypeScript Workflow (Node.js) │
│ │
│ 1. Import @happerci/sdk │
│ 2. SDK connects to engine via HAPPERCI_ENGINE_ADDR │
│ 3. Each step() call sends RegisterStep RPC │
│ 4. After all steps registered, sends Complete RPC │
│ 5. Process exits │
└─────────────────────────────────────────────────────────────┘
- Language Flexibility: Workflows can be written in any language with a SDK
- Execution Control: The Go engine has full control over step execution
- Security: User code only registers steps; it doesn't execute them
- Reliability: The stable Go runtime handles execution, not Node.js
This error occurs when running the workflow file directly instead of through HapperCI:
# Wrong - runs directly
npx ts-node workflow.ts
# Correct - runs through HapperCI
happerci run workflow.tsMake sure the SDK is installed in your project:
npm install @happerci/sdkOr if using a local development version, ensure the path is correct in package.json:
{
"dependencies": {
"@happerci/sdk": "file:../path/to/happerci/sdk/typescript"
}
}Make sure your workflow file:
- Imports from
@happerci/sdk - Calls
step()synchronously (not in async callbacks)
// Correct
import { step } from '@happerci/sdk';
step('test', 'npm test');
// Wrong - step() in async callback won't be registered
setTimeout(() => {
step('test', 'npm test'); // This won't work!
}, 1000);Ensure you have a valid tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"esModuleInterop": true
}
}If you see gRPC-related errors:
- Check that no firewall is blocking localhost connections
- Ensure the HapperCI CLI built successfully
- Try rebuilding:
go build -o bin/happerci ./cmd/happerci