Upstream Jobs, Downstream Jobs, and Input

Scenario: Six chained jobs run, but nobody can identify which tested artifact reached the final environment.

Passing work between jobs safely means passing proof, not just a signal

When one Jenkins job triggers another - a "build" job triggering a "deploy" job, say - the most important design question is: what exactly gets passed between them? Get this wrong, and you end up in a very common, very confusing incident: the deploy job re-builds something slightly different from what the build job actually tested.

Analogy: Imagine a relay race where, instead of physically handing off the same baton, each runner just shouts "go!" to the next runner and then goes and grabs a new baton from a shared bin before running their own leg. Even if every runner individually runs a great leg, there's no guarantee the second runner's baton is actually the same one the first runner carried across their line - it might be a similar-looking one from the bin. Jobs chained together carelessly (passing only a signal like "build succeeded, go ahead") can make exactly this mistake: the deploy job "rebuilds" from the branch instead of literally deploying the artifact the build job already tested, and now two different sets of bytes are involved without anyone realizing it.

The fix, covered below, is to pass the actual artifact's identity (its digest, its exact build number) between jobs - not just a green light to go build something similar.

Pipeline example

stage('Promote') { input { message 'Promote tested digest?' }; steps { build job: 'deploy', parameters: [string(name: 'DIGEST', value: env.DIGEST)] } }

Pass artifact identity, not just a signal to proceed

The build step can invoke another job, pass it typed parameters, optionally wait for its completion, and propagate its result back to the caller. Upstream/downstream job links explain the orchestration relationship - which job triggers which - but say nothing on their own about which actual bytes moved between them. Pass the source commit SHA, an immutable artifact digest (like a container image digest, not just a floating tag), the originating build's URL, and the intended target environment. The downstream job should then retrieve exactly that package by its digest and verify its checksum, rather than rebuilding from a mutable branch reference that may have moved since the upstream job ran.

[Pipeline] build
Scheduling project: deploy-service
Starting building: deploy-service #57
deploy-service #57 completed with parameters:
  DIGEST=sha256:9f86d0...  SOURCE_BUILD=build-service#412
Result: SUCCESS

Notice what's passed here: a content-addressed digest and a reference back to the exact upstream build number - a future investigator, or an incident responder, can trace deploy-service #57 back to build-service #412 and confirm they're talking about the same, single set of tested bytes.

One Pipeline versus separate chained jobs

Long chains of triggered jobs can hide real coupling in credentials, default parameter values, retention policy, and result thresholds - a change to one job's defaults can silently change downstream behavior nobody remembered to check. A single Pipeline gives unified visualization and a simpler mental model when all its stages genuinely share the same ownership and lifecycle. Separate jobs make more sense at real permission boundaries, team boundaries, or where a deployment step is itself a reusable service invoked by many different upstream pipelines. Whichever you choose, document the interface between jobs explicitly, prevent accidental recursive triggering (job A triggers B which somehow triggers A again), and decide up front what happens if a downstream job's own timeout expires while the operation it started keeps running anyway on the target system.

input is a controlled, auditable pause - not just a button

The input step pauses a Pipeline for a human decision, can restrict who is allowed to submit it, and records exactly who did. Wrap it in a timeout so an abandoned approval can't hold an executor (or, worse, a deployment agent) hostage indefinitely, and place it before allocating any scarce deployment agent, not after - there's no reason to hold a valuable resource idle while waiting on a human. A well-designed approval binds together the approver's identity, the target environment, the specific policy evidence being approved against, and the exact artifact digest being promoted - "someone clicked Proceed" is meaningless on its own if it isn't tied to what they approved.

A pitfall: an approval that outlives the thing it approved

Test deliberately: an abort, a timeout expiring, a controller restart while paused at input, a duplicate click on Proceed, and a downstream failure that happens after acceptance but before the actual deployment completes. Ensure any external locks taken during this process have their own expiry, and that partial deployment operations are idempotent (safe to retry or resume). A real incident shape this guards against: an approver clicks "Proceed" on a deployment, but by the time the pipeline actually acts on that approval, the artifact digest that was approved has since been superseded or the target environment has changed underneath it - the authority to click "Proceed" must never silently carry over to approve different bytes than the ones actually reviewed. A rejected or expired promotion should leave the already-tested package available for a later, freshly-authorized attempt, while clearly preserving the fact that it was never actually released the first time.

Goal: Practice the concrete behavior in jenkins-parameterized-deploy. Learn sections have no Jenkins terminal; the Jenkins lab opens the real shared service in a new full-screen tab inside your private folder.