A CI/CD pipeline draft is a living, executable description of how code moves from commit to production, capturing the intent, sequence, and conditions of each build, test, and deploy step. It matters most during the early phases of a project, when teams are aligning on workflows, or when a new system is being integrated into an existing deployment ecosystem. Drafting a pipeline is not just configuration—it is design: shaping the feedback loop between developer, code, and production system.

Designing the Pipeline Flow

The core of pipeline drafting is defining the sequence of stages, their dependencies, and their triggers. A minimal draft begins with a pipeline.yaml file at the project root, used by any CI system that supports declarative configuration.

pipeline:
  build:
    image: node:18-alpine
    commands:
      - npm install
      - npm run build
    when:
      branch: main
      event: push
  test:
    image: python:3.11-slim
    commands:
      - pip install -r requirements.txt
      - pytest tests/
    depends_on: [build]
    when:
      branch: develop
      event: pull_request
  deploy:
    image: ubuntu:22.04
    commands:
      - ./deploy.sh
      - kubectl apply -f k8s/
    environment: production
    triggers:
      - branch: main
      - event: push
    depends_on: [test]

Each stage is a named unit of work, defined by a container image, a sequence of shell commands, and metadata about when and where it runs. The when block applies conditions to the stage: it runs only if the current Git branch matches and the event is one of the specified types. The depends_on list ensures that a stage executes only after all listed stages complete successfully.

The most common failure is misplacing the when block: a developer assumes a test stage runs on all PRs but forgets that when: event: pull_request applies only to the PR itself, not to the PR’s head branch. A stage can run on multiple events, but only one of those events is the trigger: event: push means the stage is triggered by a git push, while event: pull_request means it is triggered by a PR open or update.

Setting Up Environment and Secrets

Each stage can specify a custom environment, defined by environment variables and secret injection. These values are passed into the container at runtime, and can be used in scripts or as configuration.

environment:
  NODE_ENV: production
  DATABASE_URL: postgres://prod:5432/myapp
  AWS_ACCESS_KEY_ID:
    from_secret: aws_access_key_id
  AWS_SECRET_ACCESS_KEY:
    from_secret: aws_secret_access_key

Secrets are sourced from a central store, referenced by a key. The system resolves from_secret: key at runtime, reading the value from a secure backend like HashiCorp Vault, AWS Secrets Manager, or a file-based store. A common failure is assuming that secrets are resolved only at pipeline launch, but in practice, they are interpolated at the moment the command starts. If a secret changes during a long-running build stage, the test stage sees the new value, but the deploy stage sees the old value—unless the pipeline explicitly reloads secrets.

Secrets are versioned and can be rotated independently. A deploy stage might use a DEPLOYMENT_TOKEN secret that is updated after a deployment, to signal the next pipeline run.

Managing Artifacts and State

Artifacts are files produced during a stage and passed to subsequent stages. A build stage generates a dist/ directory, which is then used by test and deploy.

artifacts:
  - path: dist/
    name: frontend-bundle
    cache: true
  - path: coverage/
    name: test-coverage
    upload: true

Each artifact is a named bundle of files. The path defines the directory to archive, name gives it a handle for later use, and cache: true enables incremental caching: the artifact is stored and reused across pipeline runs, with a key based on the Git commit SHA.

The upload: true flag means the artifact is uploaded to a storage backend—typically an S3-compatible bucket or a dedicated artifact server. During deploy, the deploy.sh script can pull the frontend-bundle artifact into the container:

#!/bin/bash


set -euo pipefail

# Pull the frontend bundle
aws s3 cp s3://artifacts/myapp/${CI_COMMIT_SHA}/frontend-bundle.tar.gz /tmp/frontend-bundle.tar.gz
tar -xzf /tmp/frontend-bundle.tar.gz -C /app

# Deploy to Kubernetes
kubectl apply -f k8s/

A silent failure occurs when the artifact path is incorrect: dist/ exists, but dist/index.html is missing, and the deploy.sh script assumes it. The pipeline runs successfully, but the deployed app is broken. The artifacts block should include a validate: true option, which runs a script to verify that the artifact meets expectations.

Handling Errors and Retries

Each stage can define a failure strategy, which controls how the pipeline responds to command failures.

test:
  image: python:3.11-slim
  commands:
    - pip install -r requirements.txt
    - pytest tests/
  failure:
    retry: 3
    strategy: exponential
    wait: 10s
    on:
      - exit_code: 1
      - command: pytest

The failure block defines a retry policy: retry: 3 means up to three attempts are made. The strategy: exponential applies increasing delays between attempts: 10s, 20s, 40s. The wait: 10s is the base delay between retries.

The on block specifies which conditions trigger a failure. Here, a failure occurs if the pytest command exits with code 1. But on can also check for specific output patterns, file presence, or exit codes from any command.

A common mistake is assuming that failure.retry applies to all commands in the stage, but in reality, it applies only to the entire stage, not to individual commands. A command that fails but is not listed in on does not trigger a retry. To ensure a command triggers a retry, it must be explicitly listed.

Another failure mode is the on block being evaluated too early. If a command writes output to a file and the next command expects it, but the failure condition checks for the file before it exists, the pipeline may retry a stage that succeeded in the first run.

Triggering and Scheduling

A pipeline can be triggered by multiple sources: Git events, manual actions, or time-based schedules.

triggers:
  - branch: main
    event: push
  - event: pull_request
    label: "ready for review"
  - schedule: "0 1 * * 1"  # Every Monday at 1 AM
  - manual: true

The schedule field uses cron syntax: 0 1 * * 1 means run at 1 AM every Monday. This is used for nightly builds, weekly reports, or automated maintenance.

When a pipeline is triggered manually, it prompts the user for input: environment, commit, branch, or additional_artifacts. These inputs become variables during execution.

inputs:
  - name: environment
    type: choice
    options: [staging, production]
    default: staging
  - name: commit
    type: string
    default: HEAD
  - name: additional_artifacts
    type: list
    default: []

A critical edge case arises when multiple triggers fire simultaneously. The triggers block is evaluated in order, and the first matching trigger wins. If a push to main and a pull_request on develop both occur at the same time, and the pipeline is triggered by both, the main push wins, and the develop PR is processed separately.

Managing Pipeline State and Feedback

The pipeline draft must define how state is updated and how feedback is communicated.

Each stage emits a status: pending, running, success, failure, canceled. These statuses are reported back to the Git provider—GitHub, GitLab, or Bitbucket—via a webhook.

The pipeline system updates the status for each commit, and the commit status includes a URL to the full pipeline run. A failure status includes a details_url to a dashboard showing logs, artifacts, and error traces.

A common failure is the lack of a status block in the stage definition, causing the pipeline to report only success or failure, but not pending at the start of a stage.

status:
  context: ci/cd
  target_url: https://pipeline.example.com/runs/${CI_RUN_ID}
  description: "Build and test complete"

The context field determines how the Git provider groups statuses. A context: ci/cd groups all pipeline stages under a single ci/cd status, while context: build isolates the build stage.

The target_url is a dynamic link using variables: ${CI_RUN_ID} expands to the pipeline run ID, and ${CI_COMMIT_SHA} to the commit hash.

Advanced: Conditional and Parallel Stages

For complex workflows, stages can be conditional and run in parallel.

stages:
  - build
  - test:
      when: $CI_BRANCH == "main"
  - deploy:
      when: $CI_BRANCH == "main" and $CI_EVENT == "push"
  - lint:
      when: $CI_EVENT == "pull_request"
  - release:
      when: $CI_BRANCH == "main" and $CI_EVENT == "push"
      parallel: true
      matrix:
        - version: 1.0.0
          release_notes: "Initial release"
        - version: 1.1.0
          release_notes: "Bug fixes and performance improvements"

The matrix block enables parallel execution: multiple instances of the release stage are started, each with a different version and release_notes. These instances run concurrently, and their outputs are collected.

The parallel: true flag signals that the stage should be split across multiple runners, each running one matrix item. A failure in one matrix item does not fail the entire stage, but the stage status reflects the overall outcome.

A subtle failure mode is the when condition being evaluated at the time of the stage’s creation, not at runtime. If a matrix stage is created at the start of the pipeline but the CI_BRANCH changes during execution, the when condition may not reflect the current state.

Finalizing the Draft

A well-drafted pipeline is not just a configuration file—it is a contract between developer, CI system, and production environment. It is tested by running it locally with pipeline run --dry-run --commit=abc123, which validates the pipeline without triggering a full run.

The draft is finalized when it includes:

  • A ci/ directory with pipeline.yaml
  • A scripts/ directory with reusable shell scripts
  • A docs/ directory with a pipeline.md that explains the stages, their inputs, and their expected outputs
  • A test-pipeline.sh script that simulates a full run with sample data

The draft becomes the source of truth: when a new team member asks, “How does a PR get deployed?”, the answer is: “Run the pipeline draft.”

This page was rewritten on 14 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.