This document describes how we collaborate day-to-day on the Decepticon OSS repository — branch strategy, code-review flow, what triggers maintainer review, how releases are gated, and the anti-patterns we have learned to avoid. It complements:
- CONTRIBUTING.md — how to participate (setup, PR basics, code conventions)
- RELEASE.md — versioning and the release-workflow walkthrough
- SECURITY.md — how to report vulnerabilities
If you are reading this for the first time, skim sections 1–4. Sections 5 onward are reference for when something specific comes up.
Decepticon is developed on a single trunk (main) using GitHub
Flow — short-lived feature branches, pull requests into main, no
permanent dev / staging / release-x branches. Contributors with
write access can self-merge their PRs once the required CI status check
passes. External publishing (PyPI, GHCR, GitHub Releases) is the only
step that always requires explicit maintainer approval, gated by the
pypi-release GitHub Environment.
This keeps day-to-day velocity high while preserving a hard human gate
between code landing on main and code reaching downstream users.
We do not maintain a separate roles file. The effective roles are:
- Maintainers — the reviewers configured on the
pypi-releaseGitHub Environment (@PurpleCHOImstoday). Responsible for approving release deployments; that environment's reviewer list is the canonical definition of the role. - Write contributors — collaborators with write access on the repo.
May open branches directly on the repo, open PRs, self-merge PRs into
mainonce CI is green, and trigger releases by pushingv*tags. - External contributors — anyone with a GitHub account. Contribute via
forked-repo PRs. Same review path as write contributors, but cannot push
branches directly to
BitterSecurity/Decepticon.
A contributor's role is not a level of trust; it is a question of how the change is delivered (forked PR vs direct branch). The gates below apply the same way regardless.
mainis the only long-lived branch. It is always intended to be releasable.- Feature work happens on short-lived branches. Suggested naming:
feat/<short-slug>— new functionalityfix/<short-slug>— bug fixchore/<short-slug>— tooling / CI / housekeepingdocs/<short-slug>— docs-onlyrefactor/<short-slug>— internal restructure with no behavior change
- Delete the branch after merge.
- We do not maintain a
devorstagingbranch. If you need to stage a multi-PR feature, land each PR behind a feature flag or behind an unimplemented entry point so partial progress onmainis harmless. - We do not cut long-lived release branches. The repo state at tag
vX.Y.Zis the release. Hotfixes for an already-released line are handled by tagging a new patch frommainafter a forward fix lands.
- Branch off the latest
main. - Make the change. Run the relevant local gates from the
Makefile before you push — at minimum the lane that
matches what you changed:
These mirror the GitHub Actions CI lane exactly; the Makefile is the single source of truth.
make quality # PR-gate mirror: Python + CLI + Web lint/typecheck/test make ci-lint # just lint + typecheck make ci-test # just tests
- Open the PR against
main. Use a Conventional-Commit-style title (type(scope): description) — this is what CONTRIBUTING.md and the commit log already follow. - Fill in the PR description: what changed and why. Linked issues, reproduction context for bug fixes, and breaking-change notes belong here.
main requires exactly one status check: CI OK (required status).
It is an aggregator job in .github/workflows/ci.yml that fails if any
of the lanes it needs failed or was cancelled:
- Python (lint + typecheck + test) —
make ci-lint+make ci-test - CLI (ubuntu-latest) / CLI (macos-latest) — typecheck + build + test for the Ink CLI
- Web (lint + build) — Prisma generate, ESLint, Next.js build
- Launcher —
go vet+go test - Compose validate, Docker build (amd64 + arm64)
- Security (pip-audit + gitleaks) — dependency CVE scan + leaked secret detection
- actionlint — workflow syntax
Adding a new CI job means adding it to ci-ok's needs: list; the
ruleset itself does not need to change. If a lane fails, the PR cannot
merge. If a lane is flaky, fix the flake — do not paper over it by
re-running.
PRs are self-mergeable once CI is green. There is no per-path review
requirement: .github/CODEOWNERS was retired in
ADR-0012, which explains why
(the gate was configured in a way that never blocked a merge, and
enforcing it is self-blocking while there is a single code owner).
Review is therefore a convention, not a branch rule. Ask for one when
the change is one you would want a second pair of eyes on — and
supply-chain surfaces are still where that pays off most:
.github/workflows/**, scripts/install.sh, docker-compose.yml and
containers/*.Dockerfile, package manifests and lockfiles, and the
plugin contracts under packages/decepticon-core/decepticon_core/. The
Blast radius section of the PR template exists to make you name which
of those you are touching.
The hard human gate has not moved: nothing you merge reaches a user
until a maintainer approves the pypi-release deployment (§5).
- Use squash merge by default. Use merge-commit only when the branch represents a coherent series of independently meaningful commits (uncommon).
- Delete the branch after merge.
- Do not force-push to
main, deletemain, or rewrite published history — the ruleset blocks deletion / non-fast-forward updates ofmain. The same ruleset blocks deletion / update / non-fast-forward of anyv*tag once created.
These are written here because each one has caused real damage in the past, not as theoretical hygiene.
- Mega-PRs that bundle unrelated work — for example, "merge all open PRs", "integrate the backlog", "consolidated branch". They hide review surface, corrupt blame history, and have shipped regressions before. Land each PR individually. If two PRs genuinely depend on each other, say so in the description.
- AI co-author trailers (
Co-Authored-By: Claude,Co-Authored-By: Copilot, etc.) on commits. They are not used in this repo and will be stripped. Use the assistant for help; commit under your own identity. - Skipping hooks (
--no-verify,--no-gpg-sign) without explicit maintainer agreement. If a hook fails, fix the underlying issue. - Bypassing CI by editing or removing required checks in the same PR
— weakening
ci-ok'sneeds:list, or making a lane pass unconditionally, alongside the change it would have caught. Workflow changes go in a PR of their own so the diff is reviewable on its own terms. Nothing in the repository configuration blocks this; it is the one anti-pattern the gates cannot catch for us (see ADR-0012 §Consequences).
The release process is documented end-to-end in RELEASE.md. This section covers only the collaboration gate — who approves what, and where the human-in-the-loop step lives.
.github/workflows/release.yml triggers on push: tags: ["v*"]. Every
job in that workflow that produces an externally-visible artifact
declares environment: pypi-release:
publish-pypi— three workspace wheels to PyPI via Trusted Publishinglauncher— GoReleaser binaries + config-checksums manifest attached to the GitHub Releasedocker,docker-heavy,docker-heavy-merge,docker-web,docker-web-merge— multi-arch image builds, GHCR pushes, cosign keyless signing, CycloneDX SBOM generation and attestationpublish-release— promotes:<version>→:latestand undrafts the GitHub Release
The pypi-release environment requires approval from one of its
configured reviewers (PurpleCHOIms today — the environment's reviewer
list is the canonical definition, see §2) before any of these jobs can
start their runs-on block. The deployment branch
policy on this environment is restricted to refs/tags/v*, so the
environment cannot be invoked from workflow_dispatch on main or any
non-tag ref.
.github/workflows/release-recover.yml (the workflow_dispatch recovery
path used to re-promote :latest after a partial-failure rerun) is
gated by the same environment for the same reason.
- You land your fixes on
mainvia normal PRs. - You (or a maintainer) push the tag:
git tag vX.Y.Z && git push origin vX.Y.Z. - The Release workflow starts and immediately pauses on every gated job, showing pending deployments in the "Review deployments" widget on the workflow-run page.
- A maintainer reviews the pending deployments and approves them. Builds then proceed and publish externally.
- If anything looks wrong at step 3 — wrong tag, wrong commit, wrong
moment — a maintainer rejects the deployment. Nothing leaves the repo.
Delete the tag and start over once the issue is fixed. (Note: the
tag-immutability-vruleset blocks deletion of a tag once created — only organization admins can bypass this, and only for recovery from a botched tag.)
These are baked into release.yml and do not require contributor action,
but you should know they exist so you can verify a release artifact:
- PyPI Trusted Publishing (OIDC) — no long-lived API token. The PyPI
side also requires the OIDC token to come from the
pypi-releaseenvironment; defense in depth in case theenvironment:line is ever removed fromrelease.yml. - Cosign keyless signing — every GHCR image and multi-arch manifest
is signed via Sigstore. Verify with:
cosign verify ghcr.io/bittersecurity/<image>:<version> \ --certificate-identity-regexp 'https://github\.com/BitterSecurity/Decepticon/' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com
- CycloneDX SBOMs — generated by
anchore/sbom-actionfor each image and attested to the OCI artifact viacosign attest. Also uploaded as workflow artifacts. - Config-checksums manifest —
docker-compose.yml,config/litellm.yaml, and.env.exampleget a sha256 manifest attached to the GitHub Release. The launcher and install script verify against this before writing user files. Treat this as the source of truth for those three files; do not fetch them fromraw.githubusercontent.com/mainfor production use.
- Bugs: open an issue using the Bug Report template.
- Features / design discussion: Feature Request template, or open a draft PR with a design sketch in the description.
- Security: do not open a public issue. Follow SECURITY.md.
- Release-blocking questions: leave them on the PR or issue in question; do not DM maintainers about open-source release decisions.
Any contributor can propose a change to this file; no branch rule gates it. But because it codifies how the project is operated, open the change as a PR with a description that explains what is changing and why, and tag a maintainer for review even though nothing requires you to. If the change reverses a decision rather than clarifying one, it wants an ADR (see docs/adr/README.md) — this document describes current practice; ADRs record why it changed.