Skip to content

ci: adopt a staging branch as integration gate for all PRs with auto-release on main merge #44

Description

@martyy-code

Problem

Today, every PR — whether Dependabot or a developer contribution — is merged directly onto main. This creates friction:

  • Every merge triggers a CI run on main, even for WIP or draft contributions
  • Release cadence is decoupled from development — no consolidated view of what is about to ship
  • No integration step to catch cross-package conflicts before shipping
  • Developers must coordinate merges to avoid stepping on each other on main

Solution

Adopt a staging branch model for all contributions (flat):

  feat/xyz PR  ─┐
  fix/abc PR   ─┼──►  staging  ──►  main  (release)
  dep PRs      ─┘

All PRs (dev + Dependabot) target staging directly. main is never the target of a PR.

  • Staging is an accumulation and integration branch — never a working branch
  • Each dev opens one PR per feature/fix, targeting staging
  • Staging is merged into main when the team decides the train is ready

Changes required

.github/dependabot.yml:

target-branch: staging

.github/workflows/release-on-merge.yml — new file:

  • Trigger: push on main
  • Steps: checkout (full history), pnpm install, changesets/version + changesets/publish
  • OIDC/Trusted Publishing: permissions: { id-token: write } + NPM_CONFIG_PROVENANCE: true
  • Skip the merge commit itself (detect via github.event.head_commit.message)

Branch setup (manual, not in repo):

  • Create staging from main
  • Add branch protection on staging: PRs required, CI required, no force-push
  • Update branch protection on main: direct pushes disabled, only staging PR can merge
  • All existing contributors notified: PRs now target staging, not main

What stays the same

  • Major-version Dependabot PRs still require manual review before merge onto staging
  • group-by: dependency-name (from ci: deduplicate Dependabot PRs in monorepo via group-by #43) still applies
  • apps/web with private: true is never published
  • changesets/action v1.9.0 is still the production-safe version

Merge strategy on staging (Decision needed)

Option Behavior
Squash and merge One commit per PR lands on staging
Merge commit Full history preserved on staging

Recommendation: Squash and merge — keeps staging history clean and linear.

Hotfix path

If a hotfix must bypass the staging gate:

  1. Branch from main, fix, open PR to staging with label "hotfix"
  2. Fast-track CODEOWNERS review
  3. Merge staging -> main immediately
    Normal features always go through staging.

Acceptance Criteria

  • staging branch created from main
  • Branch protection on staging: PR required, CI required, no force-push
  • Branch protection on main updated: direct pushes disabled, PR from staging required
  • .github/dependabot.yml updated: target-branch: staging + group-by: dependency-name
  • .github/workflows/release-on-merge.yml created and functional
  • One full train cycle tested: PR -> staging -> main -> npm publish
  • CONTRIBUTING.md updated to reflect new branching model
  • OIDC / Trusted Publishing configured (drop NPM_TOKEN)
  • changesets/action pinned to v1.9.0 in the workflow
  • Merge strategy on staging decided (squash or merge commit)

Related

Risks

  • Medium: Process change for all contributors — requires communication and CONTRIBUTING.md update.
  • Low: Staging can drift from main if left unmerged too long. Mitigate with a scheduled sync or reminder.
  • Low: CI reliability on staging — if CI is flaky, the train stalls.

Metadata

Metadata

Assignees

Labels

area:ciWorkflows, dependabot, toolingdependenciesDependency updates (used by Dependabot)status:blockedBlocked by an external dependency (TS, upstream)

Type

Fields

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions