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 required CI status checks pass
and any CODEOWNERS-protected paths have been reviewed by a maintainer.
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 — listed in .github/CODEOWNERS.
Responsible for reviewing supply-chain-critical paths and approving
release deployments. They are the required reviewer for the
pypi-releaseenvironment. - 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
PurpleAILAB/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.
Required status checks for main (enforced by repository ruleset):
- 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
- Security (pip-audit + gitleaks) — dependency CVE scan + leaked secret detection
If any of these fail, the PR cannot merge. If any are flaky, fix the flake — do not paper over it by re-running.
Most PRs are self-mergeable once CI is green — no formal review required. This is intentional: it keeps day-to-day work fast.
.github/CODEOWNERS carves out a small set of paths where a maintainer
review is required because the blast radius of a mistake is large.
Today those are:
| Path | Why it needs maintainer review |
|---|---|
.github/workflows/**, .github/actions/**, .github/CODEOWNERS |
These files define what happens in CI and what gates a release; a malicious or careless change here can disable every other gate in the repo. |
pyproject.toml, packages/*/pyproject.toml, uv.lock, package.json, package-lock.json, go.mod, go.sum, .goreleaser.yaml |
These control what ends up published to PyPI, npm, and the launcher binaries. A pinned-dep change is a supply-chain change. |
packages/decepticon-core/decepticon_core/contracts/**, protocols/**, registry/** |
The public plugin API surface. Plugin authors build against these; breakages cascade. |
scripts/install.sh |
Executed by users with curl | bash. One-shot RCE surface. |
docker-compose.yml, containers/*.Dockerfile |
Defines the runtime stack every user spins up. |
If your PR touches one of these paths, expect to wait for a maintainer review. Everything else can land on green CI.
The dismiss_stale_reviews_on_push policy means any new commit
invalidates prior approvals — review is on the exact tree being merged,
not on an earlier version.
- 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. CI / workflow changes go in a CODEOWNERS-reviewed PR of their own.
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 a maintainer
(PurpleCHOIms today; see CODEOWNERS for the canonical list) 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/purpleailab/<image>:<version> \ --certificate-identity-regexp 'https://github\.com/PurpleAILAB/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.
This file is .github/CODEOWNERS-protected indirectly — it lives outside
the protected paths, so any contributor can propose a change. But because
it codifies how the project is operated, please open the change as a PR
with a description that explains what is changing and why, and tag a
maintainer for review even though it is not strictly required.