Migration - Upgrades

How to Use Version Control for Safe Migration

Contents show

How to Use Version control for Safe Migration

Introduction

Migrating a codebase, framework, or Infrastructure platform carries real risk: downtime, data loss, unexpected behavior, and long rollbacks. A robust Version control strategy turns that risk into manageable, observable change. With Git (or similar systems), you can plan, test, and execute upgrades step-by-step, maintain an Audit trail, and roll back quickly if needed. This guide shows how to leverage branches, tags, pull requests, continuous Integration (CI), and feature flags to perform a safe, reversible Migration with confidence.


Prerequisites / Before You Start

A migration succeeds or fails based on what you do before the first commit. Treat preparation as a first-class phase.

Versions, Scope, and Dependencies

  • Define the exact target versions: application version, runtime (e.g., Node.js, Python), framework (e.g., Django, Spring), database engine, OS image, IaC modules, and CI runners.
  • Capture dependency constraints:
    • Package managers: package.json/package-lock.json, requirements.txt, Gemfile.lock, go.mod, etc.
    • System dependencies: OpenSSL, glibc, JVM.
    • Tooling: Docker, Kubernetes (server and client), Helm charts, Terraform providers.
  • Confirm Compatibility matrix and read upstream release notes, deprecations, and migration guides.

Backups and Snapshots

  • Create restorable backups for:
    • Databases (logical and physical).
    • Object/binary stores (S3 buckets, artifacts).
    • Configuration stores (Consul, Vault, Secrets Manager).
    • Infrastructure state (Terraform state, CloudFormation stacks).
  • Validate restore procedures in a non-production environment. Practice restoring from the latest snapshot.

Repository Health Checks

  • Ensure the main branch is green: all tests pass in CI.
  • Confirm you can build immutable artifacts (containers, packages) deterministically.
  • Audit repository protections:
    • Require pull request reviews.
    • Require CI checks for merges.
    • Enable branch protection and signed commits if applicable.
  • Ensure Git LFS is configured if large files are involved.

Environment Readiness

  • Provision a staging environment closely mirroring production (ideally via Infrastructure as Code).
  • Prepare feature flag or toggle framework for incremental activation.
  • Ensure monitoring, logging, and tracing cover both current and target states (dashboards exist).
See also  How to Handle Character Encoding Issues During Migration

Stakeholder Alignment

  • Agree on a change freeze window.
  • Document rollback and communication plans.
  • Identify owners for application, database, infrastructure, and Security.
  • Draft change tickets per your organization’s change management process.

Step-by-Step Migration guide

Step 1 — Create a Dedicated Migration Branch

Create a long-lived branch for the migration. Keep changes isolated and reviewable.

git checkout -b chore/migration-

  • Use a naming convention like chore/migration-2025Q1 or feature/upgrade-spring-6.
  • Push early and often to the remote to share visibility.

git push -u origin chore/migration-2025Q1

Why this matters

A dedicated branch preserves a clean main branch, supports parallel hotfixes via cherry-picks, and maintains a clear audit trail.


Step 2 — Tag a Baseline and Freeze Scope

Create a baseline tag to identify the last known-good state. This tag is your rollback anchor.

git checkout main
git pull –ff-only
git tag -a vX.Y.Z-baseline -m “Baseline before migration to
git push –tags

  • Freeze scope by documenting exactly what will change. Use a PR description or a MIGRATION.md file.

Step 3 — Set Up CI Job Templates for the Migration

Duplicate and tune CI pipelines for the migration branch. Keep original pipelines intact.

Example GitHub Actions workflow:

yaml
name: Migration CI
on:
push:
branches: [ “chore/migration-2025Q1” ]
pull_request:
branches: [ “chore/migration-2025Q1” ]
jobs:
build-test:
runs-on: ubuntu-latest
steps:

  • uses: actions/checkout@v4

  • uses: actions/setup-node@v4
    with:
    node-version: ’20’

  • run: npm ci

  • run: npm run test:ci

  • run: npm run build

  • Pin tool versions to ensure reproducibility.

  • Add a job for database migrations in a sandbox environment.


Step 4 — Introduce Feature Flags for Risky Changes

Wrap behavior changes behind flags so you can turn them on/off without redeploying.

Example flag usage (TypeScript):

ts
import { flags } from “./flags”;

export function calculatePrice(input: Order) {
if (flags.get(“use-new-pricing-engine”)) {
return newPricingEngine(input);
}
return legacyPricing(input);
}

  • Default to off. Gradually enable in staging, then production.
  • Keep flags short-lived; schedule cleanup tasks post-migration.

Step 5 — Version Your Database schema Changes

Use a migration tool like Flyway or Liquibase so schema changes are tracked in version control.

Example Flyway SQL script V2025_01__rename_column.sql:

sql
BEGIN;
ALTER TABLE customer RENAME COLUMN surname TO last_name;
COMMIT;

Automate migrations in CI for staging:

bash
flyway -url=jdbc:postgresql://db/staging \
-user=${DB_USER} -password=${DB_PASSWORD} migrate

  • Write reversible migrations if possible, or prepare down scripts.
  • Coordinate with application changes: maintain Backward compatibility (read-old/write-both pattern) during rollout.

Step 6 — Incremental Commits, Small Pull Requests, and Thorough Reviews

Break the migration into small, cohesive PRs:

  • PR #1: Bump dependency versions only.
  • PR #2: Code updates due to deprecations.
  • PR #3: Config changes and environment variables.
  • PR #4: Database scripts and versioning.
  • PR #5: Feature flag wiring and toggles.

Use a PR template to enforce consistency:

md

Summary

Brief description of the change.

Risks

  • Impact on
  • Rollback strategy

Verification

  • Tests added
  • Manual checks
  • Monitoring updates

Tickets

  • JIRA-1234

  • Require at least one reviewer with domain expertise (database, infra, Security).

  • Enforce static analysis, linting, and SAST checks in CI.


Step 7 — Build and Tag Release Candidates

Build immutable artifacts for each RC. Use semantic versioning with pre-release identifiers.

git checkout chore/migration-2025Q1

bump version to 2.0.0-rc.1 in your version file

git commit -am “chore: bump to 2.0.0-rc.1”
git tag -a v2.0.0-rc.1 -m “RC1 for migration”
git push –tags

  • Publish artifacts to your registry (Docker, PyPI, Maven, npm). Attach checksums and SBOMs.
  • Keep a changelog with breaking changes highlighted.

Step 8 — Deploy to a Staging Environment and Run Exhaustive Tests

Deploy RC to staging using your CD pipeline. Validate:

  • Unit, Integration, E2E tests with new versions.
  • Data migration rehearsal on a recent anonymized snapshot.
  • Performance benchmarks with the new runtime or framework.
  • Security scans (dependency scanning, container scans).

Example smoke test script:

bash
set -euo pipefail
curl -fsS https://staging.example.com/healthz
curl -fsS -X POST https://staging.example.com/api/login -d ‘{“u”:”test”,”p”:”test”}’ \
-H ‘Content-Type: application/json’ | jq .

  • Compare metrics to baseline dashboards: latency, error rates, CPU/memory, DB locks.
See also  How to Update Datasource Drivers During Migration

Step 9 — Plan and Practice Rollback

A migration is safe if rollback is simple and fast.

  • Practice reverting to the baseline tag:

git checkout main
git revert -m 1 # or
git checkout tags/vX.Y.Z-baseline


Step 10 — Production Deployment strategy

Choose a low-risk rollout pattern:

  • Blue-Green Deployment: keep old and new stacks; switch traffic via load balancer.
  • Canary releases: route a small percentage of traffic to the new version, then ramp up.
  • Rolling deployments: replace pods/nodes gradually with health checks.

Example Helm values toggle for canary:

yaml
replicaCount: 10
canary:
enabled: true
weight: 10

  • Keep feature flags off initially. Monitor, then enable gradually.
  • Have a go/no-go checkpoint with stakeholders after initial canary.

Step 11 — Post-Deployment Monitoring and Toggle Activation

  • Monitor SLOs and SLIs: request success rate, P95 latency, error budgets, queue backlogs.
  • Review logs and distributed traces for new warnings or Slow queries.
  • Turn on feature flags incrementally:
    • 10% users → 50% → 100%, with soak times between steps.
  • Announce completion or rollback according to the comms plan.

Step 12 — Merge to Main and Create the Final Release Tag

Once stable in production:

git checkout main
git merge –no-ff chore/migration-2025Q1
git tag -a v2.0.0 -m “Stable release After migration
git push origin main –tags

  • Keep the migration branch a few days for hotfixes, then delete if not needed.
  • Update release notes and MIGRATION.md with final steps and known caveats.

Risks, Common Issues, and How to Avoid Them

Risk/Issue Impact How to Avoid
Large, monolithic PRs Hard to review; hidden defects Split into small PRs; enforce PR template and reviews
Incompatible dependency bump Runtime failures Read release notes; pin versions; test in staging; use canaries
Schema changes breaking old code Downtime or data inconsistency Backward-compatible migrations; feature flags; dual-write/read-old patterns
Missing backups or untested restores Irreversible data loss Take snapshots; rehearse restore; document RTO/RPO
CI pipeline variations “Works on my machine” Pin tool versions; use build containers; cache carefully
Secrets/config drift Broken deployments Store config in version control (sans secrets); use secret managers; environment parity
Rollback not planned Prolonged outages Practice rollback; keep baseline tags; write down scripts/owners
Merge conflicts with hotfixes Delays, mistakes Cherry-pick hotfixes into migration branch; rebase frequently
Insufficient monitoring Blind to regressions Define SLOs; create dashboards before rollout; alerting tuned
Long-lived feature flags Technical debt Add expiration dates; track flags; remove after stabilization

Tips:

  • Prefer merge commits for auditability during migrations; avoid rewriting history.
  • Use signed tags and signed commits for Compliance and traceability.
  • Keep a change log and release notes with breaking changes clearly marked.

Post-Migration Checklist

Technical Validation

  • Application builds reproducibly; artifacts are stored and signed.
  • All services pass health checks; readiness and liveness probes stable.
  • Database migrations fully applied; Data integrity checks pass.
  • Background jobs, queues, and cron tasks functioning.
  • Caches warm; hit ratios comparable or improved.

Operational Validation

  • Dashboards updated for new components/paths.
  • Alerts firing only when needed; thresholds adjusted.
  • Logs and traces observable in centralized tooling.
  • Feature flags enabled to 100% (or documented if partial).
  • Runbook updated with new commands, endpoints, and procedures.

Security & Compliance

  • Dependencies scanned; no critical vulnerabilities introduced.
  • SBOMs regenerated and archived with the release.
  • Secrets rotated if touched during the process.
  • Access reviews completed for new services or infra.
  • Change records linked to commits and tags for audit.
See also  How to Migrate ColdFusion Datasources to a New Database Server

Documentation and Knowledge Transfer

  • MIGRATION.md updated with final notes, gotchas, and roll-forward plans.
  • Team demo or brown-bag explaining new patterns, flags, and rollback steps.
  • Tickets closed with references to commits, tags, and dashboards.

Practical Examples and Patterns

Example: Rolling Back a Partial Migration

  • Disable feature flags:
    • use-new-pricing-engine: false
  • Roll back service only, keep DB:
    • Redeploy vX.Y.Z-baseline container
  • If schema is incompatible:
    • Run down migration (if available)
    • Otherwise switch traffic (blue-green) while restoring from backup

Example: Combining Application and Infrastructure Migrations

  • Use a dedicated IaC branch: infra/migration-terraform-1.9
  • Pin providers and modules in versions.tf
  • Apply to staging with targeted modules:
    • terraform plan -target=module.db -var-file=staging.tfvars
    • terraform apply -target=module.db -var-file=staging.tfvars
  • Tag infra states with release identifiers in your state backend metadata.

Example: Git Hooks to Block Unsafe Commits

Pre-commit hook to block direct commits to main:

bash

!/usr/bin/env bash

branch=”$(git rev-parse –abbrev-ref HEAD)”
if [ “$branch” = “main” ]; then
echo “Direct commits to main are blocked. Use a PR.”
exit 1
fi

Make executable:

chmod +x .git/hooks/pre-commit


Tooling Recommendations

  • Version control: Git with protected branches, signed commits and tags.
  • CI/CD: GitHub Actions, GitLab CI, CircleCI, Jenkins (pinned agents).
  • Feature flags: LaunchDarkly, Unleash, OpenFeature, simple config toggles.
  • DB migrations: Flyway, Liquibase, Alembic, Rails Active Record Migrations.
  • Observability: Prometheus, Grafana, OpenTelemetry, ELK/EFK.
  • IaC: Terraform, Pulumi, CloudFormation, Helm with charts versioned.

Naming and Versioning Conventions

  • Branches: chore/migration- or upgrade/
  • Tags:
    • Baseline: vX.Y.Z-baseline
    • RCs: v2.0.0-rc.N
    • Final: v2.0.0
  • Semantic Versioning (SemVer):
    • Major for breaking changes
    • Minor for compatible Features
    • Patch for backward-compatible fixes

Communication Templates

Change Announcement (Internal)

  • Summary: “Upgrading to framework X.Y; expected benefits: Performance +20%, security patches.”
  • Schedule: “Staging today 15:00 UTC, production canary tomorrow 08:00 UTC.”
  • Impact: “No downtime expected; feature flags will be toggled gradually.”
  • Rollback: “Baseline tag v1.8.7-baseline; restore within 15 minutes.”

Post-Deployment Update

  • Outcome: “Migration successful; all flags at 100%.”
  • Metrics: “Error rate steady at 0.02%, P95 latency improved by 12%.”
  • Next steps: “Remove deprecated flags next sprint.”

FAQ

What branching strategy works best for migrations?

Short answer: a dedicated migration branch merged via pull requests. Keep the main branch stable and protected. For larger programs, you can combine GitFlow-like release branches with trunk-based development practices, but the key is isolation, reviewability, and frequent integration with hotfixes.

How do I handle database changes without downtime?

Design migrations to be backward compatible. Use the “expand and contract” pattern: add new columns/tables first, write to both schemas if needed, switch reads to the new schema, then remove old columns in a later release. Tools like Flyway or Liquibase help version and automate these steps.

Should I use rebase or merge during a migration?

Prefer merge commits for traceability and auditability, especially in regulated environments. Rebasing rewrites history, which can obscure the sequence of events. If rebasing is part of your usual workflow, avoid rebasing shared branches and never rebase after tags have been published.

What is the safest way to roll back?

Tag a baseline before starting. Keep builds for that baseline available. For application code, redeploy the baseline tag. For databases, either apply down migrations or design the rollout so the old application version can still work with the new schema. Practice the rollback in staging and document the steps.

How do feature flags reduce Migration risk?

Feature flags let you deploy code in a dormant state and activate changes gradually. You can run canaries, compare metrics, and turn off a problematic feature instantly without redeploying. Just remember to remove flags once the change is fully validated to avoid long-term complexity.

About the author

Aaron Longnion

Aaron Longnion

Hey there! I'm Aaron Longnion — an Internet technologist, web software engineer, and ColdFusion expert with more than 24 years of experience. Over the years, I've had the privilege of working with some of the most exciting and fast-growing companies out there, including lynda.com, HomeAway, landsofamerica.com (CoStar Group), and Adobe.com.

I'm a full-stack developer at heart, but what really drives me is designing and building internet architectures that are highly scalable, cost-effective, and fault-tolerant — solutions built to handle rapid growth and stay ahead of the curve.