Skip to content

Latest commit

 

History

History
478 lines (340 loc) · 11.1 KB

File metadata and controls

478 lines (340 loc) · 11.1 KB

HapperCI TypeScript Guide

This guide covers how to define and run CI/CD workflows using TypeScript with HapperCI.

Table of Contents

Overview

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

Quick Start

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

Output (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)

Installation

Prerequisites

  • Go 1.21 or later
  • Node.js 18 or later
  • npm or yarn

Install HapperCI CLI

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

Set Up Your Project

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/typescript

Create a tsconfig.json if you don't have one:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "esModuleInterop": true,
    "strict": true
  }
}

Writing Workflows

Basic Steps

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.

Using Shell Features

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');

Conditional Steps

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');
}

Dynamic Steps

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`);
}

Environment Variables

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`);

Importing Modules

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}`);
  }
}

API Reference

step(name: string, command: string): void

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');

defineWorkflow(config: Workflow): Workflow

Type-safe helper for defining complete workflow configurations.

Parameters:

Parameter Type Description
config Workflow Workflow configuration

Returns: The same configuration object (for type inference).

defineJob(config: Job): Job

Type-safe helper for defining job configurations.

defineStep(config: Step): Step

Type-safe helper for defining step configurations.

Interfaces

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
}

Examples

Node.js Project

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');

Go Project

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');

Docker Build

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}`);
}

Monorepo with Workspaces

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}`);
}

Matrix Testing

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}"`
    );
  }
}

Architecture

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                                           │
└─────────────────────────────────────────────────────────────┘

Why This Architecture?

  1. Language Flexibility: Workflows can be written in any language with a SDK
  2. Execution Control: The Go engine has full control over step execution
  3. Security: User code only registers steps; it doesn't execute them
  4. Reliability: The stable Go runtime handles execution, not Node.js

Troubleshooting

"HAPPERCI_ENGINE_ADDR environment variable not set"

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

"Cannot find module '@happerci/sdk'"

Make sure the SDK is installed in your project:

npm install @happerci/sdk

Or if using a local development version, ensure the path is correct in package.json:

{
  "dependencies": {
    "@happerci/sdk": "file:../path/to/happerci/sdk/typescript"
  }
}

Steps Not Running

Make sure your workflow file:

  1. Imports from @happerci/sdk
  2. 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);

TypeScript Compilation Errors

Ensure you have a valid tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "esModuleInterop": true
  }
}

gRPC Connection Errors

If you see gRPC-related errors:

  1. Check that no firewall is blocking localhost connections
  2. Ensure the HapperCI CLI built successfully
  3. Try rebuilding: go build -o bin/happerci ./cmd/happerci