Navigating the Transition from Master to Main in GitLab CI/CD Configurations

The landscape of version control has undergone a significant shift in naming conventions, moving from the legacy master branch to the more inclusive main branch. While this change may seem like a simple string replacement, it introduces substantial operational risks within the GitLab CI/CD ecosystem. When a project's default branch name changes, any pipeline configuration that explicitly references the branch name—specifically within the only or rules keywords—becomes a point of failure. If a .gitlab-ci.yml file is configured to trigger only on master, but the repository has been migrated to main, the pipeline will simply fail to trigger upon a commit. This silent failure is catastrophic in production environments where continuous deployment depends on the successful execution of these pipelines.

The fundamental issue arises from the hard-coding of branch names. In older GitLab repositories, master was the standard. In newer repositories, main is the default. This dichotomy creates a situation where templates, samples, or copy-pasted configurations from different repositories may not align with the actual default branch of the current project. For instance, a developer copying a configuration that uses only: - master into a new project using main will find that their pipelines never start, leading to hours of fruitless troubleshooting.

To resolve this, GitLab provides predefined variables and advanced logic structures. The transition from the legacy only/except syntax to the modern rules syntax allows for more dynamic evaluations. By leveraging variables like $CI_DEFAULT_BRANCH and $CI_COMMIT_REF_NAME, engineers can create branch-agnostic pipelines that function regardless of whether the primary branch is named main, master, or any other custom identifier.

The Critical Impact of Default Branch Naming

The discrepancy between main and master is not merely semantic; it is a functional barrier in the CI/CD pipeline. In GitLab, the .gitlab-ci.yml file acts as the orchestrator for all automation. If the logic within this file specifies a branch that does not exist or is not the current target of a push, the GitLab runner will not initiate the job.

Consider a scenario where a repository is migrated from master to main. If the configuration remains:

yaml only: - master

The pipeline will not trigger on pushes to the main branch. This results in a complete cessation of automated testing and deployment. The impact is felt most acutely when using GitLab SaaS or self-managed instances where multiple projects vary in age. Older projects typically retain master, while newer ones default to main. This inconsistency makes the sharing of CI templates dangerous, as a template designed for one project may break the pipeline of another.

To mitigate this, the use of the $CI_DEFAULT_BRANCH variable is recommended. This variable dynamically resolves to the name of the project's default branch as defined in the repository settings. This ensures that the pipeline remains functional across different repositories regardless of their age or naming convention.

Syntactic Evolution: From Only/Except to Rules

GitLab has evolved its pipeline control mechanisms, moving away from the restrictive only/except keywords toward the more powerful and flexible rules keyword.

The only keyword is a legacy approach. While simple, it lacks the granularity required for complex workflows. For example, specifying multiple branches:

yaml only: - master - uat - test - dev

This configuration ensures the job runs on any of the listed branches. However, it is static. If the organization decides to rename master to main, every single .gitlab-ci.yml file across all projects must be manually updated.

The rules keyword provides a logical framework using if-statements. This allows for a more programmatic approach to pipeline triggering. The equivalent of the above using rules would be:

yaml rules: - if: '$CI_COMMIT_BRANCH == "master"' when: on_success - if: '$CI_COMMIT_BRANCH == "uat"' when: on_success - if: '$CI_COMMIT_BRANCH == "test"' when: on_success - if: '$CI_COMMIT_BRANCH == "dev"' when: on_success - when: never

The impact of using rules is a significant increase in control. It allows developers to specify not just which branch a job runs on, but under what conditions (e.g., on success of a previous stage) and provides a clear fallback using when: never to ensure jobs do not run unexpectedly.

Predefined Variables for Branch Agnosticism

To avoid the "main vs master" trap, GitLab provides a suite of predefined variables. These variables allow the configuration to adapt to the environment.

The $CI_DEFAULT_BRANCH variable is particularly crucial. It is available from version 12.4 onwards. Instead of hard-coding a branch name, using this variable allows the pipeline to target the default branch regardless of its name.

However, there is a critical technical limitation: $CI_DEFAULT_BRANCH does not work within the only keyword. Attempts to use it there will result in pipelines that do not trigger.

```yaml

INCORRECT USAGE

only:
- $CIDEFAULTBRANCH
```

The correct implementation requires the rules keyword:

```yaml

CORRECT USAGE

rules:
- if: $CICOMMITBRANCH == $CIDEFAULTBRANCH
```

This shift in syntax is vital for maintainability. By using variables, the same .gitlab-ci.yml file can be deployed across hundreds of projects without modification, whether those projects use main, master, or develop as their primary branch.

Managing Pipeline Triggers and Sources

Understanding the source of a pipeline is essential for preventing duplicate executions and ensuring jobs run in the correct context. GitLab uses the CI_PIPELINE_SOURCE variable to categorize how a pipeline was started.

The following table details the relationship between pipeline sources and their corresponding variables:

Variable Branch Tag Merge Request Scheduled
CICOMMITBRANCH Yes Yes No No
CICOMMITTAG Yes Yes (if configured) No No
CIPIPELINESOURCE = push Yes Yes No No
CIPIPELINESOURCE = schedule Yes No No Yes
CIPIPELINESOURCE = mergerequestevent Yes No Yes No
CIMERGEREQUEST_IID No No Yes No

The CI_PIPELINE_SOURCE variable can take several values, including:

  • api: Triggered via the pipelines API.
  • chat: Created through GitLab ChatOps.
  • external: Triggered by non-GitLab CI services.
  • externalpullrequest_event: Triggered by GitHub pull requests.
  • mergerequestevent: Triggered when a merge request is created or updated.

By utilizing these sources, developers can create highly specific rules. For example, a job that should only run for merge request pipelines and scheduled pipelines, but never for standard push pipelines, would be configured as follows:

yaml job1: script: - echo "Running specific pipeline source" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_PIPELINE_SOURCE == "schedule" - if: $CI_PIPELINE_SOURCE == "push" when: never

Avoiding the Double Pipeline Trap

A common failure in GitLab CI configuration is the creation of duplicate pipelines. This occurs when a job is configured in a way that satisfies both a branch pipeline and a merge request pipeline simultaneously.

If a job has no rules defined, it defaults to except: merge_requests. If another job is specifically defined for merge requests using rules, GitLab may trigger both. For instance:

```yaml
job-with-no-rules:
script: echo "This job runs in branch pipelines."

job-with-rules:
script: echo "This job runs in merge request pipelines."
rules:
- if: $CIPIPELINESOURCE == "mergerequestevent"
```

In this scenario, if a commit is pushed to a branch that has an open merge request, two pipelines will run: one branch pipeline containing job-with-no-rules and one merge request pipeline containing job-with-rules.

To prevent this, GitLab recommends the use of workflow: rules. The workflow block defines the top-level conditions for the entire pipeline, ensuring that only one type of pipeline is triggered.

Example of a safe workflow configuration:

yaml workflow: rules: - if: $CI_MERGE_REQUEST_ID when: never - if: $CI_COMMIT_TAG when: never - when: always

This configuration explicitly prevents merge request and tag pipelines from triggering if the general conditions are met, thereby eliminating duplication.

Securing CI/CD Configurations and Branch Permissions

In business-critical environments, it is often necessary to restrict who can modify the pipeline logic. A common requirement is to allow users to create branches and merge requests for validation, but prevent them from altering the .gitlab-ci.yml file to bypass security checks or grant themselves unauthorized permissions (such as running an "apply" stage in a production environment).

The challenge is that if the .gitlab-ci.yml file resides within the project repository, any user with branch access can modify the file in their own branch. They could potentially change the rules to allow a restricted job to run on their branch, bypassing the intended restrictions.

To solve this, the following architectural strategies are recommended:

  • Pipeline Execution Policies: By defining policies that live outside the project, administrators can ensure that specific jobs always run or that access to the CI/CD configuration is limited.
  • External Configuration: Moving the CI/CD configuration to a separate, protected project.
  • Narrow Group Permissions: Utilizing GitLab's protected branch and tag features to ensure that only a small group of authorized individuals can merge changes into the main branch.

For a pipeline that requires a validation stage on any branch but an apply stage only on the main branch via a merge request, the rules should be structured as follows:

```yaml
validation-job:
stage: validate
script:
- echo "Validating code..."
rules:
- when: always

apply-job:
stage: apply
script:
- echo "Applying changes to production..."
rules:
- if: $CICOMMITREFNAME == $CIDEFAULTBRANCH
when: on
success
- when: never
```

Branch-Specific Configurations and Pitfalls

A critical misunderstanding among some users is the assumption that a .gitlab-ci.yml file in the default branch applies to the entire project across all branches. In reality, GitLab looks for the .gitlab-ci.yml file in the specific branch being processed.

If a user has different configurations on different branches, they must ensure that the file exists and is correctly configured on each branch. A common error occurs when a user assumes the master version of the config will govern a feature branch; if the feature branch lacks a .gitlab-ci.yml file, or contains a different one, the pipeline behavior will diverge from the master branch.

To avoid this, the recommended approach is to make the .gitlab-ci.yml file branch-agnostic. This means avoiding hard-coded branch names and using variables like $CI_COMMIT_BRANCH or $CI_DEFAULT_BRANCH so that the same configuration file can be merged across all branches without causing the pipeline to go silent.

Summary of Best Practices for Branch Management

To ensure maximum reliability and maintainability in GitLab CI/CD, the following standards should be applied:

  • Transition all only and except blocks to rules to leverage complex logic and predefined variables.
  • Replace hard-coded strings like main or master with $CI_DEFAULT_BRANCH to support projects of all ages.
  • Implement workflow: rules to prevent the creation of double pipelines when both push and merge request events are present.
  • Use CI_PIPELINE_SOURCE to precisely control job execution based on the trigger mechanism (API, Schedule, Push, etc.).
  • Avoid mixing only/except and rules within the same pipeline to prevent troubleshooting nightmares and unpredictable default behaviors.
  • Utilize !reference tags to reuse common rule sets across multiple jobs, reducing duplication and the risk of configuration drift.

Conclusion

The transition from master to main in GitLab is a catalyst for a broader shift toward more robust, variable-driven CI/CD configurations. The reliance on hard-coded branch names is a technical debt that leads to silent pipeline failures and operational instability. By adopting the rules keyword and integrating predefined variables such as $CI_DEFAULT_BRANCH, organizations can create a seamless automation layer that is agnostic to branch naming conventions.

The complexity of modern pipelines—incorporating merge request events, scheduled tasks, and API triggers—demands a move away from the legacy only syntax. The implementation of workflow rules and the strategic use of pipeline source variables are the only ways to truly eliminate duplicate pipelines and secure the deployment process. Ultimately, the goal is to move the CI/CD configuration from a static file to a dynamic engine capable of adapting to the repository's state, ensuring that critical business processes, such as production applies, are executed only under the exact conditions required by the organization's governance policies.

Sources

  1. Beware: in your CI/CD configuration that master is now main
  2. CI/CD: Add only: default-branch
  3. Restrict access to change gitlab-ci.yml only for main branch
  4. CICD pipeline only runs for the master branch and never for other branches
  5. GitLab Job Rules Documentation

Related Posts