Skip to content

Trunk-Based Development

Overview

We are transitioning from Git Flow to Trunk-Based Development (TBD) to prioritize deployment speed, continuous integration, and cleaner repository history. In this model, we eliminate long-lived branches (like develop and release/x.y) in favor of a single, always-deployable main branch. Deployments are managed via ArgoCD, and versioning is fully automated using Git Tags and Semantic Commit Messages.


1. GIT Repo Structure

Our repository relies on a simplified structure:

  • main: The primary, stable trunk. It replaces all legacy master and develop branches.
  • feature/xxx: Short-lived branches created from main. All work is done here and merged directly to main via Merge Request (MR). Branches are deleted immediately after merging.
  • hotfix/{version}: Created from a specific tag via the "Create Hotfix" button in the deploy pipeline. Each hotfix branch builds its own Docker image and provides manual deploy buttons for TEST, STAGE, and PROD environments. Changes should be merged back to main via a standard MR after deployment.

2. Core Development & Release Principles

  1. Direct Integration: All feature work is merged directly into main.
  2. Feature Flags: Incomplete or untested features are wrapped in Feature Flags. This decouples deployment from release, allowing us to safely merge code to main without exposing it to end-users. Read more about Feature Flags.
  3. Continuous Alignment: The main branch should be frequently pulled into active feature branches to resolve conflicts early.
  4. Semantic Commits: Developers must use semantic prefixes (e.g., feat:, fix:, chore:, remove:). The CI pipeline calculates the next version automatically based on these prefixes.
  5. Tag-Based Releases: Releases are no longer managed by branches. Instead, the pipeline generates a Git Tag (e.g., v1.2.0) to snapshot a release version.

3. Version Lifecycle

Versions adhere to Semantic Versioning.

  • Major: Increased automatically when a commit includes BREAKING CHANGE: or BREAKING-CHANGE: (in the subject line or commit body).
  • Minor: Increased automatically when a new feature (feat:) is merged.
  • Patch: Increased automatically for bug fixes (fix:), maintenance (chore:), or removals (remove:). Resets when Minor or Major is increased. Non-semantic commits default to a patch bump.
  • Build Number: Pipeline IID appended as a 4th component (X.Y.Z.N), used only in hotfix and MR pipeline versions.

4. Environments & Deployment (ArgoCD)

ArgoCD manages our target environments. The CI/CD pipeline triggers deployments by updating the ArgoCD manifest repository.

  • DRAFT (overlay dft, container ENV=DFT): Draft environment. Auto-deployed. Every successful merge to main triggers a sync.
  • DEV: Manually deployed. A "Deploy DEV" button on the Tag pipeline triggers an API pipeline that creates a GitLab Release and deploys to DEV.
  • TST, STG, PRD: Promoted via the Environment Promotion pipeline in the ArgoCD repository. Promotion copies image.tag values from a source to a target environment (DEV → TST → STG → PRD) without rebuilding.

5. Changelogs

Changelogs are generated on-the-fly and published exclusively as GitLab Releases. No CHANGELOG.md file is committed to the repository.

When a deploy pipeline creates a release, it extracts commit messages since the previous release and groups them into standard categories:

  • Added: New features (feat:).
  • Changed: Changes to existing functionality (chore:, BREAKING CHANGE:).
  • Removed: Deprecated or removed features (remove:).
  • Fixed: Bug fixes (fix:).

6. Auto-Tagging on Main

  • Developer merges a feature or fix to main.
  • The CI pipeline triggers automatically.
  • The script reads the semantic commits since the last tag and calculates the new version (Major, Minor, or Patch).
  • The pipeline creates an annotated Git Tag (e.g., v1.2.0) directly on the merge commit and pushes it — this triggers a separate Tag pipeline.
  • The main pipeline auto-deploys the new version to DRAFT via ArgoCD.
  • The Tag pipeline provides a manual Deploy DEV button for the next stage.
--- config: layout: elk --- graph LR A[Developer: feature/TASK-123] -->|Merge Request| B(main branch) B -->|Push| C{Main Pipeline ∙ DRAFT} C --> D[Get Version — semantic bump] D --> E[Build Docker Image] E --> F[Apply Version — create + push tag] F --> G[Deploy DFT via ArgoCD] F -->|Tag push triggers| H{Tag Pipeline ∙ TAG} H -->|Manual button| I[Deploy DEV] I -->|API trigger ref=main| J{Deploy Pipeline ∙ DEV} J --> K[Create GitLab Release] K --> L[Deploy DEV via ArgoCD] L -->|Manual button| M[Create Hotfix] classDef branch stroke:#03a9f4,stroke-width:2px; classDef auto stroke:#4caf50,stroke-width:2px; classDef manual stroke:#ff9800,stroke-width:2px; classDef tag stroke:#9c27b0,stroke-width:2px; class A,B branch; class D,E,F,G,K,L auto; class I,M manual; class H tag;

7. Use Cases & Workflows

Note

In the git log examples below, # ENV annotations indicate which environment currently runs that version. These are conceptual markers, not actual git tags.

Case A: Developing a Standard Feature

Scenario: A feature (feat:) is merged. The pipeline automatically calculates a Minor bump, tags the commit, and deploys to DRAFT.

* chore: release v1.1.0 (HEAD -> main, tag: v1.1.0, tag: synced-to-DFT)
* Merge branch 'feature/TASK-101'
|\  
| * feat: add user login (feature/TASK-101)
|/  
* chore: release v1.0.0 (tag: v1.0.0, tag: synced-to-PRD)

Case B: Developing a Bug Fix (Patch Bump)

Scenario: A bug fix (fix:) is merged into main. The pipeline automatically calculates a Patch bump, tags it, and deploys to DRAFT.

* chore: release v1.1.1 (HEAD -> main, tag: v1.1.1, tag: synced-to-DFT)
* Merge branch 'feature/bug-103'
|\  
| * fix: data validation (feature/bug-103)
|/  
* chore: release v1.1.0 (tag: v1.1.0, tag: synced-to-STG)

Case C: Manual Promotion to PRD

Scenario: The Team Lead verifies v1.1.1 in the DRAFT and STG environments. They run the Environment Promotion pipeline in the ArgoCD repository with SOURCE_ENV=STG, TARGET_ENV=PRD. ArgoCD syncs the production environment. Meanwhile, developers have merged a new feature, automatically creating v1.2.0 in DRAFT.

* chore: release v1.2.0 (HEAD -> main, tag: v1.2.0, tag: synced-to-DFT)
* Merge branch 'feature/search'
|\
| * feat: elastic integration
|/
* chore: release v1.1.1 (tag: v1.1.1, tag: synced-to-PRD, tag: synced-to-STG)
* Merge branch 'feature/bug-103'

Case D: Emergency Hotfix

Scenario: A critical issue is found in PRD running v1.1.1. From the v1.1.1 deploy pipeline, the Team Lead clicks Create Hotfix — a hotfix/1.1.1 branch is created from the tag. A developer pushes a fix commit to that branch; the hotfix branch pipeline builds a new image. The Team Lead clicks Apply Hotfix — tag v1.1.2 is created on the hotfix branch with an annotated message hotfix hotfix/1.1.1. The tag push triggers the hotfix tag pipeline, which creates the GitLab Release and exposes Deploy TEST/STAGE/PROD buttons. The Team Lead clicks Deploy PROD. The hotfix branch is kept until the fix is also merged back to main via a regular MR.

* chore: release v1.2.1 (HEAD -> main, tag: v1.2.1, tag: synced-to-PRD, tag: synced-to-DFT)
* Merge branch 'hotfix/crash-fix'
|\  
| * fix: critical crash (hotfix/crash-fix)
|/  
* chore: release v1.2.0 (tag: v1.2.0)

Case E: A BREAKING CHANGE (Major Version Bump)

Scenario: A developer works on a major overhaul. They use BREAKING CHANGE: as the commit prefix (or include it in the commit body). When merged, the pipeline calculates a Major bump (e.g., 1.2.12.0.0), tags the commit, and deploys to DRAFT.

* chore: release v2.0.0 (HEAD -> main, tag: v2.0.0, tag: synced-to-DFT)
* Merge branch 'feature/api-v2'
|\  
| * BREAKING CHANGE: completely redesign user payload (feature/api-v2)
|/  
* chore: release v1.2.1 (tag: v1.2.1, tag: synced-to-PRD)

Case F: Synchronizing main into a Feature Branch

Scenario: A developer is working on feature/long-task. While they are working, another developer merges a quick fix into main, which generates v1.2.2. The first developer merges main into their feature branch to resolve conflicts. When they finally merge their feature back to main (squash merge), the pipeline calculates a Minor bump, tags v1.3.0, and deploys to DRAFT.

* chore: release v1.3.0 (HEAD -> main, tag: v1.3.0, tag: synced-to-DFT)
* Merge branch 'feature/long-task'
|\  
| * feat: finish long task (feature/long-task)
| * Merge branch 'main' into feature/long-task
| |\  
| |/  
|/|   
* | chore: release v1.2.2 (tag: v1.2.2)
* | fix: quick fix from another dev
| * wip: start long task
|/  
* chore: release v1.2.1 (tag: v1.2.1, tag: synced-to-PRD)

Case G: Non-Semantic Commit Rejected (Server-Side)

Scenario: A developer attempts to push a commit with a message like "fixed the login button" instead of the required "fix: resolve login button issue". Action: The Gitaly server-side pre-receive hook validates the commit message against the Conventional Commits format. Result: The push is rejected outright. The main branch is completely unaffected, no version calculation occurs, and no deployments are triggered. The developer must amend their local commit message (git commit --amend) and push again.

GitLab ServerGitLab ServerDeveloperGitLab ServerCI.CD PipelineDeveloperDeveloperGitLab ServerGitLab ServerCI/CD PipelineCI/CD PipelineGitLab ServerGitLab Servergit push (commit: "fixed the login button")Pre-receive hook validation❌ REJECTED (GL-HOOK-ERR)Pipeline never triggers.Version remains unchanged.git commit --amend -m "fix: resolve login button"git pushPre-receive hook validation✅ ACCEPTED (Triggers Pipeline)
GitLab ServerGitLab ServerDeveloperGitLab ServerCI.CD PipelineDeveloperDeveloperGitLab ServerGitLab ServerCI/CD PipelineCI/CD PipelineGitLab ServerGitLab Servergit push (commit: "fixed the login button")Pre-receive hook validation❌ REJECTED (GL-HOOK-ERR)Pipeline never triggers.Version remains unchanged.git commit --amend -m "fix: resolve login button"git pushPre-receive hook validation✅ ACCEPTED (Triggers Pipeline)

Case H: Promoting ArgoCD Manifests (DEV to TST)

Scenario: Version v1.2.1 has been deployed to DEV. QA has finished testing. The Team Lead runs the Environment Promotion pipeline in the ArgoCD repository with SOURCE_ENV=DEV, TARGET_ENV=TST.

Action: The promotion script reads image.tag from each service's DEV overlay and writes it to the TST overlay. A Git tag and GitLab Release with changelog are created (only on DEV → TST promotion).

Result: No new code is built, and no new version bumps occur in the application repository. ArgoCD detects the manifest change and syncs the TST environment.

* chore: release v1.2.1 (HEAD -> main, tag: v1.2.1, tag: synced-to-DEV, tag: synced-to-TST)
* Merge branch 'hotfix/login'
|\  
| * fix: resolve login button (feature/fix-login)
|/  
* chore: release v1.2.0 (tag: v1.2.0, tag: synced-to-PROD)

Note

The application repository's history doesn't change during promotion. Only the ArgoCD manifest repository is updated (image.tag values in the target environment overlay).