Your Dippin workflows deserve the same CI treatment as your application code.
Here's how to set up GitHub Actions
to validate, lint, test, and format-check your .dip files on every push.
We'll build a workflow YAML from scratch, then look at how dippin-lang dogfoods
its own CI.
A Dippin CI pipeline has four stages. Each catches a different class of problem:
| Stage | Command | What it catches | Exit code on failure |
|---|---|---|---|
| Validate | dippin validate |
Structural errors (DIP001-DIP009) | 1 |
| Lint | dippin lint |
Semantic warnings (DIP101-DIP125) | 0 (warnings don't fail) |
| Test | dippin test |
Scenario test failures | 1 |
| Format | dippin fmt --check |
Non-canonical formatting | 1 |
dippin validate and dippin test exit 1 on failure,
making them suitable as CI gates. dippin lint exits 0 even with
warnings -- warnings are advisory, not blocking. To fail CI on warnings,
use dippin check, which combines validate + lint and can
be configured to fail on warnings.
Create .github/workflows/dippin.yml in your repository:
name: Dippin CI
on:
push:
branches: [main]
paths: ['**/*.dip', '**/*.test.json']
pull_request:
branches: [main]
paths: ['**/*.dip', '**/*.test.json']
jobs:
dippin:
name: Validate, Lint, Test, Format
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: true
- name: Install dippin
run: go install github.com/2389-research/dippin-lang/cmd/dippin@latest
- name: Validate all .dip files
run: |
failed=0
for f in $(find . -name '*.dip' -not -path './.git/*'); do
if ! dippin validate "$f"; then
failed=1
fi
done
exit $failed
- name: Lint all .dip files
run: |
for f in $(find . -name '*.dip' -not -path './.git/*'); do
dippin lint "$f"
done
- name: Run scenario tests
run: |
failed=0
for f in $(find . -name '*.dip' -not -path './.git/*'); do
test_file="${f%.dip}.test.json"
if [ -f "$test_file" ]; then
if ! dippin test "$f"; then
failed=1
fi
fi
done
exit $failed
- name: Check formatting
run: |
failed=0
for f in $(find . -name '*.dip' -not -path './.git/*'); do
if ! dippin fmt --check "$f"; then
echo "::error::$f is not formatted. Run: dippin fmt --write $f"
failed=1
fi
done
exit $failed
The paths filter means the job only runs when .dip or
.test.json files change, saving CI minutes.
Validation catches fatal errors. If a workflow fails validation, nothing else matters -- the file is structurally broken. Run this first.
$ dippin validate pipelines/deploy.dip FAIL pipelines/deploy.dip DIP001: start node "Begin" is not defined DIP004: node "Cleanup" is unreachable from start
Validation errors block CI: exit code 1. Fix the structural issue before proceeding.
Linting checks for best-practice violations that aren't structurally wrong but may indicate problems:
$ dippin lint pipelines/deploy.dip WARN pipelines/deploy.dip DIP108: node "Analyze": unknown model "gpt-4-turbo" for provider "openai" DIP111: tool node "RunTests" has no timeout DIP103: node "Review" has no prompt
Warnings exit 0 by default. The CI step runs lint for visibility -- developers
see warnings in the job log. To block merges on warnings, swap in
dippin check.
Scenario tests verify that given specific context values, the workflow follows the expected execution path. See the Scenario Testing guide for details on writing test files.
$ dippin test pipelines/deploy.dip PASS happy path -- all checks pass PASS validation fails -- routes to manual review FAIL timeout -- routes to fallback expected visited: [Fallback], got visited: [Retry] 2/3 passed pipelines/deploy.dip
Any test failure exits 1, blocking the pipeline.
The --check flag verifies formatting without modifying the file.
Non-canonical formatting exits 1:
$ dippin fmt --check pipelines/deploy.dip FAIL pipelines/deploy.dip is not formatted
The fix is always the same: dippin fmt --write pipelines/deploy.dip.
The formatter is idempotent, so formatting an already-formatted file is a no-op.
Use --format json with dippin check to produce
machine-readable output for GitHub job summaries:
$ dippin --format json check pipeline.dip { "file": "pipeline.dip", "valid": true, "errors": 0, "warnings": 2, "diagnostics": [ {"code": "DIP111", "severity": "warning", "message": "tool node \"RunTests\" has no timeout"}, {"code": "DIP103", "severity": "warning", "message": "node \"Review\" has no prompt"} ] }
Parse it with jq to build Markdown tables for
$GITHUB_STEP_SUMMARY:
- name: Lint summary
run: |
summary="## Dippin Lint Results\n\n"
summary+="| File | Status | Errors | Warnings |\n"
summary+="|------|--------|--------|----------|\n"
for f in pipelines/*.dip; do
output=$(dippin --format json check "$f" 2>/dev/null) || true
valid=$(echo "$output" | jq -r '.valid // false')
errors=$(echo "$output" | jq -r '.errors // 0')
warnings=$(echo "$output" | jq -r '.warnings // 0')
status="pass"
[ "$valid" != "true" ] && status="fail"
summary+="| \`$f\` | $status | $errors | $warnings |\n"
done
echo -e "$summary" >> "$GITHUB_STEP_SUMMARY"
This renders a table in the GitHub Actions run summary showing the status of every pipeline file at a glance.
The dippin-lang repository
runs five CI jobs on every push and pull request. The most relevant for pipeline
authors is validate-examples, which runs seven checks against
all example files:
| Step | What it does |
|---|---|
| Parse all .dip examples | Verifies the parser accepts every file |
| Validate all .dip examples | Structural correctness |
| Lint all .dip examples | Semantic best practices |
| Format check (idempotency) | Formats twice, asserts output is identical |
| Full round-trip | Parse, format, re-parse, validate -- nothing lost |
| Validate migration parity | DOT/Dippin pairs remain structurally equivalent |
| Simulate all examples | Dry-run simulation for each workflow |
The idempotency check is worth highlighting. It formats the file, formats the formatted output, and asserts they're identical. This catches formatter bugs where a second pass changes the output:1
- name: Format check all .dip examples (idempotency)
run: |
failed=0
for f in examples/*.dip; do
formatted=$(./dippin fmt "$f" 2>/dev/null)
reformatted=$(echo "$formatted" | ./dippin fmt /dev/stdin 2>/dev/null)
if [ "$formatted" != "$reformatted" ]; then
echo "::error::$f is not format-idempotent"
failed=1
fi
done
exit $failed
A separate dippin-lint job uses dippin check
with JSON output to generate a summary table in the GitHub Actions UI --
Dippin linting its own example files:
$ dippin --format json check examples/code_quality_sweep.dip { "file": "examples/code_quality_sweep.dip", "valid": true, "errors": 0, "warnings": 0, "diagnostics": [] }
If you already have a GitHub Actions workflow, add Dippin checks as a new job or append steps to an existing one:
# Add to your existing workflow
dippin-check:
name: Dippin Workflows
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25'
- run: go install github.com/2389-research/dippin-lang/cmd/dippin@latest
- run: |
for f in $(find . -name '*.dip' -not -path './.git/*'); do
dippin check "$f"
done
The minimum viable CI step is a single dippin check loop --
it combines validate + lint in one command. Add dippin test and
dippin fmt --check when you want stricter enforcement.
For instant local feedback before pushing, add Dippin checks to a pre-commit hook:
#!/bin/sh # .git/hooks/pre-commit # Check all staged .dip files staged_dip=$(git diff --cached --name-only --diff-filter=ACM | grep '\.dip$') if [ -n "$staged_dip" ]; then for f in $staged_dip; do if ! dippin validate "$f"; then echo "Validation failed for $f" exit 1 fi if ! dippin fmt --check "$f"; then echo "$f is not formatted. Run: dippin fmt --write $f" exit 1 fi done fi
Your CI pipeline now catches structural errors, semantic issues, test failures, and formatting drift.
formatter/.