How to Pass Build Metadata Between GitLab Jobs with dotenv Artifacts

Pass computed values like version tags, package paths, and review URLs between GitLab CI/CD jobs with dotenv report artifacts. Learn when to use them, how `needs` and `dependencies` affect inheritance, and where the security limits really are.

By Network Nuts · Published 2026-10-15

A common GitLab CI/CD problem appears right after your first useful pipeline: one job computes something valuable, but later jobs need the same value without recomputing it. Typical examples include a generated version string, a package filename, a review URL, or a build number derived from the branch and pipeline ID.

GitLab documents dotenv report artifacts for exactly this handoff pattern. A job writes variables to a .env-style file, saves that file with artifacts:reports:dotenv, and later jobs receive those values as CI/CD variables through the documented dotenv variables workflow. GitLab also makes two operating rules clear: dotenv variables are for later job execution, and they are not available during rules evaluation.

This guide is for GitLab users who already have a working runner and want a clean, official way to move small computed values through a pipeline without hardcoding them. The examples below are lab examples only. Review them before using them in a shared or production pipeline.

When dotenv artifacts are the right tool

Use a dotenv artifact when the producer job creates small key-value metadata that later jobs should consume as environment variables. GitLab's dotenv variables guide shows this pattern for passing computed values between stages, setting dynamic environment URLs, and sharing values with downstream pipelines.

Good candidates include:

  • generated version tags
  • artifact filenames
  • deployment URLs
  • image tags
  • package coordinates

Do not use this pattern for secrets. GitLab explicitly warns not to place credentials, API keys, or tokens in dotenv files because pipeline users can access the contents. If a value is sensitive, keep it in masked or protected CI/CD variables or another approved secret workflow instead, not in a dotenv artifact. See the dotenv variables guide and job artifacts documentation.

Prerequisites

Before using this pattern, make sure you have:

  • a project with GitLab CI/CD enabled
  • a runner that can execute ordinary shell commands
  • permission to edit .gitlab-ci.yml
  • a producer job that can write a plain-text .env file

Also follow GitLab's documented dotenv file rules from the dotenv variables guide:

  • one NAME=value pair per line
  • UTF-8 encoding only
  • no empty lines
  • no comments
  • no multiline values
  • no special-character escaping expectations

GitLab's docs currently show a 5 KB maximum dotenv file size in the dotenv requirements, and GitLab Self-Managed administrators can change that limit or set it to 0 to disable it under CI/CD limits. If your data is JSON, multiline text, or anything more complex than a few scalar values, use a regular file artifact instead of dotenv.

A minimal working pipeline

The following example computes three values once and reuses them later:

  • VERSION_TAG
  • PACKAGE_FILE
  • REVIEW_URL
stages:
  - prepare
  - test
  - package
  - deploy

prepare_meta:
  stage: prepare
  script:
    - mkdir -p dist
    - VERSION_TAG="${CI_COMMIT_REF_SLUG}-${CI_PIPELINE_IID}"
    - PACKAGE_FILE="dist/${CI_PROJECT_NAME}-${CI_COMMIT_SHORT_SHA}.txt"
    - REVIEW_URL="https://${CI_PROJECT_PATH_SLUG}-${CI_PIPELINE_IID}.example.test"
    - printf 'VERSION_TAG=%s\n' "$VERSION_TAG" > build.env
    - printf 'PACKAGE_FILE=%s\n' "$PACKAGE_FILE" >> build.env
    - printf 'REVIEW_URL=%s\n' "$REVIEW_URL" >> build.env
    - printf 'demo artifact for %s\n' "$VERSION_TAG" > "$PACKAGE_FILE"
  artifacts:
    access: developer
    expire_in: 1 day
    paths:
      - dist/
    reports:
      dotenv: build.env

unit_test:
  stage: test
  needs:
    - job: prepare_meta
      artifacts: true
  script:
    - test -n "$VERSION_TAG"
    - test -f "$PACKAGE_FILE"
    - echo "Testing package built for $VERSION_TAG"

package_info:
  stage: package
  needs:
    - job: prepare_meta
      artifacts: true
  script:
    - echo "Package file is $PACKAGE_FILE"
    - ls -l dist/

deploy_review:
  stage: deploy
  needs:
    - job: prepare_meta
      artifacts: true
    - job: unit_test
      artifacts: false
  script:
    - echo "Deploying $VERSION_TAG"
    - echo "Review URL: $REVIEW_URL"

The key line is reports: dotenv: build.env. GitLab's artifacts reports reference documents the dotenv report type, while the dotenv variables guide explains how later jobs receive those values. The producer job also stores dist/ as a normal artifact so later jobs can verify the file path they received.

How this pipeline behaves

In prepare_meta, the job calculates the values once, writes them to build.env, and uploads that file as a dotenv report artifact.

By default, GitLab says a job receives dotenv variables from all jobs in earlier stages. But once a job uses needs, dotenv inheritance narrows to the jobs listed in needs. That is why every consumer in this example explicitly names prepare_meta.

The deploy_review job shows another useful refinement: unit_test is needed for execution order, but its artifacts are not needed, so artifacts: false avoids an unnecessary artifact download. That behavior is documented in the needs reference and the broader CI/CD YAML syntax reference.

Expected lab result:

  • unit_test sees a populated VERSION_TAG
  • unit_test finds the file referenced by PACKAGE_FILE
  • package_info lists the generated file under dist/
  • deploy_review prints the same version tag and review URL created in prepare_meta

If test -n "$VERSION_TAG" fails, the usual cause is that the consumer job is not actually inheriting the producer's dotenv artifact.

Four mistakes that cause most dotenv pipeline failures

1. Expecting dotenv variables to work in rules

This is the most common mistake. GitLab is explicit in the dotenv variables guide: dotenv variables can be used in job scripts and in runner-side expansion contexts, but not in rules. If you need to decide whether a job exists, base that decision on predefined variables or other pipeline-creation-time inputs instead.

2. Forgetting that needs changes inheritance

Without needs, later jobs can receive dotenv values from earlier stages automatically. After you introduce needs, only the listed jobs supply dotenv variables. If a job suddenly stops seeing VERSION_TAG, check whether the producer job was omitted from needs. GitLab calls this out in both the dotenv guide and the needs documentation.

3. Treating artifacts:access as a secret-protection feature

Dotenv files are still artifacts, and GitLab documents artifacts:access for controlling who can download job artifacts from the GitLab UI or API. In the example above, access: developer is a reasonable lab default for non-secret metadata.

But GitLab also documents an important limitation: artifacts:access only affects UI and API downloads. CI/CD jobs using job tokens can still access artifacts with the runner API regardless of that setting. If you need tighter job-token-based restriction, GitLab points you to the project's CI/CD visibility setting for project-member-only access. Even then, the safer rule does not change: never store secrets in dotenv artifacts.

4. Keeping metadata artifacts longer than necessary

GitLab documents artifacts:expire_in in the YAML syntax reference. For short-lived build metadata, storing artifacts for a day or a week is usually enough. This matters in busy repositories where even tiny artifacts accumulate over time.

What if the producer job is conditional?

If the producer job sometimes does not exist because of rules, a downstream needs relationship can fail pipeline creation. GitLab's needs documentation covers this with optional: true.

deploy_review:
  needs:
    - job: prepare_meta
      artifacts: true
      optional: true

That pattern is useful when metadata should exist only in specific pipeline types, such as merge request pipelines.

When to use dependencies instead

If you want a later-stage job to inherit dotenv variables from only one earlier producer, but you do not need DAG-style early execution, dependencies can be simpler than needs. GitLab documents this directly in the dotenv variables guide.

prepare_meta:
  stage: prepare
  script:
    - echo "BUILD_VERSION=v1.0.0" > build.env
  artifacts:
    reports:
      dotenv: build.env

package_info:
  stage: package
  dependencies:
    - prepare_meta
  script:
    - echo "$BUILD_VERSION"

Expected result:

  • package_info runs in normal stage order
  • BUILD_VERSION is available in the job script
  • no other earlier job artifacts are inherited unless listed

Use needs when you want faster execution or cross-stage DAG behavior. Use dependencies when you mainly want tighter artifact inheritance in a normal staged pipeline.

Cleanup

This lab does not create cloud resources, but it does create pipeline artifacts and a demo file under dist/.

Cleanup is simple:

  1. remove the demo jobs from .gitlab-ci.yml if you no longer need the lab
  2. let the artifact expire naturally with expire_in
  3. delete the test branch if you created one only for this exercise

If you adapt this pattern to real review environments or deployments, add a proper stop or teardown job for the environment itself. The dotenv file should carry metadata, not become your only cleanup mechanism.

Final thoughts

For GitLab pipelines, dotenv artifacts are the cleanest official solution when one job computes small, non-secret values that later jobs must reuse. The operating rules are straightforward: keep the file simple, keep it small, do not put secrets in it, remember that rules cannot see dotenv values, and be explicit with needs or dependencies so inheritance stays predictable.

If you want structured hands-on practice with pipelines, runners, security scans, registries, DAG execution, caching, and Kubernetes delivery, see Automating CI/CD with GitLab.

Sources