Reusable GitHub Actions checks for Python and Flutter projects. Each caller project keeps one thin workflow file; everything complicated — lint, type checks, tests, dependency and secret scanning — lives here once and is versioned once.
Checks only. There is no deploy workflow here, and that is deliberate — see Why there is no deploy below.
| File | Does | Sees secrets |
|---|---|---|
python-backend.yml |
ruff, mypy, pytest (Postgres/Redis optional), pip-audit |
no |
security.yml |
gitleaks over full history, CodeQL, Trivy, dependency-review | no |
flutter-android.yml |
analyze, test, and an APK when asked for one | keystore, only when signing |
dart-package.yml |
format, analyze, test, the example, changelog and pana | no |
All four run on GitHub's cloud runners. None of them touches a server, holds a key, or has any route into a private network.
Copy examples/ci.yml into a project's .github/workflows/, and
examples/dependabot.yml into its .github/. Delete the jobs and ecosystems
you don't need (a Python-only repo drops the app job and the pub/gradle
entries; a Flutter-only repo drops backend and pip/docker), then adjust
working-directory, the Flutter version and the codegen commands to match your
layout. That is the whole integration — about twenty lines per project, so a
fix here fixes every project at once.
flutter-android.yml verifies an application; dart-package.yml verifies
something you publish to pub.dev, which needs a different gate — formatting,
--fatal-infos analysis, tests, the example/ (which pana does not look at),
a CHANGELOG entry for the version in pubspec.yaml, and the pub.dev score
itself with --exit-code-threshold 0. A caller is two lines:
jobs:
package:
uses: CtrlAltDevelop/ci-workflows/.github/workflows/dart-package.yml@v1.1.0
with:
sdk: flutter # or "dart" for a package with no Flutter dependency
runs-on: macos-latest # only if the package has golden testsEach workflow's inputs are documented as comments in its on: workflow_call:
block — see python-backend.yml,
flutter-android.yml,
dart-package.yml and
security.yml for the full list.
Callers reference @v1, which is resolved when a run starts. Change a
workflow here, forget to move the tag, and the next run silently uses the old
version — the failure then looks exactly like a bug you already fixed. This cost
two debugging sessions during bring-up.
Push and retag in one command, always:
git push origin main && git tag -f -a v1 -m "v1" && git push -f origin v1For a change that breaks callers, cut v2 and migrate projects one at a time
rather than moving v1.
dart-package.yml arrived in v1.1.0, and the sixteen package repositories
pin that tag rather than a moving major tag. A pinned tag does not move, so
those repos opt into a change here instead of receiving it — worth it on a
gate that blocks sixteen release pipelines at once. Cut v1.2.0 for the next
change to it and bump the callers:
git push origin main && git tag -a v1.2.0 -m "v1.2.0" && git push origin v1.2.0This repository is public, so any repository can call these workflows with no extra setup. If you fork it and keep the fork private, a caller in a different private repository needs Settings → Actions → General → Access set to accessible from repositories owned by the user on this repo — without it every caller fails with "reusable workflow not found", which is not an obvious error message.
There was a deploy-stack.yml: it built on the target server through a
self-hosted runner, tagged images by commit, ran migrations and health checks,
and rolled back by tag. It was removed in September 2026, and the reasons are
worth keeping so the idea is not reinvented without them:
- Two repositories and one moving tag. Every pipeline fix meant a commit here, a retag, and a re-trigger in the calling project. A meaningful share of the failures during bring-up were not real — they were runs that started before the tag moved.
- A self-hosted runner is a standing shell inside the network. It executes
repository code as a user in the
dockergroup, which on Linux is root-equivalent. That is a fair trade for real automation and a poor one for automation that is fighting you. - The gain was small at this size. One developer per project, one test server. What a deploy pipeline really buys is safety when several people push and nobody is watching.
Deploying is now a command on the server — git pull and
docker compose up -d --build, wrapped in a make target. What that costs,
stated plainly: nothing prevents deploying untested code, there is no deploy
history, and rollback is a checkout rather than a click.
That is a size-appropriate trade, not a permanent verdict. When a project gets a production server or a second person pushing, rebuilding the deploy half is the right call — start from the git history of this repository, where the removed workflow still lives.
- Every workflow starts
permissions: {}; each job asks for exactly what it needs. - Android release signing requires a GitHub Environment. A job only reaches
environment secrets by declaring the environment, and a caller job that
uses:a reusable workflow cannot declare one — so the declaration sits insideflutter-android.yml, andsign: truewithout an environment fails on purpose. Pull-request builds are debug-signed and never see the key. - Actions here are pinned to tags, not commit SHAs — a mutable reference, and
the one real supply-chain gap left. Convert with
pinact runand let Dependabot maintain them.
MIT.