GitLab CI/CD pipeline stages with artifact passing between jobs

devops-cicd

The pipeline kept running all three jobs in parallel. Not what I wanted. I had a build job producing a compiled binary, a test job that needed that binary, and a deploy job. Without stages: defined explicitly, GitLab defaults to running everything it can concurrently, and artifacts from a job in the same implicit stage don’t propagate the way you’d expect.

The fix is obvious in hindsight but the docs bury it.

You need stages: at the top of .gitlab-ci.yml, and each job must declare which stage it belongs to. That’s what controls order. Artifacts then flow forward automatically to jobs in later stages, but only if you declare artifacts: paths: on the producing job. The consuming job doesn’t need to do anything special, it just finds the files at the same relative path they were saved from.

One thing that actually bit me: the artifact wasn’t showing up in the deploy job. I had expire_in: 1 hour set during testing and forgot the pipeline was queued for 90 minutes before the runner picked it up. Artifact expired before deploy ran. Changed it to expire_in: 1 day and stopped thinking about it.

.gitlab-ci.yml

stages:
  - build
  - test
  - deploy

build_app:
  stage: build
  image: golang:1.22
  script:
    - go build -o bin/app ./cmd/app
  artifacts:
    paths:
      - bin/app
    expire_in: 1 day

run_tests:
  stage: test
  image: golang:1.22
  script:
    - ./bin/app --version
    - go test ./...
  dependencies:
    - build_app

deploy_to_staging:
  stage: deploy
  image: alpine:3.19
  script:
    - apk add --no-cache openssh-client
    - scp -i $SSH_KEY bin/app deploy@$STAGING_HOST:/opt/app/
    - ssh -i $SSH_KEY deploy@$STAGING_HOST "systemctl restart app"
  environment:
    name: staging
    url: https://staging.example.com
  only:
    - main

dependencies vs needs

The dependencies: key on run_tests tells GitLab which jobs’ artifacts to download. If you omit it, GitLab downloads artifacts from all previous stage jobs, which is usually fine but gets messy in larger pipelines with many parallel jobs producing different artifacts. Being explicit is cleaner.

needs: is a different thing. It lets a job start before its stage officially begins, creating a DAG instead of a linear stage sequence. Useful when you have independent jobs and want to cut total pipeline time. But if you use needs:, GitLab won’t automatically pass artifacts unless you also set artifacts: true inside the needs: block.

# needs: with artifact passing
deploy_to_staging:
  stage: deploy
  needs:
    - job: build_app
      artifacts: true

Without artifacts: true there, the deploy job starts early but finds an empty bin/ directory. GitLab doesn’t warn you about it.

environment variables for SSH

The $SSH_KEY and $STAGING_HOST variables go in Settings, CI/CD, Variables. Mark the key as type File, not Variable, so GitLab writes it to a temp path and you can pass that path directly to scp and ssh -i. If you set it as Variable, you get the raw key contents injected as an env var and then have to write it to a file yourself in the script, which is just extra steps and a place to get permissions wrong (chmod 600 or ssh rejects it).

Also set the variable to protected and masked if the branch is protected. Otherwise any pipeline on any branch can read it.

official docs

Leave a Reply

Your email address will not be published. Required fields are marked *

Support us · 💳 Monobank