GitLab CI stages are not jobs, and the difference matters

Jobs in the same stage run in parallel and jobs in later stages wait for all of the previous stage, which is obvious once stated and produces surprising pipelines when it is not.

stages: [build, test, deploy]

build:
  stage: build
  script: [composer install --no-dev]
  artifacts:
    paths: [vendor/]
    expire_in: 1 hour

unit:
  stage: test
  script: [vendor/bin/phpunit]

static:
  stage: test
  script: [vendor/bin/phpstan analyse]

The two test jobs run at the same time and both receive the vendor/ artifact from the build stage automatically, which is the mechanism that makes splitting worthwhile — the pipeline is as long as its slowest job per stage rather than the sum. Artifacts are uploaded and downloaded, so a large one can be slower than rebuilding; a cache is the right tool for dependencies that change rarely and an artifact for anything a later stage genuinely needs.