A Pragmatic Pipeline: Approval Gate, Parallelism, and Triggers

K
Kai··4 min read

The pipeline in the previous article runs straight from commit to production — fast, but too automatic for many teams: nobody gets a chance to look before code hits prod. A real production pipeline needs a stop point with a human reviewer, runs a few things in parallel for speed, and triggers only on the right branch. This article adds all three.

Goal

Add a manual approval gate and approve it via the CLI, understand runOrder for running actions in parallel or in sequence, and filter triggers by branch.

Manual approval gate

Insert an Approval stage between Build and Deploy. An Approval/Manual action does nothing except halt the pipeline to wait for someone to approve:

{"name": "Approval", "actions": [{
  "name": "ManualApproval",
  "actionTypeId": {"category": "Approval", "owner": "AWS", "provider": "Manual", "version": "1"},
  "configuration": {"CustomData": "Phê duyệt để deploy lên production"},
  "runOrder": 1
}]}

Update the pipeline (now 4 stages) and run it:

$ aws codepipeline update-pipeline --cli-input-json file://pipeline.json \
    --query 'pipeline.stages[].name' --output text
Source  Build   Approval    Deploy

$ aws codepipeline start-pipeline-execution --name awscicd-pipeline

Poll the status — the pipeline runs Source, Build, then stops at Approval:

$ aws codepipeline get-pipeline-state --name awscicd-pipeline \
    --query 'stageStates[].[stageName,latestExecution.status]' --output text
Source:Succeeded  Build:Succeeded  Approval:InProgress  Deploy:None

Approval:InProgress means it's waiting. Deploy has not run — nothing reaches prod until it's approved. To approve via the CLI, fetch the approval action's token and submit a result:

$ TOKEN=$(aws codepipeline get-pipeline-state --name awscicd-pipeline \
    --query "stageStates[?stageName=='Approval'].actionStates[0].latestExecution.token | [0]" --output text)

$ aws codepipeline put-approval-result --pipeline-name awscicd-pipeline \
    --stage-name Approval --action-name ManualApproval \
    --result summary="Đồng ý deploy",status=Approved --token "$TOKEN" \
    --query 'approvedAt' --output text
2026-05-25T13:51:12+07:00

After approval, the pipeline continues with Deploy and finishes:

$ aws codepipeline get-pipeline-execution --pipeline-name awscicd-pipeline \
    --pipeline-execution-id $EXID --query 'pipelineExecution.status' --output text
InProgress
...
Succeeded

The token is the crux: each approval pause produces its own token, and put-approval-result must include that exact token — so you can't accidentally approve a stale execution. Send status=Rejected instead of Approved and the pipeline stops dead, no deploy. In practice people approve via the console or via a notification (Article 14), but the underlying mechanism is this same token.

   Source ─▶ Build ─▶ [ Approval: wait for approver ] ─▶ Deploy
                            │
                            ├─ put-approval-result Approved (token) ─▶ run Deploy
                            └─ Rejected ─▶ pipeline stops, no deploy

runOrder: parallel or sequential

Within a stage, actions run by runOrder: actions with the same runOrder run in parallel, a higher runOrder runs after. For example, a Build stage that runs two things in parallel (build + lint) then one thing afterward (package):

"actions": [
  {"name": "Build", "runOrder": 1, ...},
  {"name": "Lint",  "runOrder": 1, ...},
  {"name": "Package", "runOrder": 2, ...}
]

Build and Lint (both runOrder 1) run concurrently; Package (runOrder 2) runs only after both finish. This is how you shorten a pipeline: independent work (building multiple components, running multiple test suites) runs in parallel instead of queuing.

Triggers: run only on the right branch

By default the pipeline runs every time the source branch gets a commit. Pipeline type V2 allows finer trigger filtering — triggering only on the right branch, tag, or file path. Declare it in triggers:

"triggers": [{
  "providerType": "CodeStarSourceConnection",
  "gitConfiguration": {
    "sourceActionName": "Source",
    "push": [{"branches": {"includes": ["main", "release/*"]}}]
  }
}]

With this configuration the pipeline runs only when you push to main or release/*, skipping feature branches — avoiding wasted build/deploy for branches that aren't ready. Filtering by tag (deploy only when a version tag is attached) or by file path (run only when some directory changes) is declared the same way. This is a V2-only feature, one reason to use V2 for new pipelines.

🧹 Cleanup

The pipeline + target are reused in Article 14, kept through the end of Part V. Delete commands as in Article 12.

Summary

Three things turn a pipeline that "runs" into one you can "use": a manual approval gate (an Approval/Manual stage halts the pipeline waiting for put-approval-result with a token — approve and it deploys, reject and it stops); runOrder to run actions in parallel (same number) or sequentially (increasing number), cutting time; and V2's trigger filtering so the pipeline runs only on the right branch/tag/path. The pipeline now has a human control point and runs only when it should.

The next article — closing Part V — attaches quality and observability to the pipeline: running test/scan as a blocking stage, and firing pipeline status notifications (success, failure, awaiting approval) to SNS/email via a notification rule and EventBridge.