GitLab CI include: Reusable Pipeline Building Blocks Across Multiple Repositories
AI generated
CI/CD
.yml
GitLab · CI/CD · Pipeline Architecture
GitLab CI include: Reusable Pipeline Building Blocks
local, project, remote and template compared

Anyone maintaining a .gitlab-ci.yml in more than one repository knows the problem: build, test and deploy jobs get copied over and over, and slowly drift apart. The include keyword solves exactly this, merging pipeline definitions from other files, other projects, or even external URLs at pipeline compile time. This article walks through all four include variants in detail and shows how they combine into a central CI template library for an entire organization.

16 min read include: CI Templates DRY Principle Pipeline Architecture

1. Why include Is More Than Copy and Paste

In most teams a GitLab CI pipeline starts as a single .gitlab-ci.yml file that grows with the project. As soon as a second or third repository joins, the temptation is to simply copy the working pipeline and adjust the project names. That works short term, but it becomes a maintenance nightmare over time: a change to the Docker registry URL, a new cache key or an extra security scan then has to be applied individually across ten, twenty or a hundred repositories.

include solves this by letting GitLab CI merge job definitions from multiple sources into a single effective pipeline before it runs. The local .gitlab-ci.yml then becomes a thin entry point that references shared building blocks instead of containing every line itself. Four variants are available: local for files in the same repository, project for other repositories on the same GitLab instance, remote for arbitrary HTTPS URLs, and template for GitLab's own built-in templates.

2. include:local: Splitting One Large Pipeline Into Several Files

The simplest form is include:local. It references a file in the same repository and the same Git ref as the calling .gitlab-ci.yml. This is particularly useful once a single pipeline file has grown unwieldy, for example because it holds build, test, security and deploy stages for several microservices in a monorepo. Instead of an 800 line file, several topically separated files emerge, typically under a directory like .gitlab/ci/, each of which is easy to review on its own.

It is important to note that include:local is purely structural and offers no versioning across repository boundaries, because the file and the caller always come from the same commit. That makes it ideal for splitting up a single project but unsuitable when several independent repositories need to share the same logic. For team internal use, though, it is entirely sufficient to significantly improve readability and reviewability without giving up control over the pipeline.


# .gitlab-ci.yml
include:
  - local: '.gitlab/ci/build.yml'
  - local: '.gitlab/ci/test.yml'
  - local: '.gitlab/ci/deploy.yml'

stages:
  - build
  - test
  - deploy

3. include:project: Pulling Pipeline Building Blocks From Another Repository

include:project takes a decisive step further and lets you include a file from a completely different GitLab project, as long as the executing user or runner has read access to the source project. This is exactly the foundation for a central CI library: a dedicated repository, say devops/ci-templates, holds finished jobs for PHP builds, Composer caching, PHPUnit execution or deployment to a specific server type, and every application project includes only what it actually needs.

The big advantage over copying is centralized maintenance: when a job in the library is fixed or extended, for example with a new PHP 8.4 compatibility check, every including project benefits automatically once it updates the referenced ref, or immediately on the next pipeline run if no fixed ref is set. That does carry a risk, though: without deliberate versioning, a change to the library can unintentionally break all dependent pipelines at once, which is why versioning deserves its own section further below.


# .gitlab-ci.yml of an application project
include:
  - project: 'devops/ci-templates'
    ref: 'v2.4.0'
    file:
      - '/php/build.yml'
      - '/php/phpunit.yml'
      - '/deploy/ssh-deploy.yml'

4. include:remote: External URLs and Their Limits

include:remote loads a YAML file from any publicly reachable HTTPS URL. This is useful for pulling in templates from third parties, or for delivering pipeline building blocks through an internal content delivery system that does not live inside GitLab itself. In practice, though, it is the least suitable choice for an internal template library, because GitLab's own authentication does not apply: the URL must be reachable without a GitLab token, which effectively rules out internal, access restricted templates through this route.

Another difference from include:project is the lack of a clean tie to Git refs for versioning. A URL can be parameterized with a tag or commit hash, for instance raw.githubusercontent.com/org/repo/v1.2.0/template.yml, but GitLab itself never verifies that the referenced file belongs to a specific, checkable state. For public, well versioned templates like community security scanners this is acceptable, but for internal company CI building blocks with access restrictions, include:project remains the far more robust and secure choice.


# .gitlab-ci.yml
include:
  - remote: 'https://raw.githubusercontent.com/example-org/ci-lib/v1.3.0/php-lint.yml'

5. include:template: Using GitLab's Built-In Standard Templates

include:template pulls in one of the ready made templates that GitLab ships as part of the platform itself, for example for security scanning, Auto DevOps stages or language specific build patterns. These templates live inside GitLab's own gitlab-org/gitlab repository and are updated with every GitLab release without users having to maintain them themselves. They are the fastest way to bring established security or compliance capability into a pipeline without any implementation effort of your own.

The downside is reduced control: when GitLab updates a template, the behavior of your own pipeline can change without any action on your part, unless a fixed GitLab version reference is in place. In practice, many teams combine include:template for GitLab's own standard functions like SAST or dependency scanning with include:project for everything the team develops and versions itself. This combination plays to the strengths of both approaches without giving up either one's flexibility.


# .gitlab-ci.yml
include:
  - template: 'Security/SAST.gitlab-ci.yml'
  - project: 'devops/ci-templates'
    file: '/php/build.yml'

6. Building a Central CI Template Library

A mature template library is a software project in its own right and deserves the same care as application code. A clear directory structure per language or technology makes sense, for example /php, /node, /docker and /deploy, along with a README showing examples of how each building block is included. Every job in the library should be as generic as possible and configurable through variables like APP_NAME or DEPLOY_TARGET, rather than hard wiring project specific assumptions.

For the rollout, a step by step approach is recommended: first fully migrate a pilot project to the library, then let a second and third project follow, and only afterwards tackle migrating every remaining repository. This surfaces teething problems in the templates early, before a hundred projects would be affected simultaneously. A dedicated merge request template inside the library repository that reminds authors to notify all dependent projects of changes further prevents unpleasant surprises from breaking changes.

7. Versioning: Why ref Should Never Be main

The most common mistake when using include:project is omitting the ref parameter or hard coding it to main. That means every pipeline run automatically pulls the very latest state of the library, and a faulty change can instantly take down every dependent project at once. Instead, every application project should reference a fixed tag, say v2.4.0, and update it deliberately and after testing whenever a new version of the library becomes available.

Semantic versioning fits perfectly here: patch releases for bug fixes with no behavior change, minor releases for new, opt in jobs, and major releases for breaking changes such as renamed variables or altered job names. A CHANGELOG.md in the library repository that documents every version makes migration traceable for dependent teams. That way the central library stays a productivity win instead of becoming a source of unpredictable pipeline outages.

8. Parameterizing With spec:inputs and Combining It With rules

Since GitLab 15.11, templates can define typed input parameters at the top of a file through a spec:inputs block, which are passed in at include time. This replaces the previously common and error prone practice of setting variables somewhere else in the pipeline and hoping the included job happens to pick them up. With inputs it is instead explicitly visible which values a template expects, including default values and allowed options.

Combined with rules inside the included jobs, building blocks emerge that behave differently depending on the calling context, for example a deploy job that only actually deploys on the main branch and only performs a dry run on feature branches. This combination of parameterized templates and context dependent rules is exactly what distinguishes a real CI library from a mere collection of copied YAML fragments: it behaves like an actual, configurable software component.


# devops/ci-templates: /deploy/ssh-deploy.yml
spec:
  inputs:
    environment:
      default: 'staging'
      options: ['staging', 'production']
---
deploy:
  stage: deploy
  script:
    - echo "Deploying to $[[ inputs.environment ]]"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main" && $[[ inputs.environment ]] == "production"'
    - if: '$CI_COMMIT_BRANCH != "main"'
      when: manual

9. Best Practices, Pitfalls and a Comparison of All Four Variants

In practice a clear rule of thumb works well: include:template for anything GitLab offers as a standard feature, include:project for self developed, versioned team building blocks, include:local purely for splitting large files within one repository, and include:remote only for public, externally maintained templates without access restrictions. Mixing all four variants indiscriminately quickly leads to confusion about where a given change actually needs to be made.

A frequent pitfall is circular includes, where two library files reference each other, which GitLab rejects with a clear error message. Equally relevant is the maximum depth of 150 included files per pipeline, which can matter in very large monorepo setups. The table below summarizes the four variants and their key properties to help choose the right method for a given use case.

Variant Source Versioning Typical Use
include:local Same repository, same ref Automatic via commit Splitting large pipelines into files
include:project Another GitLab project Via ref (tag/branch/SHA) Central, team owned CI library
include:remote Any HTTPS URL Manual via URL path only Public, externally maintained templates
include:template GitLab standard templates Tied to GitLab version Security scans, Auto DevOps building blocks

Mironsoft

CI/CD pipelines, zero-downtime deployments and release automation

Deployments that run without downtime and without the nail-biting?

We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.

Pipeline Review

Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.

Zero-Downtime Deployment

Building symlink releases, health checks and rollback strategies for Magento stores.

CI/CD Automation

Connecting tests, security scans and deployments into one reliable pipeline.

10. Summary

GitLab CI include: Key Takeaways

Four variants

local, project, remote and template cover every use case from splitting files to external templates.

Central library

include:project with a fixed ref turns a dedicated repository into the single source of truth for CI jobs.

Versioning matters

A fixed tag instead of main prevents one library change from breaking every project at once.

Parameterization

spec:inputs turns templates into configurable building blocks instead of rigid copy paste snippets.

11. FAQ: GitLab CI include: Key Takeaways

1What is the difference between include:local and include:project?
include:local includes a file from the same repository and the same commit, while include:project references a file from a completely different GitLab project, with its own ref for versioning.
2Can I use several include variants at once in one pipeline?
Yes, include accepts a list where local, project, remote and template can be freely mixed. GitLab merges all referenced files into a single effective pipeline before it runs.
3Why should I reference a fixed tag instead of main with include:project?
Without a fixed ref, every pipeline automatically pulls the latest library state. A faulty commit in the library would then instantly affect every dependent project at once. A fixed tag decouples library changes from their adoption in individual projects.
4Does include:remote work with private, access restricted URLs?
No, include:remote does not support GitLab's own authentication. The referenced URL must be publicly reachable without a token, which makes this variant unsuitable for internal, access restricted templates.
5What happens with circular include references?
GitLab detects circular references, for example when file A includes file B and file B includes file A again, and rejects the pipeline with a clear error message before any job runs.
6How many files can I include at most?
GitLab allows up to 150 included files per pipeline and a maximum nesting depth of includes within includes. For the vast majority of setups this limit is not a relevant constraint in practice.
7What is spec:inputs and what is it for?
spec:inputs is a block at the top of an included file that defines typed input parameters with default values and allowed options. It makes explicit which values a template expects, instead of relying on implicitly set variables.
8Should every organization maintain its own CI template library?
Once you reach roughly three to five repositories with a similar pipeline structure, the effort usually pays off already. With only one or two projects, the maintenance overhead of a separate library repository often still outweighs the benefit.
9Can I combine GitLab's built-in templates with my own building blocks?
Yes, this is common practice. include:template is typically used for standard features like security scanning, while include:project is used for self developed, team specific jobs, both within the same pipeline.
10How do I migrate existing projects to a central library step by step?
The safest approach is a pilot project that is fully migrated and observed over several sprints, followed by a gradual migration of further projects. A CHANGELOG in the library repository and fixed version tags make the controlled transition easier for every team.