A Jenkins pipeline stage waiting on a manual approval has held a build agent and its workspace for two days. Why is that happening, and how do you gate the deploy without pinning the executor?
answer
- who is holding the executor right now
- the step form blocks on the agent
- the directive pauses before the agent
- bound the wait with a stage timeout
- expiry aborts, it does not approve
basics
~20 sBecause the input step sits inside the stage's steps, it blocks while the surrounding agent's executor and workspace stay allocated. Use the stage-level input directive instead — it pauses before the agent is allocated — and bound the wait with a timeout.
solid answer
~50 sThere are two places a manual gate can live and they behave very differently. The `input` step written inside `steps` pauses in the middle of the stage body, so whatever agent that stage is running on keeps its executor and its workspace reserved for the whole wait — days, if nobody clicks. The stage-level `input` directive pauses the stage after its options are applied and *before* entering the stage's agent, so no heavy executor is held; only the pipeline's own lightweight executor on the controller remains. The second half of the answer is the timeout: an unbounded gate is a build that waits forever, so wrap the stage in `options { timeout(time: 8, unit: 'HOURS') }` and let an unanswered approval abort rather than accumulate. Use `submitter` to restrict who may approve and `submitterParameter` to record who did.
code
groovy · 26 linespipeline {
agent none
stages {
stage('Build') {
agent { label 'build' }
steps { sh './build.sh' }
}
stage('Deploy to production') {
options { timeout(time: 8, unit: 'HOURS') }
input {
message 'Deploy this build to production?'
ok 'Deploy'
submitter 'release-managers'
submitterParameter 'APPROVER'
}
agent { label 'deploy' }
steps {
milestone label: 'production'
sh "./deploy.sh --approved-by=${env.APPROVER}"
}
post {
aborted { echo 'approval expired or was rejected' }
}
}
}
}go deeper
Know that input pauses the build for a human and that where you put it matters: inside steps it waits on the agent, as a stage directive it waits before the agent is claimed.
Explain the resource consequence — a blocked step keeps its executor and workspace reserved — and show the fix with agent none, per-stage agents and a stage-level timeout.
Demonstrate the operational picture: gates that starve the executor pool, expiry that aborts rather than approves, restricting approvers with submitter and recording them, and milestone for out-of-order approvals.
Own the gating policy itself — which releases need a human at all, what the approval deadline is, who is accountable for the click, and how many concurrent pending approvals the fleet can absorb before the queue deadlocks.
## Two forms, one keyword Jenkins gives you `input` twice. As a **step**, it is called from inside `steps` like any other step. As a **directive**, it is a block on the stage itself, alongside `agent`, `options` and `when`. They read almost identically in a Jenkinsfile and they behave differently in exactly the way this incident exposes. ```groovy // Step form — pauses inside the stage body, on the agent stage('Deploy') { agent { label 'deploy' } steps { input message: 'Deploy to production?' sh './deploy.sh' } } ``` ```groovy // Directive form — pauses before the agent is entered stage('Deploy') { options { timeout(time: 8, unit: 'HOURS') } input { message 'Deploy to production?' ok 'Deploy' submitter 'release-managers' submitterParameter 'APPROVER' } agent { label 'deploy' } steps { sh "./deploy.sh --approved-by=${env.APPROVER}" } } ``` ## Why the step form holds an executor A Pipeline step inside a stage with an agent runs on that agent, in that agent's workspace, occupying one of the node's executors. `input` does not release any of that while it waits — from the node's point of view the build is simply still running. Two days of waiting is two days of one executor unavailable to every other job, plus a workspace directory nobody can reclaim. On a small fleet, three pending approvals can deadlock the queue: builds wait for executors that are held by builds waiting for humans. The directive form pauses the stage after its `options` are applied and before entering the stage's `agent`, so at approval time no heavy executor has been claimed at all. The run still consumes the pipeline's own lightweight executor on the controller — that is how it stays resumable — but that is cheap and is what Jenkins is designed to hold for long-lived runs. The same reasoning explains the companion pattern: declare `agent none` at the top of the pipeline and give each stage its own agent, so nothing is allocated across a gate that sits between stages. ## Always bound the wait An approval with no deadline is a build that lives forever, and forever is long enough for the artifact to be stale, the branch to have moved on, and the queue to fill with pending gates. Wrapping the stage in `options { timeout(time: 8, unit: 'HOURS') }` makes an unanswered approval expire. When it does, the run ends ABORTED — not UNSTABLE, and not auto-approved. Handle that in `post { aborted { ... } }` if you want a notification. Rejecting the input in the UI likewise aborts the run. ## Who may approve, and recording who did `submitter` restricts approval to named users or groups, which is what makes the gate an actual control rather than a formality — without it, anyone who can see the build can click. `submitterParameter` names a variable that receives the approving user's ID, so the deployment can log or pass on who authorised it. `parameters` inside the directive prompts for values at approval time (a version, a target region), and in the step form `input` returns those values so you can assign them. ## Ordering hazards Manual gates create a queue of approvable builds, and they can be approved out of order: build 40 sits pending, build 42 is approved and deploys, then somebody approves 40 and it deploys an older commit over the newer one. The `milestone` step is the guard — a build passing a milestone causes older builds that have not reached it to be aborted, so approvals cannot resurrect superseded runs. If the gate protects a single shared environment, consider also serialising the deploy so two approvals cannot deploy concurrently. ## What a complete answer covers Diagnose (the step form blocks on the agent), fix (directive form, or `agent none` with the gate between stages), bound (a stage `timeout`, and know that expiry aborts), and control (`submitter`, `submitterParameter`, and `milestone` for out-of-order approvals). Interviewers ask this one because it is the intersection of pipeline flow control and fleet capacity, and because almost every team has been bitten by it.
- What happens to the build when a timeout wrapping an approval expires?The waiting input is interrupted and the run ends ABORTED — it is never auto-approved and it is not merely marked unstable. A `post { aborted { ... } }` block is where you notify or clean up. Rejecting the input in the UI produces the same aborted outcome, which is usually what you want for an unapproved release.
- Even with the directive form, what is the pipeline still holding while it waits?The run's own lightweight executor on the controller, which is what keeps the build resumable across a controller restart. That is cheap and expected; the expensive thing you have avoided is a heavy executor and workspace on a build agent, which the step form would have pinned for the whole wait.
- Two builds are pending approval and someone approves the older one after the newer has deployed. How do you prevent that?Use the `milestone` step around the deploy. When a build passes a milestone, older builds of the same job that have not reached it are aborted, so a stale pending approval cannot deploy over a newer release. It is the standard guard for any gate that lets humans approve out of order.
- How do you stop anyone with build access from approving a production deploy?Set `submitter` on the input to the users or groups permitted to approve, and pair it with `submitterParameter` so the approver's ID is recorded and can be passed into the deploy for auditing. Without `submitter`, the gate documents intent but enforces nothing beyond ordinary job permissions.
saying these in an interview costs you the question
- Thinking a paused input releases the agent automatically
- Leaving a manual gate with no timeout at all
- Expecting an expired approval to proceed automatically
- Putting the input inside steps on a heavyweight agent
- Assuming any user with build access should be able to approve