Structuring a Travis CI Pipeline with Build Stages: Test, Build, Deploy
Travis CI build stages let you gate deploys behind tests with a simple stages/jobs.include syntax. Here's a worked three-stage config, plus the artifact-persistence trap most teams hit.
08 Jun 2026, 07:30 UTC

If your .travis.yml runs everything in one flat job list, the useful fix is build stages: declare a stages: order, tag each entry in jobs.include with a stage, and Travis will run jobs within a stage in parallel while holding later stages until every job in the previous stage passes. That gives you a real test → build → deploy gate without any external orchestration.
How stage ordering actually works
Travis groups jobs by their stage: name and starts stages in the order listed under the top-level stages: key. Two rules matter most:
- Parallelism is per stage. All jobs sharing a stage start together (subject to your plan's concurrency limit), so three test jobs in a
teststage run side by side. - Failure propagates forward. If any job in a stage fails, later stages never start and the whole build is marked failed. This fail-fast behavior is exactly what you want gating a deploy behind tests.
If you omit stages: entirely, older configs still work: everything runs as a single implicit stage named test. That makes migration incremental — you can add stages to one branch without breaking existing behavior.
A worked three-stage configuration
This example targets a Java/Maven project, but the structure is language-agnostic. It goes in .travis.yml at the repository root; no special permissions are needed beyond a normal push that triggers a build.
language: java
stages:
- test
- build
- deploy
jobs:
include:
- stage: test
name: "Unit tests (JDK 11)"
jdk: openjdk11
script: mvn test
- stage: test
name: "Unit tests (JDK 17)"
jdk: openjdk17
script: mvn test
- stage: build
name: "Package artifact"
script: mvn package -DskipTests
- stage: deploy
name: "Deploy to production"
script: ./deploy.sh
if: branch = main AND type = push
Reading top to bottom: the two JDK test jobs run in parallel. Only when both pass does Package artifact start. Only when packaging succeeds does the deploy job become eligible — and even then, the if: condition skips it on pull requests and non-main branches.
To verify the wiring, push a deliberately failing test on a scratch branch. In the Travis web UI you should see the test stage fail, the build and deploy stages greyed out as skipped, and the build marked failed. Then fix the test and confirm all three stages run in order.
The workspace trap: artifacts do not persist between jobs
This is the most common design mistake with stages. Every job gets its own fresh VM or container. A jar produced in the build stage simply does not exist on the machine running the deploy stage.
You have two realistic handoff options:
- External storage (recommended). Upload the artifact to S3, a package registry, or your artifact server in the build stage, and download it in the deploy stage. This is deterministic and survives retries.
- The
cache:directive. Travis caches directories between builds, but the documentation describes caching as best-effort — it can be skipped, stale, or cold on a new worker. Never rely on it for correctness of an artifact handoff; at most, use it to speed up dependency downloads.
A cheap way to confirm this behavior yourself: add a build job that writes a file and a deploy job that tries to read it. The read will fail, demonstrating that the workspace is not shared.
Conditions are evaluated per job, not per stage
The if: key attaches to a job, and Travis evaluates it independently for each job in the stage. If you later add a second deploy job (say, a notification step) and forget the condition, that job will run on every branch even though its sibling is gated. The rule of thumb: every job in a conditional stage needs the condition. A typical gate is if: branch = main AND type = push, which excludes both pull-request builds and branch pushes to feature branches.
Limits and mistakes to plan around
- Concurrency is plan-limited. Stage support itself is stable, but how many same-stage jobs actually run in parallel depends on your Travis plan's concurrent-job allowance. With a low limit, "parallel" test jobs queue behind each other. Check your plan before designing a wide test matrix.
- Product tiers have shifted over time. The travis-ci.com vs. legacy travis-ci.org split and pricing changes have moved over the years; verify your repositories are on the current platform before assuming a config will trigger at all.
- Don't duplicate work across stages. Because environments are fresh, each job re-installs dependencies. Keep per-job setup lean (
install:steps, cache for dependencies) or stage runtimes balloon. - Don't use stages to sequence things that need shared state. If step B fundamentally needs step A's filesystem, merge them into one job's
script:instead of splitting across stages.
A practical rollout check
Before trusting the pipeline with real deploys: (1) push the config on a throwaway branch and confirm jobs group by stage in the UI; (2) force a test failure and confirm downstream stages are skipped; (3) open a pull request and confirm the deploy job is skipped by its if: condition. If all three behave as expected, the gating semantics are doing what you configured — and the only remaining risk is the artifact handoff, which external storage solves.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.