Multi-Environment Branching for Magento (dev/staging/prod)
AI generated
git
HEAD
Git · Branching · Multi-Environment · Magento 2
Multi-Environment Branching for Magento
Cleanly separating dev, staging, and prod

Teams that never cleanly separate branches from deployment environments eventually push untested code straight to production by accident. This article shows how Magento teams can map branches unambiguously to dev, staging, and prod, promote code through environments via merge and tag instead of rebase, keep configuration out of the shared history, and lock down branches and pipelines per environment.

13 min. read dev · staging · prod CI/CD · Branch Protection · Release Tags

1. Why Git branches should map directly to deployment environments

Once a Magento team runs more than one environment, dev for daily development, staging for acceptance, prod for the live store, the question becomes which code lands where. The most robust answer is a fixed, unambiguous mapping between branch and environment: a push to develop automatically deploys to dev, a merge into staging deploys to staging, and only main or a release tag on it deploys to prod. This unambiguity removes the most common source of error in multi-environment setups: manually guessing which commit is currently running where.

A well-known real-world reference for this pattern is Adobe Commerce Cloud (formerly Magento Commerce Cloud): it ships out of the box with the branches integration, staging, and production, and every push to one of these branches automatically triggers a deployment to the environment of the same name, each with its own database, search index, and environment variables. Additional branches spin up temporary integration environments on demand. Even without Adobe Commerce Cloud, this principle can be reproduced with self-hosted deploy scripts or GitHub Actions/GitLab CI, as long as the branch-to-environment mapping is documented and consistently followed by the team.

2. Promoting code instead of rewriting history: merge and tag progression

The core principle of multi-environment branching is this: code is promoted through environments, never rewritten. A commit that has been tested on dev must arrive bit-for-bit identical on staging and later on prod, otherwise you effectively test a different version than the one that goes live. That is achieved through fast-forward merges or merge commits from develop into staging into main, followed by a release tag on the final state. What you consistently avoid is cherry-picking individual commits between environment branches and rebasing commits that have already been promoted.

Cherry-picking creates a new commit hash for content that is functionally identical, so Git loses the relationship between the state on dev and the state on prod, and later merges produce unnecessary conflicts. Rebasing a branch that has already been merged into staging retroactively rewrites its history and invalidates commits that were already signed or already tested. The rule is simple: once a commit has reached an environment, it is immutable and is only ever passed along via merge, never reapplied via rebase or cherry-pick.


# Promote code through environments via merge, never via rebase or cherry-pick
$ git checkout develop
$ git pull origin develop

# Feature is done and tested on dev, promote to staging with a merge commit
$ git checkout staging
$ git merge --no-ff develop -m "Promote develop to staging"
$ git push origin staging

# CI runs smoke tests against staging; once approved, promote to production
$ git checkout main
$ git merge --no-ff staging -m "Promote staging to production"

# Tag the exact commit that goes live, never a re-created equivalent
$ git tag -a v2026.07.1 -m "Release 2026.07.1: checkout fix, catalog import"
$ git push origin main --follow-tags

3. Trunk-based vs. Git Flow for Magento agency teams

Trunk-based development relies on short-lived feature branches that get merged into main multiple times a day, combined with feature flags for unfinished functionality. For agency teams juggling several Magento stores in parallel and shipping frequent, small, isolated changes, this significantly reduces merge conflicts and keeps integration continuous. The environment branches develop, staging, and main still exist, but feature branches themselves live only hours to a few days.

Git Flow, with its dedicated release/* and hotfix/* branches, fits better for projects with longer stabilization phases, for example large Magento version upgrades or store migrations where release preparation spans days or weeks while the next version is already being developed in parallel. For most ongoing agency projects with weekly or daily deployments, a simplified variant is more practical: trunk-based development for features, extended with dedicated hotfix/* branches that branch directly off main and, once fixed, get merged both into main and back into develop, so no hotfix gets lost at the next regular release.

4. Environment-specific configuration: env.php must never be committed

app/etc/env.php holds database credentials, the crypt key, cache backend configuration, session storage settings, and MAGE_MODE. Every one of these settings is environment-specific: dev often runs MAGE_MODE=developer with a filesystem cache, prod runs MAGE_MODE=production with Redis or Valkey backends. If env.php were committed, either production credentials would end up in the repository, or the dev configuration would accidentally overwrite the prod settings on every deployment. Both are serious security and operational risks, as covered in more depth in the article on sensitive data in the repository.

The clean separation: env.php belongs in .gitignore consistently and is never committed, not even as a template with placeholders. app/etc/config.php, on the other hand, which only contains enabled modules and scalars without credentials, absolutely belongs in the repository, because it should be identical across environments. Rather than encoding environment differences through branches or committed files, they are resolved through environment variables injected by the deploy pipeline or cloud platform at deploy time, never through the commit history.

5. Generating app/etc/env.php from environment variables at deploy time

Instead of maintaining env.php manually per environment, a deploy hook regenerates the file on every deployment from environment variables stored as secrets in the CI/CD pipeline or directly on the cloud platform. Adobe Commerce Cloud does exactly this automatically via .magento.env.yaml and the build/deploy deploy hooks. On self-hosted setups, a simple shell or PHP script in the deploy process takes over the same job before bin/magento setup:upgrade runs.

It is important that this script is part of the pipeline configuration and can therefore live in the repository, because it does not contain any secrets itself, only references their names. The actual values, database password, Redis host, crypt key, come exclusively from the secret store of the CI/CD platform or the cloud environment. That keeps the history free of sensitive data while every environment still receives a correctly configured env.php without anyone maintaining it by hand.


#!/usr/bin/env bash
# deploy/generate-env-php.sh: build app/etc/env.php from CI/CD secrets,
# never commit the generated file itself
set -euo pipefail

php -r '
$config = [
    "db" => [
        "connection" => [
            "default" => [
                "host"     => getenv("DB_HOST"),
                "dbname"   => getenv("DB_NAME"),
                "username" => getenv("DB_USER"),
                "password" => getenv("DB_PASSWORD"),
            ],
        ],
    ],
    "crypt" => ["key" => getenv("MAGE_CRYPT_KEY")],
    "cache" => [
        "frontend" => [
            "default" => [
                "backend" => "Cm_Cache_Backend_Redis",
                "backend_options" => [
                    "server" => getenv("REDIS_HOST"),
                    "port"   => getenv("REDIS_PORT"),
                ],
            ],
        ],
    ],
    "session" => ["save" => "redis"],
    "MAGE_MODE" => getenv("MAGE_MODE") ?: "production",
];
file_put_contents("app/etc/env.php", "<?php\nreturn " . var_export($config, true) . ";\n");
'

echo "app/etc/env.php generated for environment: ${MAGE_MODE:-production}"

6. Release tags and rollback strategy

Every production release should be marked with an annotated Git tag, for example v2026.07.1, placed directly on the merge commit that landed on main. Annotated tags (git tag -a) additionally store the tagger, date, and a message, unlike lightweight tags that merely point to a commit. These tags serve as the anchor point for rollbacks: instead of reverting a broken commit after the fact or rewriting history, you simply redeploy the previous tag when something goes wrong.

This tag-based rollback strategy is considerably safer than a git revert under time pressure, because the previous state was already fully tested and no new, untested combination of reverts is introduced. The same logic applies to hotfixes: a hotfix branch branches off the current production tag, gets fixed, gets merged, gets tagged again (for example v2026.07.2), and is deployed exclusively through that new tag. That way it always stays traceable exactly which commit state was live at any given point in time, which is invaluable during incident analysis.


# Tag a production release right after promotion to main
$ git checkout main
$ git tag -a v2026.07.1 -m "Release 2026.07.1"
$ git push origin v2026.07.1

# List releases in chronological order for a quick rollback overview
$ git tag --sort=-creatordate | head -5
v2026.07.1
v2026.06.3
v2026.06.2

# Rollback: redeploy the previous known-good tag, do not revert on main
$ git checkout v2026.06.3
$ ./deploy/deploy.sh production

# Hotfix branching from the currently live tag
$ git checkout -b hotfix/checkout-500 v2026.07.1
# ... fix, commit, merge to main ...
$ git tag -a v2026.07.2 -m "Hotfix: checkout 500 error"
$ git push origin main --follow-tags

7. Branch protection: required pull request reviews and CI checks per environment

Without technical enforcement, any branch-to-environment convention is just an agreement that eventually gets broken by accident, usually through a direct push to main under time pressure. Branch protection rules in GitHub or GitLab prevent that structurally: for main and staging you can enforce a required pull request, a minimum number of reviewer approvals, required status checks (PHPStan, PHPCS, unit tests), and a ban on force pushes. For main, it is also worth applying these rules to administrators, so no exception slips through via role assignment.

The strictness of the rules should scale with how critical the environment is: develop can stay comparatively open to allow fast iteration, staging requires at least one review plus a green pipeline, and main additionally requires the branch to be up to date before merging (require branches to be up to date), to avoid silent merge conflicts. This graduated strictness mirrors exactly the risk a bad merge would cause in that particular environment.


# .github/branch-protection.yml (documentation-as-code, applied via API/Terraform)
branches:
  develop:
    required_pull_request_reviews:
      required_approving_review_count: 1
    required_status_checks:
      strict: false
      contexts: ["phpcs", "phpstan-level-5"]
    allow_force_pushes: false

  staging:
    required_pull_request_reviews:
      required_approving_review_count: 1
    required_status_checks:
      strict: true
      contexts: ["phpcs", "phpstan-level-5", "unit-tests", "integration-tests"]
    allow_force_pushes: false
    enforce_admins: false

  main:
    required_pull_request_reviews:
      required_approving_review_count: 2
      require_code_owner_reviews: true
    required_status_checks:
      strict: true
      contexts: ["phpcs", "phpstan-level-5", "unit-tests", "integration-tests", "smoke-tests"]
    allow_force_pushes: false
    enforce_admins: true
    restrictions:
      users: []
      teams: ["release-managers"]

8. CI/CD pipeline triggers per branch

The branch-to-environment mapping only becomes reliable once it is automated in the CI/CD pipeline instead of being deployed manually over SSH. A push to develop triggers a job that deploys to dev and runs the fastest but least strict checks there. A merge into staging additionally triggers integration tests and an automated smoke test against checkout. A new tag on main triggers the production deployment, often behind a manual approval gate before the job actually runs.

This trigger structure makes deployment behavior predictable for the whole team: nobody needs to remember which command runs against which environment, the pipeline decides purely from branch names and tag patterns. That drastically reduces human error and makes every deployment traceable in the CI/CD log, including commit hash, tag, and the person responsible, which saves critical time during rollbacks under pressure.


# .gitlab-ci.yml (excerpt): deploy jobs triggered by branch/tag rules
stages: [test, deploy]

deploy_dev:
  stage: deploy
  script: ./deploy/deploy.sh dev
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'

deploy_staging:
  stage: deploy
  script:
    - ./deploy/deploy.sh staging
    - ./deploy/smoke-test.sh https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "staging"'

deploy_production:
  stage: deploy
  script: ./deploy/deploy.sh production
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
      when: manual
  environment:
    name: production
    url: https://www.example.com

9. Anti-pattern vs. recommended pattern side by side

Most problems in multi-environment branching trace back to a handful of recurring anti-patterns that all share the same core: a shortcut around the intended promotion path, usually chosen under time pressure, that feels faster in the moment but erodes trust in the deployment process over time. The following table lines up the most common of these shortcuts against the more robust alternative.

Area Anti-pattern Recommended pattern Why it matters
Shipping a hotfix Cherry-picking the commit straight onto main Tag-based promotion via merge and a new tag Keeps commit hashes identical across all environments
Environment configuration Committing env.php with credentials env.php in .gitignore, generated from env vars at deploy time No credentials ever enter the Git history
Emergency fix under pressure Direct push to main without review Protected branch, pull request, and required checks Prevents unreviewed code from reaching production
Already promoted commit Rebasing it afterward onto staging/main Fast-forward or --no-ff merge, history stays stable Prevents invalid commit hashes after deployment
Production deployment Manual SSH deployment by feel Branch/tag-triggered CI/CD pipeline with an approval gate Traceable, repeatable deployment

Consistently moving these five patterns to the right column already eliminates most of the deployment risk in a multi-environment setup. The remaining work is usually just getting the team used to the new discipline, since branch protection and CI/CD triggers then enforce the rules technically instead of merely documenting them.

Mironsoft

Branching strategies, CI/CD pipelines, and deployment workflows for Magento teams

Ready to set up multi-environment branching properly?

We help Magento teams map dev, staging, and prod unambiguously to branches, introduce release tags and rollback processes, and set up branch protection and CI/CD pipelines tailored to each environment.

Branching workshop

Choose and introduce trunk-based, Git Flow, or a hybrid model suited to your team size

CI/CD pipeline setup

Set up branch- and tag-triggered deployments for dev, staging, and prod

Branch protection audit

Review existing repositories for safe merge and review rules

10. Summary

Multi-environment branching only works when branches map unambiguously to environments and code is promoted through those environments exclusively via merge and tag, never via cherry-pick or rebase of commits that have already been promoted. Adobe Commerce Cloud demonstrates a proven, directly transferable reference pattern with its integration, staging, and production branches. Trunk-based development with short-lived feature branches fits most agency teams better than classic Git Flow, provided hotfix branches are added on top.

Environment-specific configuration such as app/etc/env.php should never enter the Git history; it is generated from environment variables at deploy time instead. Release tags mark every production state and enable rollbacks through redeployment rather than risky reverts. Branch protection rules and branch- or tag-triggered CI/CD pipelines enforce all of these rules technically, instead of merely documenting them as a team convention that eventually gets broken under time pressure.

Multi-environment branching for Magento at a glance

Branch mapping

develop to dev, staging to staging, main to prod. Adobe Commerce Cloud as the reference pattern.

Promotion, not rewrite

Merge and tag instead of cherry-pick and rebase. Commit hashes stay identical across all environments.

Keep configuration separate

Never commit env.php, generate it from env vars at deploy time instead of templates in Git.

Protection & CI/CD

Required pull requests, required checks per environment, branch/tag-triggered deployments.

11. FAQ: Multi-Environment Branching for Magento

1How should I map Git branches to dev, staging, and prod?
Fixed one-to-one mapping: develop to dev, staging to staging, main or a release tag on it to prod. Prevents guessing which code state is currently active.
2How does branch mapping work on Adobe Commerce Cloud?
integration, staging, and production map directly to environments of the same name. Every push triggers an automatic deployment.
3Why avoid cherry-picking between environments?
Cherry-picking creates new commit hashes for identical content and breaks the relationship between the dev and prod state. Merge- and tag-based promotion preserves the hashes.
4Trunk-based or Git Flow for a Magento agency?
Trunk-based with short-lived feature branches fits ongoing projects with frequent deployments better. Git Flow suits long stabilization phases such as major version upgrades.
5Why must env.php never be committed?
It holds DB credentials, the crypt key, and environment-specific settings. Committing it either exposes credentials or overwrites the wrong environment's configuration.
6How do I generate env.php at deploy time?
A deploy script reads environment variables from the CI/CD secret store and rewrites the file. The script itself can go in the repository, since it holds no secrets.
7How does rollback via Git tags work?
Every release gets a tag. If something breaks, the previous, tested tag is redeployed instead of risking a revert under time pressure.
8Which branch protection rules make sense for main?
Required pull request with multiple approvals, required status checks, no force pushes, and an up-to-date branch before merge, ideally with no exception for administrators.
9What should CI/CD triggers per branch look like?
Push to develop deploys to dev, merge into staging triggers additional tests, a new tag on main triggers the production deployment with an approval gate.
10Merge-based vs. rebase-based promotion?
Merge keeps existing commit hashes and makes promotion traceable. Rebase retroactively rewrites hashes and should be avoided for commits already promoted.