GitHub Actions-native CI/CD orchestration for teams and AI agent swarms.
Flywheel is a lightweight orchestration layer that automates the journey from a conventional commit to a versioned release artifact — without prescribing how you build, what you publish, or how your branches relate to each other.
It is not a long-running orchestrator. It is a collection of short-lived, single-purpose event reactions. The repository itself — its branches, tags, PRs, and labels — is the state machine.
- Parses every PR title as a conventional commit. Rewrites the title and body. Applies one of two labels:
flywheel:auto-mergeorflywheel:needs-reviewbased on the rules in your.flywheel.yml. - Enables GitHub native auto-merge into the merge queue when eligible.
- On every push to a managed branch, generates
.releaserc.jsonand gates a separatesemantic-releasestep that computes the version, tags, and creates a GitHub Release. - On every push to a non-terminal branch in a stream, upserts a single open promotion PR to the next branch in the stream.
What Flywheel does not own: your quality checks, your build, your publish. You write those as separate workflows. Quality checks register as required status checks on managed branches (and must subscribe to both pull_request and merge_group to be merge-queue compatible). Build and publish react to the release: published and workflow_run: [Build] completed events Flywheel produces — a 30-minute mobile build incurs no waiting cost on the Flywheel pipeline side.
Run from your repo, with gh auth login already done:
curl -fsSL https://raw.githubusercontent.com/point-source/flywheel/main/scripts/init.sh | bashinit.sh picks a .flywheel.yml preset, writes both Flywheel workflow files, prompts for the GitHub App credentials, and (optionally) applies the recommended branch + tag rulesets. Validate any time with:
curl -fsSL https://raw.githubusercontent.com/point-source/flywheel/main/scripts/doctor.sh | bashAdopting into an existing repo with prior version tags, release automation, or branch protection? Skip
init.shand start with docs/adopter-setup.md §0 — it covers the audit and cleanup steps the script doesn't.
The hand-rolled equivalent — four files in your repo:
your-repo/
├── .flywheel.yml ← you write this
└── .github/workflows/
├── flywheel-pr.yml ← copy from docs/adopter-setup.md
├── flywheel-push.yml ← copy from docs/adopter-setup.md
├── quality.yml ← you write: on: pull_request + merge_group
├── build.yml ← you write: on: release published
└── publish.yml ← you write: on: workflow_run
A minimal .flywheel.yml:
flywheel:
streams:
- name: main-line
branches:
- name: develop
release: prerelease
suffix: dev
auto_merge: [fix, chore, refactor, perf, style, test, docs]
- name: main
release: production
auto_merge: []See docs/adopter-setup.md for the full setup walkthrough including the workflow templates, branch protection rulesets, and required secret scopes.
agent / developer pushes a branch
│
▼
PR opened against a managed branch
│
▼
flywheel-pr workflow → pr-conductor:
├── parse + rewrite title/body
├── compute increment (major / minor / patch / none)
├── apply flywheel:auto-merge XOR flywheel:needs-review
└── enable native auto-merge if eligible
│
▼
PR merges → push to managed branch
│
├──────────────────────────────────┐
▼ ▼
flywheel-push.yml flywheel-push.yml
(release flow) (promotion flow)
├── write .releaserc.json ├── compute pending commits (commit-message-based)
├── npx semantic-release ├── upsert promotion PR to next branch in stream
├── tag + GitHub Release └── label + enable auto-merge if eligible
└── back-merge tag + chore(release)
into upstream branches in the stream
│
▼
release: published
└── your build.yml fires
│
▼
workflow_run: build completed
└── your publish.yml fires
- Stateless and event-driven. No workflow waits for another. Each workflow reacts to one event, does one job, and exits.
- No double billing. Build and publish workflows are triggered by events Flywheel produces, not called synchronously.
- Language and destination agnostic. Flywheel produces a version, a changelog, and a tag. What you do with those is up to your build/publish workflows.
- No assumed branch hierarchy. Branch relationships are defined by stream membership in
.flywheel.yml. A project with one stream containing one branch and a project with six parallel streams use the same system. - Version numbers are stream-scoped. Within a stream, the base version is consistent across branches —
v1.3.0-dev.2,v1.3.0-rc.1, andv1.3.0all represent the same logical release. - One config file.
.flywheel.ymlis the single source of truth. Flywheel derives semantic-release config from it at runtime; adopters never configure.releaserc.jsondirectly.
Flywheel mints its own installation token from the GitHub App credentials you supply via the app-id / app-private-key inputs. The App needs these scopes:
| Scope | Purpose |
|---|---|
| Contents: r/w | Tag creation, .releaserc.json write to workspace |
| Pull req: r/w | PR creation, body updates, native auto-merge enabling |
| Issues: r/w | Adding / removing the flywheel:* labels on PRs |
| Checks: r/w | Posting the flywheel/conventional-commit check |
| Metadata: read | Required for any token interacting with a repo |
Adopters store FLYWHEEL_GH_APP_ID as a repo Variable (it's not sensitive — the App ID is printed on the App's settings page) and FLYWHEEL_GH_APP_PRIVATE_KEY as a repo Secret, and pass them straight into the action — no separate actions/create-github-app-token step. Personal Access Tokens are not supported (they don't reliably propagate the cross-workflow trigger semantics Flywheel relies on); secrets.GITHUB_TOKEN is similarly insufficient (it cannot trigger downstream workflows from PRs it creates).
| Input | Required | Description |
|---|---|---|
event |
yes | pull_request or push |
app-id |
yes | GitHub App ID; typically vars.FLYWHEEL_GH_APP_ID |
app-private-key |
yes | App private key (PEM); typically secrets.FLYWHEEL_GH_APP_PRIVATE_KEY |
| Output | Description |
|---|---|
token |
Minted installation token (masked). Pass to downstream steps that need it (e.g. semantic-release). |
managed_branch |
'true' if the pushed/targeted branch is in a stream; 'false' otherwise. |
back_merge_targets |
Comma-separated list of upstream branches in the same stream that should receive a back-merge after this branch releases. Empty for single-branch streams or when the branch is the head of its stream. |
Flywheel recognizes the standard conventional commit types: feat, fix, chore, refactor, perf, style, test, docs, build, ci, revert. Append ! to indicate a breaking change. The BREAKING CHANGE: and BREAKING-CHANGE: footers in commit bodies are also detected.
| Commit | Increment |
|---|---|
Any type with ! or BREAKING CHANGE: footer |
major |
feat |
minor |
fix, perf |
patch |
chore, refactor, style, test, docs, build, ci |
none |
Non-bumping commits accumulate silently until a qualifying commit lands.
Flywheel validates .flywheel.yml on every run. Validation errors fail the action with a descriptive check and post a failing status. Notable rules:
- A branch may belong to only one stream.
- Only the last branch in a stream may be a production release branch.
- Only one stream may produce the primary
v${version}tag namespace; all other streams use a stream-prefixed tag format. auto_mergeentries must be recognized conventional commit types (with optional!).
See spec.md §Validation for the full list.
npm install
npm run typecheck
npm test
npm run build
npm run verify-dist # rebuilds and fails if dist/ drifts from sourceSource is TypeScript under src/; the bundled dist/index.cjs is committed and is what GitHub executes. The verify-dist workflow ensures the bundle stays in sync.
See CONTRIBUTING.md for the full contributor workflow, sandbox testing, and PR conventions.
MIT — see LICENSE.md.
- spec.md — full specification
- docs/adopter-setup.md — adopter walkthrough
- CONTRIBUTING.md — contributor and sandbox-testing guide
- docs/maintainer-setup.md — operating Flywheel itself
- docs/maintainer-release-process.md — cutting a Flywheel release