Key points
- An approval step pauses the run and releases runner capacity rather than holding it, and it survives a control-plane restart.
approvalcannot be combined withtasks,goal,script, orinclude, and an approval step cannot declare outputs.teamsnames the teams whose members may decide;allow_self_approvaldecides whether the person who started the run may be one of them.timeoutbounds the wait. An expired approval produces atimed_outrun, not a failure.- Approval failures always fail closed:
ignore_failuredoes not apply to them.
Examples
- name: production-gate
depends_on: [package]
approval:
type: production-release
teams:
- platform/sre
allow_self_approval: false
timeout: 24h
- name: publish
depends_on: [production-gate]
script: |
./scripts/promote.shname: release-service
description: Build, verify, and publish the payments service.
container_image: alpine:3.20
working_directory: /workspace
timeout: 45m
display_option: list
variables:
- RELEASE_CHANNEL
- platform/shared:ARTIFACT_BUCKET
steps:
- name: build
script: |
make build
echo "1.0.$(date +%s)" > /nopsai/outputs/BUILD_TAG
outputs:
- name: BUILD_TAG
- name: verify
image: golang:1.24
depends_on: [build]
runtime_pool: ci
volumes:
- build-cache:/root/.cache
script: |
go vet ./...
go test ./...
- name: checks
depends_on: [verify]
tasks:
- name: licenses
ignore_failure: true
script: |
./scripts/license-check.sh
- name: sbom
script: |
./scripts/sbom.sh > /workspace/sbom.json
sha256sum /workspace/sbom.json | cut -d' ' -f1 > /nopsai/outputs/SBOM_DIGEST
outputs:
- name: SBOM_DIGEST
- name: report
depends_on: [checks.licenses, checks.sbom]
variables:
SBOM_DIGEST: $steps.checks.sbom.outputs.SBOM_DIGEST
script: |
echo "checks complete"
echo "sbom $SBOM_DIGEST"
- name: package
depends_on: [checks]
secrets:
- REGISTRY_TOKEN
- platform/shared:SIGNING_KEY
variables:
BUILD_TAG: $steps.build.outputs.BUILD_TAG
script: |
./scripts/package.sh "$BUILD_TAG"
echo "channel $RELEASE_CHANNEL, bucket $ARTIFACT_BUCKET"
./scripts/mint-token.sh > /nopsai/outputs/PUBLISH_TOKEN
./scripts/sign.sh --key "$SIGNING_KEY" --token "$REGISTRY_TOKEN"
outputs:
- name: PUBLISH_TOKEN
sensitive: true
- name: announce
depends_on: [package]
condition: Only run when the release channel is stable.
ignore_failure: true
script: |
./scripts/announce.sh
- name: production-gate
depends_on: [package]
approval:
type: production-release
teams:
- platform/sre
allow_self_approval: false
timeout: 24h
- name: publish
depends_on: [production-gate]
script: |
./scripts/promote.sh - name: production-gate
approval:
type: production-release
script: ./scripts/promote.shField reference
steps[].approvalstep modeobjectConditionalNone
Durable human checkpoint that pauses the run and releases runner capacity.
approval:
type: production-release
teams:
- platform/sre- Cannot be combined with
tasks,goal,script, orinclude. - Cannot declare outputs.
steps[].approval.typeapprovalstringRequiredNone
Approval kind recorded on the checkpoint and used for audit and notification routing.
type: production-release- Must match ^[a-zA-Z0-9_.-]+$.
services/nopsai/pkg/validation/pipeline.go
steps[].approval.teamsapprovalstring[]RequiredNone
Relative team paths allowed to approve or reject the checkpoint.
teams:
- platform/sre
- security- At least one team is required.
- Paths must be relative; absolute paths and
~are rejected. - Segments may not be empty,
., or... - Repeating the same team is rejected (case-insensitive).
services/nopsai/pkg/validation/pipeline.go
steps[].approval.allow_self_approvalapprovalbooleanOptionalfalse
Whether the user who started the run may approve this checkpoint.
allow_self_approval: falseLeaving this false preserves four-eyes separation between requester and approver.
steps[].approval.timeoutapprovaldurationOptionalNone
How long the checkpoint waits before the run is marked timed_out.
timeout: 24h- Must parse as a positive Go duration;
0and negative values are rejected.
services/nopsai/pkg/validation/pipeline.go
How it works
The checkpoint is durable because it is a record, not a paused process. That is why capacity is released while it waits, and why a restart does not lose the pending decision.
Put the gate where the risk changes. In this chapter it sits between packaging an artefact and promoting it, so everything reviewable has already happened and nothing irreversible has.
Who may approve is an authorization question, resolved through AAA against the named teams. The directive narrows the set; it does not grant anyone the right to decide.
Implementation evidence
services/nopsai/approval_schema.goApproval document schema and decision handling.
services/nopsai/pkg/validation/pipeline.goRules the validator enforces on this directive set.

