Local CI iteration
Iterating on CI workflows can be painful when each "try again" costs a push, a CI cycle, and a merged PR. Modgud's setup leaves two escape hatches so you can iterate cheaply.
The escape hatches at a glance
| Tool | Best for |
|---|---|
act | Quick local sanity-check of a workflow's steps without pushing |
workflow_dispatch + dry_run | The release pipeline (cd-release.yml) — exercise the full publish flow without actually shipping |
ci/** branch trigger | Iterate on the CI itself with real GitHub runners but no PR ceremony |
You'll probably want all three eventually. They cover different failure modes.
act — run workflows locally
act executes GitHub Actions workflows on your machine in a Docker container that emulates an Actions runner. About 80% of workflow bugs surface here: YAML syntax, missing inputs, broken bash pipes, wrong working-directory.
Install
# Windows
scoop install act
# or
choco install act-cli# macOS / Linux
brew install actOne-time setup
Create .actrc in the repo root (gitignored — never commit local overrides):
-P ubuntu-latest=ghcr.io/catthehacker/ubuntu:act-latest
--container-architecture linux/amd64The first directive swaps act's default minimal image for one that ships the standard toolchain (Node, Python, dotnet, common CLIs) — closer to GitHub's real runner. The second keeps things on amd64 for Apple Silicon compatibility.
If a workflow needs secrets, drop them in .secrets (also gitignored):
NUGET_API_KEY=fake-for-dry-run
SHELF_API_KEY=fake-for-dry-run
SHELF_BASE_URL=https://localhost
GITHUB_TOKEN=ghp_dummytokenfordryrunsThen point act at it: act --secret-file .secrets.
Common runs
# All PR-triggered workflows for the current diff
act pull_request
# A specific workflow file
act -W .github/workflows/ci.yml
# A specific job inside a workflow
act -j docs -W .github/workflows/ci.yml
# Manual-trigger workflow with inputs
act workflow_dispatch -W .github/workflows/cd-release.yml \
--input dry_run=true \
--input version_override=0.0.0-testKnown limitations
- Some actions check for
GITHUB_ACTIONS=trueor for a specific runner image and behave differently — match against real GH before claiming "works in act, will work in CI". - The
github.event.release.*context isn't populated under workflow_dispatch — that's whycd-release.ymlusesinputs.version_overridefor the dispatch path. docker/build-push-actionwithpush: trueagainst GHCR needs a real runner (or real auth) to actually push. For local iteration: setpush: false(or use the dry-run path ofcd-release.ymlwhich already gates it).- Container startup is slow the first time (image download). After that it's fast.
workflow_dispatch + dry_run — for cd-release.yml
The release pipeline only triggers on release: published events in production. To iterate on it without cutting fake releases, use the manual trigger with dry_run enabled:
# From your local machine, against a ci/ branch you pushed
gh workflow run cd-release.yml \
--ref ci/fix-release-pipeline \
-f version_override=0.0.0-test \
-f dry_run=trueWhat runs in dry-run:
validate-version— exercises the version-format checktest-backend— full unit + integration suitepack-nuget— packs the client-library nupkgbuild-docker— builds the image, doesn't push to GHCRbuild-docs— full VitePress buildrelease-gate— confirms all builds succeeded
What's skipped in dry-run:
publish-nuget— would push to nuget.orgpublish-docker— would retag candidate to public release tagsdeploy-docs— would upload the docs zip to Shelf
To do a non-dry-run dispatch (e.g. you need to republish a specific commit), pass -f dry_run=false. The workflow will then actually publish — be sure that's what you want.
ci/** branch trigger — for the CI workflows themselves
The ci.yml workflow triggers on pushes to develop and any branch matching ci/** (and on pull requests). So if you're tweaking ci.yml itself or a build setup, you can:
git checkout -b ci/tweak-path-filter
# make your changes
git push -u origin ci/tweak-path-filterThe CI runs as if it were a develop push (with path filtering, so docs-only changes don't waste backend test time). Iterate as many times as you need on that branch, no PR overhead. When the workflow is right, open one clean PR with the final diff.
Picking the right tool
- Editing a workflow's structure / step logic? →
actfirst. Catches the syntax/logic problems in seconds. - Editing the release pipeline specifically? →
workflow_dispatchwithdry_run=true. Real runner, real artifacts, no production side-effects. - Editing CI behaviour and want a real-runner verification of paths-filter / status reporting / matrix expansion? →
ci/**branch. - You need all of it because the change is non-trivial? → use all three in sequence.
actfirst to weed out the basics, thenci/**push for a real-runner check, thenworkflow_dispatchif you also touched the release flow.
Related
- Composite actions live in
.github/actions/(setup-dotnet,setup-node-pnpm) — bump tool versions there, not in every workflow. - Workflow design rationale lives in the workflow file headers themselves; start there if a workflow's intent is unclear.