Continuous integration (CI) means automatically checking every change pushed to the repository: installation, static analysis, tests. Continuous deployment (CD) extends the chain: a validated change is delivered automatically, to staging and then to production. The whole forms a pipeline. The goal is not speed for its own sake, but confidence: a problem is detected minutes after it is introduced, by a machine that never tires of rerunning the tests.
In this course, we use GitHub Actions, built into GitHub. The concepts (trigger, job, step, artifact, environment, secret) are found almost identically in GitLab CI, Azure Pipelines or Jenkins.
A first workflow
A workflow is a YAML file placed in the repository’s .github/workflows/ folder. The file name is free (ci.yml, deploy.yml).
# .github/workflows/ci.yml
name: CI # name displayed in the Actions tab
on: # when the workflow is triggered
push:
branches: [main]
pull_request:
jobs:
test: # job identifier
runs-on: ubuntu-latest # virtual machine provided by GitHub
steps:
- uses: actions/checkout@v4 # fetches the repository code
- uses: actions/setup-node@v4 # installs Node.js
with:
node-version: 22
- run: npm ci # shell command
- run: npm test
Let’s read it from top to bottom:
onlists the triggers: here, every push tomainand every pull request.jobscontains one or more jobs. Each job starts on a fresh runner, a blank machine: nothing is kept from one job to another, except what you explicitly pass along (artifacts, outputs).stepsis the ordered list of steps. A step does eitheruses:(a reusable action, published in a repository, with its version after@), orrun:(a shell command, run by bash on a Linux runner).with:passes parameters to an action.
If a step fails (exit code other than 0), the following steps are skipped and the job is marked as failed. That is exactly what we want: a failing npm test turns the pipeline red.
Triggers
on:
push:
branches: [main]
paths-ignore: ["docs/**", "**.md"] # don't rerun for docs
pull_request:
branches: [main] # PRs targeting main
workflow_dispatch: # "Run workflow" button
inputs:
environnement:
type: choice
options: [staging, production]
schedule:
- cron: "0 3 * * 1" # every Monday at 3:00 UTC
push: on every push, filterable bybranches,tagsandpaths.pull_request: when a pull request is opened and on each new commit. The code tested is the PR’s code merged with the target branch.workflow_dispatch: manual launch from the Actions tab, with parameters (inputs) read via${{ inputs.environnement }}.schedule: scheduled execution, in cron format, in UTC.
Chaining jobs with needs
By default, a workflow’s jobs run in parallel. The needs keyword creates a dependency:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
build:
needs: [lint, test] # waits for lint AND test to succeed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
lint and test start together; build only starts if both are green. If one fails, build is marked “skipped”. The repository’s Actions tab displays this graph, each job’s status and each step’s log, which you expand to read the error.
Making CI mandatory
A red pipeline prevents nothing as long as you can merge despite it. In Settings > Branches (protection rule) or Settings > Rules > Rulesets, you protect main:
- require a pull request before merging (no more direct pushes);
- require the selected status checks to be green, for example the
lintandtestjobs; - optionally require the branch to be up to date with
main.
The names offered for the checks are those of the jobs: choose stable names, because renaming a required job blocks pull requests until the rule is updated.
Good habits from the start
- Keep the pipeline fast (a few minutes): otherwise people stop waiting for its verdict.
- Use
npm ci(orpip install -r requirements.txt,composer install): reproducible installation from the lock file. - Add
timeout-minutesto a job that may get stuck. - Check the syntax locally with a workflow linter such as
actionlintbefore pushing.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
First green workflow
In a GitHub repository containing a Node.js project with a
testscript inpackage.json, create the file.github/workflows/ci.ymlwithname: CI, the triggerspushonmainandpull_request, and a job namedtestonubuntu-latestthat runsactions/checkout@v4,actions/setup-node@v4(Node 22),npm cithennpm test. Push tomain.- The file is located exactly at `.github/workflows/ci.yml`
- The Actions tab shows a run of the "CI" workflow triggered by the push
- The `test` job contains the steps checkout, setup-node, `npm ci` and `npm test`
- The run ends with a green status (Success)
Chain lint, test and build
Modify
ci.ymlto get three jobs:lint(npm run lint),test(npm test) andbuild(npm run build) withneeds: [lint, test]. Then deliberately introduce a lint error in a branchessai-lint, open a pull request tomain, observe, then fix it.- On main, the run graph shows `lint` and `test` in parallel then `build`
- On the pull request with the error, `lint` is red and `build` is marked "skipped"
- After the fix and a new push to the branch, the three jobs are green
- No step is needlessly duplicated between jobs apart from checkout, setup-node and `npm ci`
Make CI mandatory on main
In the repository settings, create a protection rule (or a ruleset) for
mainthat requires a pull request and thelintandteststatus checks. Verify that a pull request whosetestfails cannot be merged.- The rule targets the `main` branch
- The required checks are exactly `lint` and `test`
- On a pull request with a broken test, the merge button is disabled with a note that required checks failed
- A direct push to main is rejected (or subject to the pull request)
Job execution order
A workflow defines three jobs
a,bandc, with noneedsat all. How do they run?- In the order a, b, c
- In parallel, each on its own runner
- On the same runner, sharing files
End-of-lesson quiz
Where must a GitHub Actions workflow file be located?
- At the root of the repository
- In .github/workflows/
- In .actions/
What is the difference between
usesandrunin a step?- None
- uses calls a reusable action, run executes a shell command
- run calls an action, uses a command
Which trigger adds a manual launch button?
- push
- workflow_dispatch
- schedule
If the
testjob fails, what happens to abuildjob that hasneeds: test?- It runs anyway
- It is skipped
- It is rerun automatically
The rest of the course
- Tests and Quality in CI20 min
- Artifacts, Job Outputs and Releases15 min
- Automated Deployment and Environments20 min
- Secrets and Pipeline Security20 min