| | |

GitHub 18 ๐ŸŒ GitHub Actions CI/CD Pipeline Automation

โฑ๏ธ Estimated read: 9 min | |

A workflow that runs tests is continuous integration. A workflow that builds, tests, and deploys is continuous delivery. A workflow that deploys automatically on every merge to main is continuous deployment. GitHub Actions supports all three, but the difference between them is not the YAML syntaxโ€”it is the gates, environments, and conditions that separate one stage from the next. This chapter covers the transition from a basic CI workflow to a production-ready pipeline with build artifacts, environment protections, secrets, and deployment gates.

Key point: CI/CD in Actions is built on the needs keyword, the environment key, and the separation of jobs. Each stage is a job. The dependencies between jobs define the pipeline. Environment protection rules define who can deploy and when.


Why CI/CD requires more than a single job

The shared-state problem. A single job runs every step on the same runner. If the test step modifies files, the deployment step sees those modifications. If the build step caches artifacts locally, the deployment step uses them. This is convenient for small projects but creates hidden dependencies and makes it difficult to retry a failed stage without re-running the entire pipeline. Splitting into jobs isolates each stage on a fresh runner, eliminates shared state, and enables stage-level retries.

The artifact problem. A build artifactโ€”a compiled binary, a Docker image, a JavaScript bundleโ€”must move from the build job to the deployment job. Jobs do not share a filesystem. The actions/upload-artifact action stores files from the build job, and actions/download-artifact retrieves them in the deployment job. This is the mechanism that carries the output of one stage into the input of the next.

The environment problem. Production is not a test environment. Deploying to production requires credentials, approval, and often a waiting period. GitHub Environments are named targets (staging, production) with protection rules: required reviewers, wait timers, and deployment branch restrictions. A job that specifies environment: production cannot run until the rules are satisfied. This is the gate between CI and CD.

The secrets problem. Deployment credentialsโ€”API keys, SSH keys, cloud provider tokensโ€”must not be stored in the repository. GitHub Secrets are encrypted values stored at the repository, organization, or environment level. Environment-level secrets are only accessible to jobs that specify that environment. A production deployment job with environment: production can access production secrets; a test job cannot.

The trade-off. A multi-job pipeline is slower than a single job because each job waits for a runner. It is also more complex to configure. The trade-off is correctness: each stage is isolated, each failure is attributable to a specific job, and the production gate is enforced by the platform, not by convention.


a. Separating build, test, and deploy into jobs

A CI/CD pipeline is a sequence of jobs. Each job has a purpose: build the artifact, run the tests, deploy to staging, deploy to production. The needs keyword creates the sequence. The environment keyword attaches protection rules.

name: Pipeline

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm test

The build job produces the dist directory and uploads it as an artifact. The test job depends on build and re-runs npm ci because each job has a fresh filesystem. If the test job needs the built artifact, it uses actions/download-artifact instead of rebuilding.


b. Using environments for deployment gates

An environment is a named target with protection rules. The environment: key on a job attaches the job to that environment and enforces the rules before the job can start.

  deploy-staging:
    needs: test
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ./deploy.sh staging
        env:
          DEPLOY_TOKEN: ${{ secrets.STAGING_TOKEN }}

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ./deploy.sh production
        env:
          DEPLOY_TOKEN: ${{ secrets.PRODUCTION_TOKEN }}

The staging environment has no protection rulesโ€”the job runs immediately after test. The production environment has required reviewers, so the job pauses until an authorized user approves the deployment in the Actions tab. The secrets.PRODUCTION_TOKEN is only accessible to the job that specifies environment: production.


c. Conditions, concurrency, and caching

The if: condition controls whether a job or step runs. A deployment job should only run on the main branch, not on pull requests.

  deploy-production:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    needs: deploy-staging
    environment: production

The concurrency block prevents two deployments from running simultaneously. This is critical when a rapid sequence of merges triggers multiple pipeline runs.

concurrency:
  group: production-deploy
  cancel-in-progress: false

The cancel-in-progress: false setting ensures that a running deployment is not canceled by a new oneโ€”it waits for the current deployment to finish. For CI (test) jobs, cancel-in-progress: true cancels the older run to save resources.

Caching accelerates repeated installs. The actions/cache action stores dependency directories across runs, keyed by the lockfile hash.

      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}

The cache key includes the lockfile hash, so the cache is invalidated when dependencies change. Without the lockfile in the key, stale dependencies would be restored.


Complete Example Session

# ============================================
# PART 1: DEFINE THE PIPELINE
# ============================================

name: CI/CD Pipeline
on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
# ============================================
# PART 2: BUILD JOB
# ============================================

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
# ============================================
# PART 3: TEST JOB
# ============================================

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm test
# ============================================
# PART 4: STAGING DEPLOYMENT
# ============================================

  deploy-staging:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ./deploy.sh staging
        env:
          DEPLOY_TOKEN: ${{ secrets.STAGING_TOKEN }}
# ============================================
# PART 5: PRODUCTION DEPLOYMENT
# ============================================

  deploy-production:
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: ./deploy.sh production
        env:
          DEPLOY_TOKEN: ${{ secrets.PRODUCTION_TOKEN }}
# ============================================
# PART 6: CONFIGURE ENVIRONMENTS
# ============================================

# Settings โ†’ Environments โ†’ New environment: production
# Add required reviewers
# Add deployment branch restriction: main only
# ============================================
# PART 7: ADD SECRETS
# ============================================

# Settings โ†’ Secrets and variables โ†’ Actions
# Add STAGING_TOKEN and PRODUCTION_TOKEN
# For environment-specific: Settings โ†’ Environments โ†’ production โ†’ Secrets
# ============================================
# PART 8: VERIFY PIPELINE
# ============================================

# Push to main โ†’ pipeline runs
# build โ†’ test โ†’ deploy-staging โ†’ pause at deploy-production
# Approve in Actions tab โ†’ deploy-production runs
# ============================================
# PART 9: INSPECT LOGS
# ============================================

# Each job shows its own log.
# A failed stage can be re-run without re-running earlier stages.

The nine parts trace the pipeline: definition, build, test, staging, production, environment configuration, secrets, verification, and inspection.


Quick Reference

Pipeline Keywords

KeywordPurpose
needsJob dependency
environmentAttach protection rules
ifConditional execution
concurrencyPrevent parallel runs
uses: actions/upload-artifactStore files from build
uses: actions/download-artifactRetrieve in next job

Environment Protection

RuleEffect
Required reviewersJob pauses until approval
Wait timerJob waits N minutes
Deployment branchesOnly specified branches can deploy

Concurrency Settings

SettingEffect
groupUnique name for the concurrency group
cancel-in-progress: trueCancel older runs (CI)
cancel-in-progress: falseQueue runs (CD)

Best Practices

โœ… Do This:

if: github.ref == 'refs/heads/main'
<!-- Only deploy from main -->                       # โœ…
environment: production
<!-- Attach protection rules -->                     # โœ…
concurrency:
  group: production-deploy
  cancel-in-progress: false
<!-- Prevent concurrent deploys -->                 # โœ…

โŒ Don’t Do This:

# Deploy from pull requests
<!-- Allows unreviewed code into production -->      # โŒ
# Hardcode deployment tokens
<!-- Use environment secrets -->                     # โŒ
# Allow concurrent deployments
<!-- Two deploys can race and corrupt state -->       # โŒ

Common Pitfalls

PitfallWhy It HappensFix
Deployment runs on PRNo if conditionAdd if: github.ref == 'refs/heads/main'
Artifact not foundPath mismatchVerify upload-artifact path
Deployment secrets inaccessibleSecrets defined at repo level, not envAdd secrets under Environment settings
Two deploys raceNo concurrency blockAdd concurrency with cancel-in-progress: false
Environment not pausingNo required reviewersAdd protection rules in Settings

Real-World Examples

1. Build and Upload

- uses: actions/upload-artifact@v4
  with:
    name: dist
    path: dist/

2. Download in Deploy

- uses: actions/download-artifact@v4
  with:
    name: dist

3. Staging Environment

environment: staging

4. Production Gate

environment: production

5. Conditional Deploy

if: github.ref == 'refs/heads/main'

6. Concurrency Guard

concurrency:
  group: production
  cancel-in-progress: false

7. Cache Dependencies

uses: actions/cache@v4

8. Environment Secret

env:
  TOKEN: ${{ secrets.PRODUCTION_TOKEN }}

9. Job Dependency

needs: deploy-staging

10. Manual Approval

Settings โ†’ Environments โ†’ Required reviewers

Visual

Pipeline Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Push to main                                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  build                                       โ”‚
โ”‚  โ†’ upload-artifact: dist                     โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚ needs
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  test                                        โ”‚
โ”‚  โ†’ npm test                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚ needs + if
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  deploy-staging                              โ”‚
โ”‚  environment: staging                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚ needs
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  deploy-production                           โ”‚
โ”‚  environment: production (requires approval) โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Environment Gate

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  deploy-production job                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Environment: production                     โ”‚
โ”‚  โ”œโ”€ Required reviewer?                       โ”‚
โ”‚  โ”‚   โ”œโ”€ YES โ†’ PAUSE                          โ”‚
โ”‚  โ”‚   โ””โ”€ NO  โ†’ RUN                            โ”‚
โ”‚  โ”œโ”€ Wait timer?                              โ”‚
โ”‚  โ”‚   โ”œโ”€ YES โ†’ WAIT                           โ”‚
โ”‚  โ”‚   โ””โ”€ NO  โ†’ RUN                            โ”‚
โ”‚  โ””โ”€ Branch restriction?                      โ”‚
โ”‚      โ”œโ”€ main only โ†’ RUN if on main           โ”‚
โ”‚      โ””โ”€ else โ†’ BLOCK                         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The environment is the gate. Nothing reaches production until the rules are satisfied.


Summary

ItemValue
Pipeline stageJob
Job dependencyneeds
Artifact uploadactions/upload-artifact
Artifact downloadactions/download-artifact
Environment gateenvironment:
Required reviewersSettings โ†’ Environments
Concurrency guardconcurrency: group:
Conditional deployif: github.ref == 'refs/heads/main'

Key takeaways:

  • A CI/CD pipeline is a sequence of jobs. Each job runs on a fresh runner. The needs keyword establishes the sequence, and artifacts carry output from one stage to the next.
  • Environments enforce production gates. A job with environment: production cannot run until required reviewers approve and any wait timer expires. This is the line between continuous delivery and continuous deployment.
  • Environment secrets are scoped. Secrets defined under an environment are only accessible to jobs that specify that environment. A test job cannot read production credentials.
  • Concurrency prevents racing deploys. Two simultaneous deployments to the same environment can corrupt state. The concurrency block with cancel-in-progress: false serializes them.
  • if conditions gate deployments by branch. Deployments should not run on pull requests. The condition github.ref == 'refs/heads/main' ensures that only merged code reaches production.

Remember: CI/CD is not a single workflow fileโ€”it is a sequence of jobs with gates. The build job produces artifacts, the test job validates them, the staging job deploys automatically, and the production job waits for approval. The environment is the gate, the secrets are the credentials, and the concurrency block is the safety. Without these, the pipeline is just a sequence of commands that happen to run in order.

Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!