CI/CD Course Introduction and Prerequisites
This course picks up where the Git & GitHub course left off. You already know how to write a basic GitHub Actions workflow that checks out code and runs a command. From here, we build toward pipelines you could actually run in production: workflows that test, scan for vulnerabilities, build container images, publish them to a registry, deploy to real infrastructure, and roll back safely when something breaks.
Overview / How it works
“CI/CD” is often used as one phrase, but it names three distinct practices with different guarantees:
- Continuous Integration (CI) merges code changes frequently and verifies each merge automatically—compiling, running tests, and linting on every push or pull request. CI tells you whether the code is healthy; it does not, by itself, ship anything.
- Continuous Delivery extends CI by keeping the codebase in a state that is always releasable. Every change that passes CI produces a deployable artifact, but a human still decides when and whether to release it.
- Continuous Deployment goes one step further: every change that passes all automated checks is deployed to production automatically, with no manual approval gate.
This course teaches all three. You will see when a manual approval gate (delivery) is the right call versus when full automation (deployment) is safe, and how the same GitHub Actions workflow can support either model depending on how you configure environments and required reviewers.
We will work almost entirely in GitHub Actions, with Docker as the packaging format for the applications we build, test, and ship. Later lessons cover build caching, image scanning, registry publishing, deployment strategies, observability, and incident recovery.
Syntax or workflow structure
Every workflow you write in this course will follow the same overall shape, expanded over time: a trigger (on:), an explicit permissions: block, and a sequence of dependent jobs. The default GITHUB_TOKEN permissions granted to a workflow are broader than most jobs need, so from lesson one we set permissions explicitly per job rather than relying on repository defaults. This is not optional hardening—it is the baseline we build on.
The skeleton below previews where each part of the course fits. You are not expected to understand every line yet; later lessons implement each job in depth.
name: Production Pipeline Preview
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
run: echo "test suite runs here"
security-scan:
needs: build-and-test
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Scan dependencies
run: echo "dependency and container scanning runs here"
publish:
needs: security-scan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Build and push image
run: echo "image build and push runs here"
deploy:
needs: publish
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
permissions:
contents: read
steps:
- name: Deploy to production
run: echo "deployment with health checks runs here"
Notice the shape: security-scan only runs after build-and-test succeeds, publish only after the scan passes, and deploy only after publish—and only on main. Each job requests only the permissions it actually uses. This ordering (test, then secure, then publish, then deploy) is the backbone of every pipeline in this course.
Examples
Before writing any pipeline code, confirm your local environment matches what the course assumes. You need Docker, Git, the GitHub CLI, and a runtime for the sample application (Node.js in most examples, though the concepts transfer to any language).
docker --version
git --version
gh --version
node --version
gh auth status
Expected behavior: each command prints a version number. gh auth status reports that you are logged in to github.com with a token that has at least repo and workflow scopes. If gh auth status reports you are not logged in, run gh auth login and follow the interactive prompts before continuing—several later lessons use the CLI to inspect workflow runs and manage environments.
Next, check what default permissions your repository grants to Actions. This setting, not the workflow file, is often the first thing that surprises people moving from a personal project to a team repository.
gh api repos/OWNER/REPO/actions/permissions/workflow
Expected behavior: a JSON response containing a default_workflow_permissions field, set to either read or write, and a can_approve_pull_request_reviews boolean. Replace OWNER/REPO with your own repository; if the response is 404, you either mistyped the path or don’t have admin access to that repository. Record the current value—you’ll compare it against the explicit permissions: blocks we write in every workflow going forward.
Step by step
- Install Docker Desktop or Docker Engine and confirm the daemon is running with
docker info. - Confirm Git and the GitHub CLI are installed and on your
PATH. - Authenticate the CLI with
gh auth login, choosing a token scope that includesworkflowso you can push workflow file changes. - Create or choose a GitHub repository you can administer, since later lessons configure branch protection, environments, and repository secrets.
- Check the repository’s default Actions workflow permissions using the
gh apicommand above, and note it—this course will have you tighten it explicitly rather than trust the default. - Skim your existing workflow file from the Git & GitHub course (typically
.github/workflows/*.yml) and identify whether it currently sets apermissions:block. Most first workflows don’t—that’s the gap this course closes.
Common Mistakes
Mistake 1: Assuming the default GITHUB_TOKEN permissions are safe. A workflow with no permissions: block inherits the repository’s default, which on many repositories is broad read/write access to contents, issues, and pull requests. A compromised or overly permissive third-party action can then do far more damage than intended.
# Mistake: no explicit permissions, workflow inherits repository default
name: CI
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
# Correction: request only what this job needs
name: CI
on: push
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
Mistake 2: Referencing third-party actions by a mutable tag like @main or @v4 without understanding the trade-off. A floating tag can be moved by the action’s maintainer—or by an attacker who compromises their account—to point at malicious code, and your pipeline would run it on the next trigger without any change to your own repository.
# Risky: tag can be repointed at any time by the action owner
- uses: some-org/some-action@main
# Safer: pin to a specific release tag, or better, a commit SHA
- uses: some-org/some-action@v3.2.1
# or, for maximum immutability:
- uses: some-org/some-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Pinning to a commit SHA is the strongest guarantee but means you won’t automatically receive security patches from the action—you must update the pin deliberately. Official actions published by GitHub itself (like actions/checkout) are lower risk to pin loosely; third-party actions from unfamiliar publishers deserve the SHA pin, especially if they run on workflows with write access to secrets.
Best Practices
- Set an explicit, minimal
permissions:block on every workflow and, where jobs differ, on every job. - Treat pull requests from forks as untrusted input. Never run fork PR code with access to repository secrets; avoid
pull_request_targetunless you fully understand it only checks out trusted base-branch code by default. - Never print, echo, or interpolate a secret into a log or command. Reference secrets only through
${{ secrets.NAME }}in contexts designed to receive them (likeenv:or action inputs). - Prefer image digests over mutable tags when deploying containers; a digest always resolves to the exact bytes that were scanned and tested.
- Gate deployment jobs behind passing tests and security scans, and use GitHub Environments with required reviewers for anything deploying to production.
- Remember every workflow example in this course is a template. Hostnames, registry paths, cloud credentials, and secret names must be replaced with your own infrastructure’s real values, configured through your platform’s secret store.
Practice Exercises
- Run the four version-check commands and
gh auth statuslocally. If any tool is missing, install it before the next lesson. - Use
gh apito check your test repository’s default Actions workflow permissions, and write down whether it is currentlyreadorwrite. - Open your existing workflow file from the Git & GitHub course and add an explicit
permissions: contents: readblock at the top level if one is missing. - Find one action reference in that workflow file (for example
actions/checkout@v4) and decide, based on this lesson’s guidance, whether it should stay on a version tag or be pinned to a commit SHA. Explain your reasoning in a comment.
Summary
CI, continuous delivery, and continuous deployment are related but distinct: CI verifies changes, delivery keeps them always releasable, and deployment ships them automatically. This course builds a full pipeline across those stages using GitHub Actions and Docker, starting from the explicit-permissions and pinned-action habits introduced here. Before the next lesson, make sure Docker, Git, and the GitHub CLI are installed and authenticated, and that you know your test repository’s current default Actions permissions—you’ll be replacing that default with explicit, minimal grants in every workflow from here on.
