Artifacts, cache & rules
Pass files, speed builds, run jobs conditionally.
Jobs are isolated, so to pass files between them you declare artifacts. A build job produces a dist/ folder and lists it under artifacts:paths; GitLab uploads it and automatically makes it available to later stages. This is how a compiled binary reaches the deploy job or a test report reaches the UI — without it, each job starts from a clean checkout with nothing from the previous stage.
build:stage: buildscript: [make dist]artifacts:paths: [dist/]expire_in: 1 week # do not keep build outputs foreverreports:junit: report.xml # GitLab renders this as test resultsdeploy:stage: deployscript: [./deploy.sh dist/] # dist/ is here automatically, from build
Cache vs artifacts
They look similar but serve different jobs. Artifacts pass build outputs forward between stages and are tied to a pipeline. Cache speeds up jobs by reusing downloaded dependencies (node_modules, .m2, pip cache) across pipelines — it is an optimization, not a contract, and a job must still work if the cache is empty. Rule of thumb: artifacts for things you produce and need later; cache for things you can always re-fetch.
install:stage: buildscript: [npm ci]cache:key: ${CI_COMMIT_REF_SLUG} # per-branch cachepaths: [node_modules/]policy: pull-push # read it, then update it
rules: run jobs only when they should
The rules: keyword decides whether a job runs, based on conditions — the branch, whether it is a merge request, changed files, pipeline variables. It replaces the older only/except and is how one .gitlab-ci.yml handles feature branches, merge requests, and production deploys without duplicating jobs. A deploy job, for instance, should have a rule that runs it only on the default branch.
deploy-prod:stage: deployscript: [./deploy.sh production]rules:- if: '$CI_COMMIT_BRANCH == "main"' # only on mainwhen: manual # and only when a human clicks- when: never # otherwise this job does not exist