A mature CI/CD pipeline goes beyond “run tests and deploy” — it enforces code quality standards automatically, manages secrets safely across environments, uses branch strategies to control what reaches production, and enables the team to move fast with confidence. This lesson covers the advanced pipeline features that separate a professional CI/CD setup from a basic one: branch protection rules, environment promotion strategies, secrets scoping, dependency update automation, and the complete pipeline visualised as a development workflow.
Branch Strategy and Pipeline Mapping
| Branch | Purpose | Pipeline Trigger | Deploys To |
|---|---|---|---|
feature/xyz |
Feature development | CI (lint + test) on push and PR | Nowhere (or preview env) |
develop |
Integration branch — all features merged here | CI + auto-deploy on push | Staging (automatic) |
main |
Production-ready code — only merged from develop via PR | CI + deploy on push | Production (with approval) |
hotfix/xyz |
Emergency production fix | CI on push, manual deploy | Production (expedited) |
v1.2.3 (tag) |
Release tag | Docker build + push + release notes | Production (version tag) |
Secrets Scoping Strategy
| Secret Type | Scope | Example |
|---|---|---|
| Repository secret | All workflows in the repo | NPM_TOKEN, SNYK_TOKEN |
| Environment secret | Only workflows targeting that environment | production.JWT_SECRET, staging.MONGO_URI |
| Organisation secret | All repos in the organisation | SLACK_BOT_TOKEN, DOCKERHUB_TOKEN |
| Variable (not secret) | Repository or environment — not encrypted | API_BASE_URL, SENTRY_DSN |
.github/dependabot.yml) to automatically open PRs for dependency updates. With CI running on every PR and auto-merge configured for patch updates that pass CI, your dependencies stay up-to-date with minimal manual effort. Set open-pull-requests-limit: 10 to avoid being overwhelmed, and group updates with groups: to batch minor updates into single PRs.Complete Advanced Pipeline Configuration
# .github/workflows/pr-checks.yml — PR quality gate with auto-assignment
name: PR Checks
on:
pull_request:
branches: [main, develop]
pull_request_review:
types: [submitted]
jobs:
size-check:
name: PR Size Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Check PR size
uses: actions/github-script@v7
with:
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
const additions = pr.additions;
const deletions = pr.deletions;
const total = additions + deletions;
let label = total > 500 ? 'size/xl' :
total > 200 ? 'size/large' :
total > 50 ? 'size/medium' : 'size/small';
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number:context.issue.number,
labels: [label],
});
if (total > 800) {
core.warning(`This PR changes ${total} lines. Consider breaking it into smaller PRs.`);
}
# ── Dependency updates with Dependabot ────────────────────────────────
# .github/dependabot.yml
# version: 2
# updates:
# - package-ecosystem: npm
# directory: /api
# schedule: { interval: weekly }
# groups:
# prod-deps:
# patterns: ['*']
# update-types: ['minor', 'patch']
# - package-ecosystem: npm
# directory: /client
# schedule: { interval: weekly }
# - package-ecosystem: docker
# directory: /
# schedule: { interval: monthly }
# ── Auto-merge Dependabot patch/minor PRs that pass CI ────────────────
auto-merge-dependabot:
name: Auto-merge Dependabot
runs-on: ubuntu-latest
if: |
github.actor == 'dependabot[bot]' &&
github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Auto-merge patch and minor updates
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
// Only auto-merge minor and patch updates
const updateType = pr.title.match(/bump .+ from .+ to/i);
if (!updateType) return;
if (pr.title.includes('major')) return; // skip major updates
await github.rest.pulls.merge({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
merge_method:'squash',
});
# ── Release workflow ───────────────────────────────────────────────────────
# .github/workflows/release.yml
name: Release
on:
push:
tags: ['v*.*.*']
jobs:
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Generate changelog
id: changelog
uses: orhun/git-cliff-action@v3
with:
config: cliff.toml
args: --latest --strip header
- name: Create release
uses: actions/github-script@v7
with:
script: |
github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: context.ref.replace('refs/tags/', ''),
name: context.ref.replace('refs/tags/', ''),
body: `${{ steps.changelog.outputs.content }}`,
draft: false,
prerelease: context.ref.includes('-rc') || context.ref.includes('-beta'),
});
# .github/dependabot.yml — Automated dependency updates
version: 2
updates:
# API dependencies
- package-ecosystem: npm
directory: /api
schedule:
interval: weekly
day: monday
time: "09:00"
timezone: "Europe/London"
open-pull-requests-limit: 10
groups:
api-prod-deps:
patterns: ['*']
update-types: ['minor', 'patch']
api-dev-deps:
patterns: ['*']
dependency-type: development
update-types: ['minor', 'patch']
commit-message:
prefix: 'chore(api)'
# Angular dependencies
- package-ecosystem: npm
directory: /client
schedule:
interval: weekly
day: monday
groups:
angular-core:
patterns: ['@angular/*']
client-deps:
patterns: ['*']
commit-message:
prefix: 'chore(client)'
# Docker base images
- package-ecosystem: docker
directory: /api
schedule:
interval: monthly
commit-message:
prefix: 'chore(docker)'
# GitHub Actions
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
commit-message:
prefix: 'chore(ci)'
How It Works
Step 1 — Branch Protection Rules Make CI Non-Bypassable
Branch protection rules configured in GitHub Settings enforce that every PR to main must pass all required status checks before merging — regardless of who created the PR, even the repository owner. Required status checks list specific job names from CI workflows. If any listed check fails or is skipped, the merge button is disabled. This closes the loophole of pushing directly to main to bypass CI.
Step 2 — Dependabot PRs Keep Dependencies Fresh Without Toil
Dependabot scans dependency manifests on the configured schedule, compares installed versions against the registry, and opens PRs for outdated packages. Each PR includes a changelog summary from the package’s release notes. With CI running on every PR, you can verify that a dependency update does not break anything before merging. Grouping related updates (all Angular packages together) reduces PR noise.
Step 3 — Auto-merge Eliminates Manual Approval for Safe Updates
Patch and minor updates are generally safe — they fix bugs and add backward-compatible features. Automating merges for these (after CI passes) eliminates the manual effort of reviewing and approving dozens of small dependency update PRs. Major version updates (which may have breaking changes) are excluded from auto-merge and require human review. This keeps dependencies current with minimal effort while maintaining oversight for risky changes.
Step 4 — Conventional Commits Enable Automated Changelogs
Tools like git-cliff or conventional-changelog parse commit messages formatted as type(scope): description (feat(tasks): add due date filtering, fix(auth): handle expired refresh token) to automatically generate changelogs. The release workflow generates the changelog from commits since the last tag and creates a GitHub Release with it. This removes manual changelog maintenance while producing meaningful, user-readable release notes.
Step 5 — Environment Secrets Prevent Secret Cross-Contamination
Staging and production have different secrets: different MongoDB URIs, different JWT secrets, different payment API keys. By storing each environment’s secrets under the corresponding GitHub Environment (not as repo-level secrets), you prevent a staging workflow from accidentally using production credentials — and prevent production credentials from being accessible in staging workflow logs. A deployment job only receives secrets from its targeted environment.
Common Mistakes
Mistake 1 — No branch protection rules — CI can be bypassed
❌ Wrong — developers can push directly to main without CI:
git push origin main # bypasses CI entirely without branch protection
✅ Correct — configure branch protection in GitHub Settings:
# Settings → Branches → Add rule:
# Branch name pattern: main
# ✅ Require status checks: lint, test-api, test-angular
# ✅ Require branches to be up to date
# ✅ Include administrators (nobody bypasses)
Mistake 2 — Using repo-level secrets for environment-specific credentials
❌ Wrong — staging workflow can access production JWT secret:
# Repository secret: JWT_SECRET (same for staging and production)
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }} # same value in both environments!
✅ Correct — environment secrets with different values per env:
# staging environment secret: JWT_SECRET = staging-value
# production environment secret: JWT_SECRET = production-value
environment: production
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }} # resolves to production value only in production job
Mistake 3 — Ignoring major dependency updates from Dependabot
❌ Wrong — major version PRs pile up, never merged, dependencies fall years behind:
# .github/dependabot.yml:
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major'] # all major updates ignored forever!
✅ Correct — schedule regular major version review sprints, don’t permanently ignore:
# Monthly team process: review and address major version Dependabot PRs
# Do not auto-merge, but do not permanently ignore — migrate periodically
Quick Reference
| Task | Where / How |
|---|---|
| Require CI before merge | Settings → Branches → Require status checks |
| Prevent force push to main | Settings → Branches → Do not allow force pushes |
| Dependabot config | .github/dependabot.yml |
| Auto-merge Dependabot | github.rest.pulls.merge() in github-script |
| Env-scoped secrets | Settings → Environments → [env name] → Add secret |
| Release changelog | orhun/git-cliff-action on tag push |
| PR size labelling | github-script checking pr.additions + pr.deletions |
| Hotfix deploy | workflow_dispatch with image-tag input targeting production |