Last updated 2026-05-07
Authoring a Workflow
A worked example: write a CI workflow from scratch, run it locally, and ship it. By the end, the same YAML works on cloud GitHub Actions and on the local runner.
1. Sketch the workflow
The simplest useful CI: install, lint, type-check, test.
name: CIon: push: branches: [main] pull_request: workflow_dispatch: inputs: skip-tests: description: "Skip the test job" required: false default: "false"
jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '22', cache: 'npm' } - run: npm ci - run: npm run lint
typecheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: '22', cache: 'npm' } - run: npm ci - run: npx tsc --noEmit
test: needs: [lint, typecheck] if: ${{ inputs.skip-tests != 'true' }} runs-on: ubuntu-latest strategy: matrix: node: [18, 20, 22] fail-fast: false steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: ${{ matrix.node }}, cache: 'npm' } - run: npm ci - run: npm test - uses: actions/upload-artifact@v4 if: always() with: name: test-output-node${{ matrix.node }} path: junit.xml2. Run it locally
stax runner start # if not already runningstax workflows # confirm "ci" appearsstax run ci # default inputsstax run ci -i skip-tests=true # workflow_dispatch inputWatch the live event stream:
# Live log of the most recent runstax log -f
# Or open the dashboardstax dashboard3. Iterate
Local-first means: edit ci.yml, hit stax run
ci again. No commits required. The runner re-discovers the
file on each trigger.
Common iteration patterns
| Goal | How |
|---|---|
| Pin a single matrix combination while debugging | Add if: matrix.node == '20' to a step. |
| Skip slow steps during iteration | Add a workflow_dispatch input and gate the step's if:. |
| Inject test secrets | Use stax secrets sync runner for repo-backed values, or stax secret set DB_URL postgres://... for ad hoc local-only values. Reference ${{ secrets.DB_URL }}. |
| Avoid sandbox setup time | stax run ci --no-sandbox while iterating; remove for the final run. |
4. Use built-in shims to your advantage
These actions are shimmed locally, so they "just work" without a real GitHub Actions runner image:
actions/checkout— already mounted, no-op.actions/setup-{python,java,go,ruby,dotnet,gradle,xcode}— detect system installs.actions/cache— local filesystem cache.actions/upload-artifact/download-artifact— copies under.artifacts/.
See the marketplace actions page for the full shim table.
5. Add expression-driven gates
jobs: deploy: needs: [test] if: github.ref == 'refs/heads/main' && success() runs-on: ubuntu-latest steps: - run: ./scripts/deploy.sh env: DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}6. Ship the workflow
stax diff "Add CI workflow"stax syncstax submit# Once approved:stax landThe same YAML now runs on GitHub Actions automatically because that's what the file was always for. Local execution gave you a fast feedback loop without committing half-finished changes.