Managing dozens of projects relying on GitLab CI/CD templates can be a challenge.
At Prisma Media, on front-end libraries and some Symfony apps, we had a mix of configurations that were inconsistent, hard to maintain, and often not optimized. This article explains how we centralized our GitLab CI/CD pipelines into reusable, modular templates, and how you can adopt a similar approach.
Why a dedicated CI templates repository
The problem: scattered CI configurations
Before reusable templates:
- Each project had its own
.gitlab-ci.yml
with a lot of duplication - Jobs were inconsistent across projects (
npm ci
vsnpm install
, different caching strategies) - Pipelines were often slow and hard to debug
- Documentation was minimal or missing
- Synchronizes updates were complicated to deploy
Our solution: modular templates
We created a dedicated repository (ci
) hosting modular GitLab CI templates. Each template:
- Handles a single task (install dependencies, build, run tests, publish)
- Is optimized (cache, artifacts, retries)
- Is fully documented with usage examples and configurable variables
- Uses semantic versioning (
v1
,v2
) so projects can upgrade at their own pace
Our templates are stage-agnostic, with no predefined needs
or stages
, so consumer projects keep full control over their pipeline flow.
Templates also support different working directory, depending on where NPM commands are executed.
💡 Tip: prefix template names (e.g.
.npm-install
) to avoid conflicts with jobs in the consuming project.
Our template catalog
Template | Purpose | Key features |
---|---|---|
npm-install |
Install dependencies | Cache optimization |
npm-build |
Build application | Custom command support |
npm-qa |
Run linter | Custom command support |
npm-unit-test |
Run unit tests | JUnit reports integration |
npm-pack |
Create .tgz package |
Artifact output |
npm-publish |
Publish to registry | Manual trigger |
create-tag |
Git tag creation | Version auto-detection |
Each template is independent and hosted in a dedicated directory with its own documentation.
Pipeline overview
Shared configuration
All templates extend a common.yaml
to centralize variables, cache, and default behaviors. This ensures consistency across templates and reduces duplication.
variables:
MAIN_BRANCH: main
NODE_IMAGE: node:22-alpine
WORKING_DIRECTORY: ./
.default:
variables:
FF_USE_FASTZIP: 'true' # Optimize cache/artifact compression
interruptible: true
artifacts:
expire_in: 3 days
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
.cache-npm:
cache:
key:
files:
- ${WORKING_DIRECTORY}/package-lock.json
paths:
- ${WORKING_DIRECTORY}/node_modules
policy: pull
💡 Note on branch rules:
All our templates use$MAIN_BRANCH
in rules instead of$CI_DEFAULT_BRANCH
.
This ensures the pipeline behaves consistently relative to the versioned branch (v1
,v2
, etc.) rather than the repository's default branch, which may change over time.
Selected CI templates
Here are a few representative templates; all templates are available in the GitHub repository.
npm-install
: install dependencies
.npm-install:
extends:
- .default
- .cache-npm
image: ${NODE_IMAGE}
cache:
policy: pull-push
script:
- cd ${WORKING_DIRECTORY}
- npm ci
rules:
- if: >
$CI_PIPELINE_SOURCE == 'merge_request_event'
|| $CI_COMMIT_REF_NAME == $MAIN_BRANCH
npm-build
: build application
.npm-build:
variables:
COMMAND: 'npm run build'
extends:
- .default
- .cache-npm
image: ${NODE_IMAGE}
script:
- cd ${WORKING_DIRECTORY}
- ${COMMAND}
rules:
- if: >
$CI_PIPELINE_SOURCE == 'merge_request_event'
|| $CI_COMMIT_REF_NAME == $MAIN_BRANCH
npm-qa
: run linter (Biome)
.npm-qa:
variables:
COMMAND: 'npm run test:qa'
extends:
- .default
- .cache-npm
image: ${NODE_IMAGE}
script:
- cd ${WORKING_DIRECTORY}
- ${COMMAND}
rules:
- if: >
$CI_PIPELINE_SOURCE == 'merge_request_event'
|| $CI_COMMIT_REF_NAME == $MAIN_BRANCH
npm-unit-test
: run unit tests (Jest)
.npm-unit-test:
variables:
COMMAND: 'npm run test:unit'
extends:
- .default
- .cache-npm
image: ${NODE_IMAGE}
script:
- cd ${WORKING_DIRECTORY}
- ${COMMAND}
artifacts:
paths:
- junit.xml
reports:
junit: junit.xml
rules:
- if: >
$CI_PIPELINE_SOURCE == 'merge_request_event'
|| $CI_COMMIT_REF_NAME == $MAIN_BRANCH
💡 The job improves unit test report in GitLab merge request with
jest-junit
package and GitLab unit test reports.
npm-pack
: generate local package
.npm-pack:
variables:
PACKAGES_DIR: 'npm-pack-packages'
extends:
- .default
- .cache-npm
image: ${NODE_IMAGE}
script:
- cd ${WORKING_DIRECTORY}
- mkdir $PACKAGES_DIR
- PACKAGE_TGZ=$(npm pack)
- mv "$PACKAGE_TGZ" $PACKAGES_DIR
artifacts:
paths:
- $WORKING_DIRECTORY/$PACKAGES_DIR
when: manual
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
💡 Use this template for local testing before publishing or integration validation. The local NPM package can be installed in your project for testing.
npm-publish
: publish to NPM registry
.npm-publish:
extends:
- .default
image: ${NODE_IMAGE}
script:
- npm publish
allow_failure: false
when: manual
rules:
- if: $CI_COMMIT_REF_NAME == $MAIN_BRANCH
💡 To publish to a private registry, update the
.npmrc
file with the registry URL and authentication token before running the publish command.⚠ As of September 2025, NPM enforces shorter token lifetimes for write operations and plans to retire legacy tokens. To future-proof your CI, consider migrating to Trusted Publishing (OIDC).
create-tag
: create Git tag
.create-tag:
extends:
- .default
image: node:22-alpine
before_script:
- apk add --no-cache git
- git config --global user.email "gitlab@gitlab.com"
- git config --global user.name "GitLab CI"
script:
- VERSION_NUMBER=$(npm pkg get version | xargs echo)
- git tag $VERSION_NUMBER
- git remote set-url origin "${CI_SERVER_PROTOCOL}://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/$CI_PROJECT_PATH.git"
- git push origin $VERSION_NUMBER
allow_failure: false
rules:
- if: $CI_COMMIT_REF_NAME == $MAIN_BRANCH
💡 Requires a Project Access Token in
GITLAB_TOKEN
variable.
Final consumer usage
In the consumer project, the .gitlab-ci.yml
becomes very simple. The project only imports the templates it needs and defines the dependencies between jobs itself. This keeps the pipeline clean, flexible, and easy to maintain.
include:
- { project: 'user/ci', file: 'templates.yaml', ref: 'v1' }
workflow:
rules:
- if: $CI_COMMIT_TAG # Disable tag pipeline
when: never
- when: always
variables:
WORKING_DIRECTORY: '.'
npm-install:
extends: .npm-install
npm-build:
extends: .npm-build
needs: [npm-install]
npm-qa:
extends: .npm-qa
needs: [npm-install]
npm-unit-test:
extends: .npm-unit-test
needs: [npm-install]
npm-pack:
extends: .npm-pack
needs: [npm-build]
npm-publish:
extends: .npm-publish
needs: [npm-build, npm-qa, npm-unit-test]
create-tag:
extends: .create-tag
needs: [npm-publish]
Versioning and evolution
We tag template releases (v1
, v2
, etc.) so teams can upgrade on their schedule. Each release includes a changelog and migration notes for breaking changes. We also use code owners for approval deployment to secure our templates.
Planned improvements:
- Consider GitLab CI Components in the future for even better reuse
- Extend templates for other ecosystems (Python, PHP) as needed
Results and lessons learned
For the projects that adopted the templates we observed:
- Significant reduction in per-repo CI maintenance effort
- Faster onboarding for new projects and engineers
- More consistent build and publish behavior across teams
Operational notes
- Template bugs can affect consumers widely, but fixing a template fixes it for all consumers at once
- Adoption takes time: provide migration guides and a short period of backward compatibility
Conclusion
Centralizing GitLab CI templates gives us modular, documented, and versioned building blocks for pipelines. It reduces duplicated effort, enforces sensible defaults, and lets teams focus on product work rather than CI boilerplate.
If you'd like to try the repository and examples, the code is available on GitHub:
Discover our reusable CI templates on GitHub
What's your current CI approach? Have you centralized templates, or do you prefer per-repo pipelines? I'd be interested to read your experience.
Resources
- GitLab CI/CD components
- Strengthening npm security: Important changes to authentication and token management
- Trusted publishing for npm packages
Top comments (0)