Integrating npm, Tailwind and Frontend Builds into GitLab Pipelines
AI generated
CI/CD
.yml
GitLab · npm · Tailwind CSS · Hyva · Frontend Build
npm, Tailwind and Frontend Builds
Integrated into GitLab Pipelines

Hyva Themes brings Tailwind CSS v4 to Magento as a CSS first build. Anyone integrating this build into a GitLab pipeline needs to coordinate Node.js versions, npm caching, artifact handoff and the ordering against the PHP build cleanly. This article shows how that works in practice.

13 min read npm ci · Tailwind v4 · node_modules cache · artifact Magento 2.4 · Hyva · GitLab CI/CD · Node.js 20

1. Why the frontend build belongs in the pipeline

The most common mistake in Magento projects using Hyva Themes is a frontend build that runs on a developer's laptop and whose results get committed to the repository. That sounds pragmatic, but in practice it is a constant source of inconsistency: different Node.js versions, different npm versions, and different Tailwind configurations across developer machines produce CSS files that vary slightly depending on the environment. Committing the result means versioning artifacts instead of source code.

The alternative is a reproducible build inside the GitLab pipeline. There, the Node.js version is fixed, npm ci guarantees that exactly the versions from package-lock.json get installed, and the CSS build output becomes a deterministic artifact of the pipeline run. The result then no longer lives in the repository; instead it is passed between jobs as a pipeline artifact and deployed to the server at the end. That approach is cleaner, more reproducible and protects the repository from generated code.

2. Pinning and reproducing the Node.js version

The Node.js version is the most common source of configuration drift between a developer's environment and CI. In Hyva projects using Tailwind CSS v4, Node.js 20 (LTS) should be used. The version is pinned in an .nvmrc file in the repository, a single line file containing 20. GitLab CI then uses the node:20-alpine Docker image for the frontend build job. That way the version is identical both locally (via nvm) and in the pipeline.

Another common pitfall is npm itself. npm install and npm ci behave differently: npm install updates package-lock.json when needed, while npm ci fails if package.json and package-lock.json do not match. In the CI pipeline, only npm ci belongs, it is faster, deterministic, and immediately flags when a developer forgot to commit package-lock.json. This is not a convenience feature, it is a reproducibility guarantee.

# .gitlab-ci.yml: Frontend build job for Hyva / Tailwind CSS v4
build:frontend:
  stage: build
  image: node:20-alpine
  variables:
    # Cache npm modules across pipeline runs
    NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.cache/npm"
    THEME_PATH: "app/design/frontend/Mironsoft/default"
  cache:
    key:
      files:
        # Cache key changes when lock file changes
        - "${THEME_PATH}/web/tailwind/package-lock.json"
    paths:
      - "${THEME_PATH}/web/tailwind/node_modules/"
      - .cache/npm/
    policy: pull-push
  script:
    # Install exact versions from lock file (no updates)
    - npm ci
        --prefix "${THEME_PATH}/web/tailwind"
        --prefer-offline
    # Build Tailwind CSS v4 (CSS-first approach)
    - npm run build
        --prefix "${THEME_PATH}/web/tailwind"
  artifacts:
    name: "frontend-${CI_COMMIT_SHORT_SHA}"
    paths:
      # Only the compiled CSS, not node_modules
      - "${THEME_PATH}/web/css/"
    expire_in: 1 day
    when: on_success

3. Configuring npm caching correctly in GitLab CI

The biggest time sink in the frontend build job is downloading npm packages. With a properly configured GitLab cache for node_modules, this step disappears entirely when dependencies have not changed. The cache key must point to package-lock.json: when the lock file changes, a new cache is created. When it stays the same, the existing cache is reused and npm ci runs with --prefer-offline, which prioritizes the local cache directory.

It is important to distinguish between a GitLab cache and a GitLab artifact. node_modules belongs in the cache: it is large, gets reused across pipeline runs and does not need to be versioned. The compiled CSS belongs in the artifact: it is small, gets passed between jobs within a single pipeline run and must be exactly reproducible. Defining node_modules as an artifact means uploading and downloading hundreds of megabytes unnecessarily between jobs. Keeping the compiled CSS only in the cache risks missing artifacts in the next job.

4. Compiling Tailwind CSS v4 in the pipeline job

Tailwind CSS v4 uses a CSS first approach: instead of a tailwind.config.js, a theme.css file drives the configuration. In Hyva projects the build command is typically npm run build, which internally calls the Tailwind CLI with the configured input file. In the GitLab pipeline it must be ensured that the input file is referenced correctly and that the output path matches the expected artifact path.

A common problem: by default, Tailwind v4 scans all template files in the project for used CSS classes. In a CI environment the scan path matters: it needs to cover the .phtml files of the Hyva theme, but must not search the vendor/ folder, which can contain hundreds of megabytes of files. Restricting the Tailwind configuration in theme.css with @source directives to the relevant template directories cuts build time in half in many projects.

5. Handing off the build artifact cleanly

In a multi stage GitLab pipeline, the frontend build artifact needs to be passed from the build job to the deploy job. This happens through GitLab artifacts with artifacts.paths. The build job defines which paths get stored as an artifact, and the deploy job can access those artifacts via needs. Important: artifacts are automatically passed between jobs within the same pipeline, even when they run in different stages.

The critical decision is which files should be part of the artifact. For the frontend build these are: the compiled CSS file under web/css/styles.css and optional source maps for debugging. Not in the artifact: node_modules/, the build tool itself, temporary files. A lean artifact considerably speeds up upload and download between jobs and reduces GitLab artifact storage. A typical compiled Tailwind CSS file is 50 to 150 KB, the artifact should never grow beyond a few megabytes.

6. Static content deploy after the frontend build

In Magento with Hyva, Static Content Deploy (SCD) is a separate step that runs after the frontend build. SCD compiles Magento module specific assets, processes require-js bundles (minimal in Hyva) and copies theme assets into the pub/static/ folder. In the GitLab pipeline, SCD either runs as part of the build job (which requires Magento as a dependency) or on the target server as part of the deploy job.

The cleaner solution for Hyva projects: run SCD in the build job using the PHP image, after the frontend build has produced the CSS. The result, the complete pub/static/ folder, is passed on as an artifact and deployed to the server, without a single SCD command ever running on the server. That is faster, more reproducible and avoids the problem of the server being unable to serve static assets while SCD is running.

# Combined PHP + Frontend build job
build:magento:
  stage: build
  image: php:8.4-cli
  variables:
    COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.cache/composer"
    THEME_PATH: "app/design/frontend/Mironsoft/default"
  cache:
    key:
      files:
        - composer.lock
    paths:
      - .cache/composer/
    policy: pull-push
  before_script:
    # Install Node.js 20 into PHP image
    - curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
    - apt-get install -y nodejs
    # Install PHP extensions required by Magento
    - docker-php-ext-install pdo_mysql bcmath intl
  script:
    # PHP dependencies
    - composer install
        --no-dev --prefer-dist --no-interaction --optimize-autoloader
    # Frontend build (npm ci + Tailwind)
    - npm ci --prefix "${THEME_PATH}/web/tailwind" --prefer-offline
    - npm run build --prefix "${THEME_PATH}/web/tailwind"
    # Magento DI compile
    - php bin/magento setup:di:compile
    # Static content deploy for all locales
    - php bin/magento setup:static-content:deploy
        de_DE en_US
        -t Mironsoft/default --force
  artifacts:
    name: "magento-build-${CI_COMMIT_SHORT_SHA}"
    paths:
      - vendor/
      - generated/
      - pub/static/
    expire_in: 2 hours

7. Common errors with frontend builds in CI

The most common error is a missing or outdated package-lock.json. When npm ci fails in the pipeline, it is almost always because the lock file no longer matches the current state of package.json. The fix: run npm install locally, commit the updated lock file, and restart the pipeline. npm ci must never silently update the lock file, that is its design principle and it protects against unintended dependency updates in CI.

A second typical error is missing CSS classes in the compiled output. When Tailwind cannot find the template files, it only generates the base styles. The fix: set explicit @source paths in theme.css pointing to the template directories. In Hyva projects those are typically the .phtml files in the theme directory and the Alpine.js components. A third error: running the build outside the theme directory, so relative paths in the Tailwind configuration cannot be resolved.

8. Frontend build strategies compared

There are several ways to handle the frontend build in Magento projects. The choice has a considerable impact on reproducibility, pipeline speed and team convenience.

Strategy Reproducible Pipeline speed Recommendation
CSS built locally, committed to the repo No Fast (no build) Not recommended
Build on the server during deploy Conditional Slow (no cache) Not recommended
Build in GitLab CI, artifact Yes Fast with cache Recommended
Build in a separate job, then combined Yes Parallelizable Recommended for large projects
Docker image with pre built assets Yes Very fast For container deployments

9. Optimizing build time

If the frontend build job takes longer than two minutes, several optimization approaches are available. The most effective one is correct npm caching: when node_modules is loaded from the cache, downloading disappears entirely. The second approach is limiting the Tailwind scan scope: with explicit @source paths pointing to the relevant template directories, Tailwind only scans a few hundred files instead of the entire project.

A third lever is running the PHP and frontend build in parallel. When both run as separate jobs in the same stage, they can execute simultaneously on different runners. A combined build job that first installs PHP dependencies and then runs the frontend build is simpler to configure, but slower than two parallel jobs. For projects with long build times, splitting them up pays off: the deploy job then waits for both via needs: ["build:php", "build:frontend"].

10. Summary

The frontend build with npm and Tailwind CSS v4 belongs in the GitLab pipeline, not on a developer's laptop or the production server. The combination of a pinned Node.js version, npm ci for deterministic dependency installs, a GitLab cache for node_modules and a lean artifact for the compiled CSS is the reproducible standard for Hyva Magento projects. This structure guarantees that every deployment on every server ends up with the same CSS state, regardless of who triggered the build.

Integrating Static Content Deploy into the same build job brings the complete pub/static/ folder into the pipeline as an artifact, so the target server no longer needs to run a single build step. That makes deployments faster, reduces server load during deployment and eliminates an entire class of errors that only show up in production because a different Node.js version is installed there than on the developer's laptop.

Frontend Builds in GitLab CI: The Essentials at a Glance

Node.js version

.nvmrc plus the node:20-alpine Docker image in the pipeline, identical to the local development environment.

npm ci instead of npm install

Deterministic install from package-lock.json, fails immediately on mismatches, mandatory in CI.

Cache vs. artifact

node_modules goes into the cache, compiled CSS goes into the artifact, never the other way around.

Limit the Tailwind scan

Restrict @source paths in theme.css to the template directories, cuts build time in half on large projects.

11. FAQ: npm, Tailwind and Frontend Builds in GitLab

1Why npm ci instead of npm install in CI?
npm ci installs exactly from package-lock.json and fails on mismatches, reproducibility is mandatory in CI.
2How do I pin the Node.js version in GitLab CI?
image: node:20-alpine in the job plus .nvmrc in the repository containing "20", identical locally and in CI.
3What belongs in the cache, what belongs in the artifact?
node_modules goes into the cache. Compiled CSS and pub/static/ go into the artifact. Never the other way around.
4How do I speed up the Tailwind build in CI?
npm caching, restricting @source paths, and running the frontend build as a separate job parallel to the PHP build.
5Does SCD have to run on the server?
In Hyva: run SCD in the build job within the pipeline, pass pub/static/ as an artifact. No SCD needed on the target server.
6Why not commit compiled CSS to the Git repository?
It is an artifact, not source code. Different Node.js versions produce different CSS files, leading to merge conflicts and inconsistent states.
7Staging vs. production: different Tailwind configs?
In Hyva v4 with the CSS first approach, theme.css settings are environment independent. Use environment variables for build scripts if needed.
8What happens if package-lock.json is missing?
npm ci fails immediately, correct behavior. Fix: run npm install locally, commit the updated lock file.
9How long should frontend build artifacts be kept?
1 to 2 days for normal pipelines. Mark release artifacts for rollback explicitly with their own expire_in.
10Can the frontend build run parallel to the PHP build?
Yes, two separate jobs in the same stage. The deploy job waits for both via needs: ["build:php", "build:frontend"]. The fastest configuration.