Mental model
1. describe expected behavior in CUE
2. produce a normalized workflow payload
3. validate expected vs actual
Getting Started
The smallest useful setup is one workflow, one declaration, one payload, and one validation command. The key idea is simple: compare expected workflow behavior with actual workflow behavior.
Mental model
1. describe expected behavior in CUE
2. produce a normalized workflow payload
3. validate expected vs actual
Use this when
Step 1
Start from the workflow file, not from a blank page. Generate the starter contract and baseline payload first, then tighten the generated files.
Bootstrap from the workflow file
just bootstrap-actionspec . ci.yml
# path inputs are also accepted
github-actionspec bootstrap \
--repo . \
--workflow .github/workflows/ci.yml
What it writes
.github/actionspec/ci/main.cuetests/fixtures/ci/baseline.json.github/actionspec/ci/bootstrap-ci-snippet.ymlStep 2
Use just first. In this repo, just lint already includes
reusable workflow interface validation through validate-callers.
Development
just docker-build
just build
just lint
just test
Contracts
just discover .
just validate-callers .
just validate-callers-report . \
target/actionspec/callers-report.json
Payloads
just validate-repo-dashboard . ci.yml \
.github/actionspec-artifacts/**/*.json \
target/actionspec/validation-report.json \
target/actionspec/dashboard.md
Step 3
The action bundles the Rust binary and cue, so the workflow only needs a
checkout plus payloads or fragments. This is the same validation model, just running
inside GitHub Actions.
Start from the generated snippet
- uses: actions/checkout@v6
- name: Validate ci.yml contract
id: actionspec
uses: v4lproik/github-actionspec-rs@main
with:
workflow: ci.yml
actual: tests/fixtures/ci/baseline.json
report-file: /github/runner_temp/actionspec/current/validation-report.json
dashboard-file: /github/runner_temp/actionspec/current/dashboard.md
- name: Upload validation artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: actionspec-ci-baseline
path: |
${{ steps.actionspec.outputs.report-path }}
${{ steps.actionspec.outputs.dashboard-path }}
Then expand from there