Canonical release policy for Forge. GitHub Flow — one long-lived branch (
main), short-lived feature branches, and serialized releases to the single self-hosted instance.
main ← only long-lived branch; always deployable; releases tagged here
codex/<issue>-* ← Codex/Forge agent work
<contributor>/<issue>-* ← human contributor work
fix/<issue>-* ← bugfix branches from main
Rules:
mainis always deployable. Every merge tomaincan ship.- Work lands via PR (review surface for humans + agents). Branch protection isn't available on this plan, so this is convention — keep it anyway.
- Branches are short-lived — open the PR within a day or two of branching; delete merged branches immediately.
- One Forge issue owns one active work session, isolated worktree/branch, and primary implementation PR. Claim it before editing; stale sessions must be resumed or abandoned, never silently replaced.
- The checkout used for production is deployment-only. Development happens in task-owned worktrees and never in the production or integration checkout.
- No long-lived
dev/stagingbranch. Staging may deploy an exactmainSHA; an environment does not require a matching branch. Introducedevonly if Forge needs a genuine integration/stabilization train, then document the new PR, release, hotfix, and back-merge rules before using it.
Forge's explicit branch contract:
| Setting | Value |
|---|---|
| Integration branch / normal PR base | main |
| Release branch / tag source | main |
| Staging source | Exact tested main SHA |
| Production source | Immutable vX.Y.Z tag contained in main |
| Hotfix base | Current production tag or main |
| Back-merge target | None; main is the only long-lived branch |
Semantic Versioning 2.0.0. Pre-1.0 (0.x): MINOR is the
"feature" bump, PATCH is fixes; breaking changes are allowed within 0.x.
| Bump | When |
|---|---|
MAJOR (X.0.0) |
(post-1.0) breaking API/schema/UX changes |
MINOR (x.Y.0) |
New features, MCP tools, UI surfaces, additive schema |
PATCH (x.y.Z) |
Bug fixes, perf, security, docs |
package.json version must match the tag you cut.
Use the fast quality gate while iterating and before pushing a release candidate:
pnpm ci:local:qualityThe required release gate is GitHub CI on the exact PR head. Its lint, typecheck, unit/integration, build, and Playwright jobs must finish successfully before merge. After any new commit, wait for CI to rerun against that new SHA; results from an earlier head do not qualify the release.
Do not routinely duplicate the complete CI matrix locally. Run the complete local gate when GitHub Actions is unavailable, when diagnosing a CI failure, or when a database, migration, browser, concurrency, or environment-sensitive change warrants local integration evidence:
pnpm ci:localThe TypeScript runner is cross-platform. It starts or verifies the guarded
local services, supplies only the known local development endpoints, generates
the Prisma client, and runs lint, typecheck, and Vitest once. It then serializes
Playwright behind a portable machine-local lock and selects an available port.
pnpm ci:local:e2e runs only the E2E phase; add -- --reuse-e2e-build only when
the existing production build is known to match the current source. These
local commands provide fast feedback or supplemental evidence; they never
replace exact-head CI when GitHub Actions is available.
A small, cohesive patch may include its version bump and curated changelog in
the implementation PR only when the operator authorizes that release and
assigns its version and release owner before implementation begins. The exact
PR head must pass CI; after squash-merge, tag the exact resulting main SHA and
follow the normal deploy and verification steps below.
Use a separate release issue/PR when release authority or the version was not preassigned, when multiple merged changes are being batched, or when release assembly needs independent review. Never add an uncoordinated version bump to an ordinary feature PR.
- Open a PR
<owner>/<issue>-* → main. CI (.github/workflows/ci.yml: lint · typecheck · unit · Playwright e2e) must be green. Squash-merge with a clean message. - Update
CHANGELOG.mdunder a heading## [YYYY-MM-DD] — vX.Y.Z · Short title— the bracket date drives the in-app "unseen" dot + ordering; the— vX.Y.Z · Titletail is the release name shown in What's New. Group items under### Added/Changed/Fixed/Removed. Bumppackage.jsonversionto match the tag. (Draft withpnpm changelog, then curate.) - Only the designated release owner changes the version, assembles final release notes, and merges the release commit. Feature PRs must not race independent version bumps or tags.
CI outage fallback: if GitHub Actions is unavailable, gate the release on
pnpm ci:localagainst the guarded local service stack and record the exact command, commit SHA, result, and reason for using the fallback in the release notes. A local result is not a substitute merely because CI is slow or queued.
git checkout main && git pull origin main
git tag vX.Y.Z && git push origin vX.Y.Z(Or trigger .github/workflows/release.yml — manual version bump + tag + GitHub Release —
once Actions billing is restored.)
Prod builds an exact tag/commit from a dedicated clean checkout, stamped with the commit. The deployment lock permits one release at a time:
flock /tmp/forge-production-deploy.lock
cd /home/bailey/deploy/forge-prod
git fetch origin main --tags
git checkout --detach vX.Y.Z
git status --porcelain # must be empty
cd ~/docker/forge
FORGE_SOURCE_PATH=/home/bailey/deploy/forge-prod \
GIT_SHA=$(git -C /home/bailey/deploy/forge-prod rev-parse --short HEAD) \
BUILD_TIME=$(date -u +%FT%TZ) docker compose build forge forge-worker
docker compose up -d # entrypoint runs `prisma migrate deploy`
# run any one-time data backfill explicitly (prod image has no tsx — copy a .cjs + `node`)The deployment Compose must preserve the checked-in
docker/docker-compose.production.example.yml network contract. In
particular, forge-worker needs the private data network plus a non-internal
egress network so managed-runtime probes and Runs dispatch use the same
reachable execution plane. It must not publish ports or join the public proxy
network. The web app may join the proxy network and must set
FORGE_DISABLE_IN_PROCESS_WORKER=1 when the sidecar is present.
forge.axiom-labs.dev loads · sign-in works · core flows (issue create, agent dispatch,
MCP health) · system.buildInfo reports the deployed SHA · docker compose logs forge
clean.
Update DEVLOG.md; mark issues done; note state in the project log if it changed.
Record the exact release, deployed SHA, and live verification in the issue's
Delivery card. “Merged,” “released,” and “verified live” are separate facts.
Same as a normal change — branch fix/* from main, PR, squash-merge, tag a PATCH,
deploy. (No back-merge needed; main is the only long-lived branch.)