BLOG/TEAM CI/CD
THEME
TEAM CI/CD

Deploy approvals and manual gates

A human confirmation before a production publish — that prompts when a person runs it, and auto-skips to a safe default in CI. One workflow, two audiences, no separate 'CI version'.

May 22, 2026 OrchStep Team 6 minROLE: DevOps EngineerSCALE: Enterprise
RUNNABLE DEMO
Full source for this post: blog/deploy-approvals
VIEW SOURCE

Production deploys want a human in the loop. Not a heavyweight change-board for every release, but a moment where someone says "yes, ship it" before the publish happens. The trouble is that the same workflow has to run two ways: interactively, where an engineer is at a terminal and should be asked, and non-interactively, in CI, where there's no one to answer and a hanging prompt deadlocks the runner forever.

Teams usually solve this by forking the logic — a "manual" script with the confirmation and an "automated" one without — and then the two drift, and the automated path quietly loses the gate it was supposed to have.

OrchStep's prompt function is built for exactly this split. The same step asks a human when there's a human, and falls back to its default when there isn't.

The workflow

orchstep.yml
name: release
# One workflow for humans and CI: a confirm prompt gates the publish.
# Interactively it asks; non-interactively it uses the default and auto-skips.
defaults:
  version: "1.4.0"

tasks:
  # `orchstep run publish`
  publish:
    steps:
      - name: build
        func: shell
        do: echo "packaging release {{ vars.version }}"

      - name: approve
        func: prompt
        args:
          message: "Publish {{ vars.version }} to production?"
          type: confirm
          default: false

      - name: gate
        if: '{{ eq steps.approve.value "true" }}'
        then:
          - name: ship
            func: shell
            do: echo "publishing {{ vars.version }} to production"
        else:
          - name: hold
            func: shell
            do: echo "approval not granted; publish skipped"

The same step, three ways

The approve step is a confirm prompt with default: false. That default is the whole trick — it's what happens when nobody answers.

# Human at a terminal — the prompt appears and waits
orchstep run publish

# CI runner — stdin isn't a terminal, so the prompt auto-skips to its default (false)
ORCHSTEP_NON_INTERACTIVE=true orchstep run publish

# Approve without typing — provide the value by the step's name
orchstep run publish --var approve=true

A prompt step skips automatically when stdin isn't a terminal, when ORCHSTEP_NON_INTERACTIVE=true, or when a --var matching the step name supplies the answer. So in CI the gate doesn't hang — it resolves to false and the deploy holds. The safe outcome is the default outcome, which is the property you want from a guard. Full behavior: Functions: prompt.

The gate reads the answer

The publish itself is conditional on what the prompt returned:

- name: gate
  if: '{{ eq steps.approve.value "true" }}'
  then:
    - name: ship
      func: shell
      do: echo "publishing {{ vars.version }} to production"
  else:
    - name: hold
      func: shell
      do: echo "approval not granted; publish skipped"

steps.approve.value is the prompt's result — "true" only if a human confirmed or someone passed --var approve=true. Default false means an unattended run takes the else branch and doesn't publish. To actually ship from automation you have to be explicit about it, which is the right amount of friction for a production gate.

Wiring it into a pipeline

In CI you make the approval a deliberate input — a workflow-dispatch field, a protected-environment step, a manual job. Your runner then passes it through:

orchstep run publish --var version=1.4.0 --var approve=$APPROVED

No approval, no publish — and the build and packaging steps before the gate still run, so you get a fully prepared artifact that's just waiting on the word.

What you gained

ConcernForked scriptsOrchStep
Ask a human"manual" scriptfunc: prompt, type: confirm
Don't hang CI"automated" scriptauto-skip to default
One source of truthtwo files that driftone workflow
Gate the publishhope it's wiredif: on steps.approve.value
Approve from CIbespoke flag--var approve=true

Where to go next

Install with curl -fsSL https://orchstep.dev/install.sh | sh. Run orchstep run publish once at your terminal and once with ORCHSTEP_NON_INTERACTIVE=true to see both paths.

#CI#APPROVALS#DEPLOYMENT#GOVERNANCE
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — TEAM CI/CD