Build GitHub Actions workflows and composite actions in TypeScript with full type safety, deterministic output, and source-first JSR distribution.
ghawb replaces hand-written YAML with fluent TypeScript builders that validate your workflow or composite action at construction time, catch mistakes before CI ever runs, and render deterministic YAML you can commit alongside your source.
import { createJobId, createWorkflowId, defineWorkflow } from "@ghawb/sdk";
import { nodeCi } from "@ghawb/job-helpers";
const workflow = defineWorkflow({
id: createWorkflowId("ci"),
name: "CI",
})
.onPush({ branches: ["main"] })
.onPullRequest({ branches: ["main"] })
.addJob(createJobId("test"), (job) => {
job.runsOn("ubuntu-latest").apply(nodeCi({ nodeVersion: "24" }));
})
.build();
export default workflow;bunx jsr add @ghawb/sdkFor CLI rendering to YAML:
bunx jsr add @ghawb/cliFor the standard Node CI helper path:
bunx jsr add @ghawb/job-helpersFor opt-in typed wrappers around common first-party actions:
bunx jsr add @ghawb/typed-actionsFor opt-in composite action authoring:
bunx jsr add @ghawb/composite-actionsFor Deno projects, use native JSR specifiers:
deno add jsr:@ghawb/sdkRuntime support: Bun 1.x, Deno 2.x. Packages are distributed through JSR only.
Start with @ghawb/sdk. Add each opt-in package only when the previous one stops being enough.
| Package | Use it when | Avoid it when |
|---|---|---|
@ghawb/sdk |
You want typed workflow builders, validation, and deterministic render payloads. | You only need a shell command to render an existing module and do not need to author workflows in code. |
@ghawb/job-helpers |
You want the standard checkout/setup/install/test Node CI path or the narrower checkout/setup/install bootstrap used before release and publish steps. | You need fully custom job steps or action-level control for most workflows. |
@ghawb/typed-actions |
Repeated action refs like checkout, setup-node, cache, Pages, or artifacts are making raw with maps noisy or error-prone. |
You mostly use one-off actions whose input surface is too niche to justify a maintained wrapper. |
@ghawb/cli |
You want a command-line path to render workflow or composite-action modules into committed YAML files. | You are embedding rendering inside your own TypeScript process and do not need a CLI entrypoint. |
@ghawb/reusable-workflow-import |
You need to call an existing reusable workflow YAML file from ghawb without rewriting that reusable workflow into builders immediately. |
You are already authoring the reusable workflow in @ghawb/sdk and can pass the builder or built definition directly to usesWorkflow(). |
@ghawb/composite-actions |
You want to author action.yml metadata with the same explicit builder style used for workflows. |
You only need workflow authoring; composite actions are a separate opt-in surface. |
Recommended adoption path:
- Start with
@ghawb/sdkfor workflow builders and validation. - Add
@ghawb/job-helpersfor Node CI or bootstrap prefixes without repeated boilerplate. - Add
@ghawb/typed-actionsfor typedwithinputs on common actions. - Add
@ghawb/clito render committed YAML from repository-local sources. - Add
@ghawb/reusable-workflow-importwhen you need to call existing reusable workflow YAML.
Create a file (e.g. workflows/ci.ts):
import { createJobId, createWorkflowId, defineWorkflow } from "@ghawb/sdk";
import { nodeCi } from "@ghawb/job-helpers";
export default defineWorkflow({
id: createWorkflowId("ci"),
name: "CI",
})
.onPush({ branches: ["main"] })
.addJob(createJobId("build"), (job) => {
job
.runsOn("ubuntu-latest")
.apply(nodeCi({ nodeVersion: "24" }))
.run("bun run build", "Build");
})
.build();ghawb render --input workflows/ci.tsYou can inject per-target authoring config into the module at render time:
import { createJobId, createWorkflowId, defineWorkflow, getRenderConfig } from "@ghawb/sdk";
const config = getRenderConfig<{ onPushBranches?: string[] }>();
export default defineWorkflow({
id: createWorkflowId("ci"),
name: "CI",
})
.onPush({ branches: config?.onPushBranches ?? ["main"] })
.addJob(createJobId("build"), (job) => {
job.runsOn("ubuntu-latest").run("bun test");
})
.build();ghawb render \
--input workflows/ci.ts \
--config ci.render.json \
--output .github/workflows/ci.ymlSupported injected config formats are JSON, YAML, and TOML. --config applies to the immediately preceding --input, so multi-target renders can pass different config files per module:
ghawb render \
--input workflows/release.ts \
--config release.json \
--output .github/workflows/release.yml \
--input workflows/hotfix.ts \
--config hotfix.toml \
--output .github/workflows/hotfix.ymlIf you want to declare many render targets in one file, use --bulk:
ghawb render --bulk ghawb.render.jsonTreat the .yml as generated output from your TypeScript source. For the supported repository-local path, keep workflow source modules under workflows/ and generated outputs under .github/workflows/.
- Want the shortest path for standard Node CI or release bootstrap? Add
@ghawb/job-helpersand usejob.apply(nodeCi(options))for the full CI path orjob.apply(nodeBootstrap(options))for the checkout/setup/install prefix. - Want typed
withinputs for common actions? Add@ghawb/typed-actions. - Want a repository command that renders and checks committed YAML? Add
@ghawb/cli. - Need to keep an existing reusable workflow YAML file in the flow? Add
@ghawb/reusable-workflow-import.
import { createJobId, createWorkflowId, defineWorkflow } from "@ghawb/sdk";
import { nodeCi } from "@ghawb/job-helpers";
export default defineWorkflow({
id: createWorkflowId("ci"),
name: "CI",
})
.onPush({ branches: ["main"] })
.onPullRequest({ branches: ["main"] })
.concurrency({
group: "ci-${{ github.ref }}",
cancelInProgress: true,
})
.addJob(createJobId("check"), (job) => {
job
.runsOn("ubuntu-latest")
.permissions({ contents: "read" })
.apply(nodeCi({ nodeVersion: "24" }));
})
.build();import { createJobId, createWorkflowId, defineWorkflow } from "@ghawb/sdk";
export default defineWorkflow({
id: createWorkflowId("deploy"),
name: "Deploy",
})
.onPush({ branches: ["main"] })
.addJob(createJobId("deploy"), (job) => {
job
.runsOn("ubuntu-latest")
.environment({ name: "production", url: "https://example.com" })
.permissions({ contents: "read", deployments: "write" })
.uses("actions/checkout@v6")
.run("bun install --frozen-lockfile")
.run("bun run build")
.run("bun run deploy");
})
.build();import { createJobId, createWorkflowId, defineWorkflow } from "@ghawb/sdk";
export default defineWorkflow({
id: createWorkflowId("matrix"),
name: "Matrix CI",
})
.onPush({ branches: ["main"] })
.addJob(createJobId("test"), (job) => {
job
.runsOn("ubuntu-latest")
.strategyMatrix({
bun: ["1.2", "1.3"],
os: ["ubuntu-latest", "windows-latest"],
})
.uses("actions/checkout@v6")
.uses("oven-sh/setup-bun@v2", {
with: { "bun-version": "${{ matrix.bun }}" },
})
.run("bun install --frozen-lockfile")
.run("bun test");
})
.build();import { createJobId, createWorkflowId, defineWorkflow } from "@ghawb/sdk";
import { actionsCache, actionsCheckout, actionsUploadArtifact } from "@ghawb/typed-actions";
export default defineWorkflow({
id: createWorkflowId("typed-actions"),
name: "Typed Actions",
})
.onPush({ branches: ["main"] })
.addJob(createJobId("build"), (job) => {
job
.runsOn("ubuntu-latest")
.uses(actionsCheckout({ fetchDepth: 0 }), "Checkout")
.uses("oven-sh/setup-bun@v2", "Setup Bun")
.uses(
actionsCache({
path: "~/.bun/install/cache",
key: "bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}",
restoreKeys: "bun-${{ runner.os }}-",
}),
"Cache Store"
)
.run("bun install --frozen-lockfile")
.run("bun test")
.uses(actionsUploadArtifact({ name: "coverage", path: "coverage" }), "Upload Coverage");
})
.build();Use @ghawb/typed-actions when you want autocomplete and typed with inputs for stable, common actions. Each wrapper defaults to the package's current pinned major and also accepts an optional second argument such as actionsCheckout({}, { version: "v5" }) when you need a different ref. Use raw .uses("owner/repo@ref", { with: ... }) for one-off actions that do not justify a wrapper, and prefer job.apply(nodeCi(...)) from @ghawb/job-helpers when the default Node CI sequence is sufficient without action-level customization, or job.apply(nodeBootstrap(...)) when you only need the checkout/setup/install prefix before custom release steps. Existing nodeCi(job, options) calls remain supported as a migration path.
import { createJobId, createWorkflowId, defineWorkflow } from "@ghawb/sdk";
export default defineWorkflow({
id: createWorkflowId("release"),
name: "Release",
})
.onPush({ tags: ["v*"] })
.addJob(createJobId("publish"), (job) => {
job
.permissions({ contents: "read", packages: "write" })
.usesWorkflow("octo-org/shared-workflows/.github/workflows/publish.yml@main", {
with: { artifact: "dist" },
outputs: ["artifact_url"],
secrets: "inherit",
});
})
.build();The @ghawb/cli package provides the ghawb command.
# Render a single workflow
ghawb render --input workflows/ci.ts
# Render a composite action definition
ghawb render --input actions/setup-bun.ts --output actions/setup-bun/action.yml
# Render multiple workflows in one pass
ghawb render \
--input workflows/ci.ts --output .github/workflows/ci.yml \
--input workflows/deploy.ts --output .github/workflows/deploy.yml
# Render with per-target config injection
ghawb render \
--input workflows/ci.ts \
--config ci.render.json \
--output .github/workflows/ci.ymlThe CLI dynamically imports your TypeScript module and renders it to YAML using the bundled YAML adapter. render auto-detects workflow or composite-action modules, validates the default export shape for the selected artifact type, and for the supported repository-local workflow path workflows/<name>.ts infers .github/workflows/<name>.yml when --output is omitted. When multiple explicit --input / --output pairs are provided, render processes each pair in order.
Use --bulk for render-plan manifests in JSON, YAML, or TOML. Use --config immediately after the corresponding --input when you want per-target render-time config injection; the injected value is exposed inside the workflow module through getRenderConfig<T>().
The SDK covers the majority of the GitHub Actions workflow syntax:
- Triggers:
push,pull_request,pull_request_target,workflow_dispatch,workflow_call,workflow_run,schedule,branch_protection_rule, and 20+ simple event types with activity-type filtering - Jobs: step-based and reusable-workflow jobs with
needsdependency validation - Steps:
run(inline commands),uses(action references), script file references with optional expand mode - Strategy:
matrixwith include/exclude,fail-fast,max-parallel - Permissions: granular per-key maps and
read-all/write-allshorthand, at workflow and job level - Environment: named environments with optional URL, per-job and per-step
envmaps - Concurrency: group-based with optional
cancel-in-progress - Container & Services: image, credentials, ports, volumes, Docker options
- Defaults:
defaults.runfor shell and working-directory - Step metadata:
id,if,name,shell,working-directory,with,env,continue-on-error,timeout-minutes - Typed helpers:
actionRef()/workflowRef()for validated references,RunnerLabelconstants for standard runners - Typed action core:
typedActionStep()plusTypedActionStepfor typedusesobjects in the SDK - Opt-in typed action wrappers:
@ghawb/typed-actionsexports typed wrappers for common first-party actions including checkout, cache, setup-node, setup-python, setup-go, setup-java, setup-dotnet, github-script, Pages deployment actions, labeler, and artifact upload/download - Opt-in composite actions:
@ghawb/composite-actionsexportsdefineCompositeAction()and renders through the canonicalghawb renderpath for the first composite-action slice (name,description,inputs,outputs, and ordered compositeruns.steps) - Expression helpers:
expr(), context accessors (github,env,secrets,matrix,inputs,steps,needs), status-check functions (success,failure,always,cancelled), and comparison/logical helpers (literal,eq,ne,gt,gte,lt,lte,and,or,not) for type-safe${{ }}construction - Identifiers: branded
WorkflowIdandJobIdtypes with format validation
For the full support matrix, see docs/SYNTAX_COVERAGE.md.
- Composite actions currently support only the Sprint 20 initial slice:
name, optionaldescription, optionalinputs, optionaloutputs, and compositeruns.stepsusingrun/usesplusname,id,if,env,with,shell, andworking-directory - A few GitHub App-only or deprecated webhook events are not modeled as workflow triggers (
deployment_protection_rule,installation*, classicproject*)
The SDK validates workflows at build() time. If your workflow has structural issues, you get a WorkflowValidationError with an array of diagnostic issues — no CI round-trip required.
Validated constraints include: required triggers, non-empty job lists, step presence, identifier format, needs referential integrity, outputs step-reference validation, duplicate step IDs, permission-level correctness, cron format, matrix axis rules, double-shell rejection on script references, and more.
The SDK catches structural and type-level problems at construction time, but it does not validate the rendered YAML against GitHub's full runtime semantics. For YAML-level static analysis of generated workflow files, use the built-in CLI bridge:
# After rendering, verify the generated YAML with actionlint
ghawb render --input workflows/ci.ts
ghawb lint .github/workflows/ci.yml
# Render and lint in one step
ghawb render --input workflows/ci.ts --lint
# Lint multiple files
ghawb lint .github/workflows/*.ymlIf actionlint is not installed, the CLI will exit with a clear message and installation instructions. You can also invoke actionlint directly:
actionlint .github/workflows/ci.ymlUsing ghawb and actionlint together gives you type-safe construction and YAML-level validation.
For maintained docs and examples, run the repository-owned drift guardrail:
bun run verify:docspackages/
├── shared/ Branded identifiers (WorkflowId, JobId) and shared validation errors
├── sdk/ Workflow model, fluent builders, validation, deterministic renderer
├── job-helpers/ Opt-in high-level helpers such as `nodeCi()` and `nodeBootstrap()`
├── composite-actions/ Opt-in composite action builder, validation, and renderer
├── typed-actions/ Opt-in typed wrappers for common action refs
├── reusable-workflow-import/ Opt-in reusable workflow import
└── cli/ CLI entrypoint, argument parsing, YAML adapter (yaml library)
- Pure TypeScript — no code generation, no macros, no build plugins.
- JSR-only distribution with Bun as the default runtime and Deno compatibility retained.
- Deterministic rendering — the same builder input always produces the same YAML output.
- Pluggable emission — the renderer produces a structured payload; YAML serialization is injected at the CLI edge.
This project maintains 100% SDK line, statement, and function coverage as measured by Vitest's v8 provider over packages/sdk/src/, with a 98% branch threshold. This covers the workflow model, builders, validation, and renderer.
Coverage does not extend to the CLI package, shared utilities, or workflow source files. In this project, "100% coverage" refers to the primary SDK coverage bar over packages/sdk/src/, while branch coverage intentionally uses a slightly lower floor for a small set of low-value branches.
See docs/CONTRIBUTING.md for the contributor verification flow and workflow authoring conventions.
bun run verify:pre-push # Full pre-push verification (recommended)
bun run verify:workflows # Workflow guardrail checks only
bun run check # Format + lint + typecheck + tests
bun run coverage # SDK line coverage with lcov output
bun run test # Vitest + Deno tests
bun run generate:workflows # Re-render all workflow sourcesAuthor committed workflow source modules inside the repository under workflows/. Treat .github/workflows/*.yml as generated output. Render every committed workflow module with bun run generate:workflows after changes, and commit the updated YAML alongside the source.
- Specification — current source of truth for behavior and constraints
- Syntax Coverage — supported/unsupported workflow syntax matrix
- Contributing — verification flow and workflow authoring guidance
- Changelog — release history
- Releasing — release workflow and publishing
- Security — vulnerability reporting policy
- Support — support channels and compatibility policy
- Documentation Index — full list of project docs