At the close of module 3 it was written down that this lesson dissects the full anatomy of a pipeline — its stages, how they are orchestrated, what is parallelised, what can be skipped and how to design one that stays fast once the team has tripled in size. That is exactly today's work, and it arrives at the right moment: Reservalia has four workflows — ci.yml, cd.yml, infra.yml and rollback.yml — built piece by piece over two modules, without anybody ever having sat down to look at them as a single system. We will fix the precise vocabulary, walk through the canonical stages and the invariant each one guarantees, see why their order is not arbitrary, draw Reservalia's real graph end to end, understand why every run starts from a clean machine and how information is passed between jobs, design the quality gate and finish with the anti-patterns that turn a pipeline into a problem. What we will not do here is optimise timings — that is 04-04 — or extract reusable templates — that is 04-05: today is about understanding the shape before touching it.
Contents
- Precise vocabulary: what exactly each piece is
- The canonical stages and each one's invariant
- Why the order is not arbitrary: fail fast
- The orchestration models: sequential, graph and fan-out/fan-in
- Reservalia's complete graph
- Ephemeral pipelines versus long-lived pipelines
- Passing information between jobs
- The quality gate: block, inform or wait
- Four pipelines, not one
- Design anti-patterns
- Common Mistakes and Tips
- Exercises
- Conclusion
- Precise vocabulary: what exactly each piece is
Up to now we have used these words somewhat loosely because the context made them clear. From this module on, precision matters: when we extract templates in 04-05 or measure timings in 04-04, "job" and "step" will stop being interchangeable.
| Term | Definition | At Reservalia |
|---|---|---|
| Pipeline | A change's complete journey from commit to production | ci.yml → cd.yml, chained |
| Workflow | A YAML file with its trigger and its own graph of jobs | ci.yml, cd.yml, infra.yml, rollback.yml |
| Job | An isolated unit of execution: its own machine, its own disk | quality, test, build, publish |
| Step | A command or action inside a job; it shares a disk with its siblings | npm ci, npm run lint |
| Stage | A logical concept: a group of jobs with the same purpose | "verification", "packaging", "deployment" |
| Trigger | The event that starts a workflow | pull_request, push, workflow_run, workflow_dispatch |
| Gate | A point where the pipeline stops until a condition is met | prod environment approval |
| Artifact | An output that outlives the machine that produced it | reservalia/api:a3f9c21 in ECR |
| Runner | The machine that executes a job | ubuntu-22.04 |
Two distinctions cause more confusion than you would think. A job and a step are not the same thing, and the difference is physical. Two steps of the same job run on the same machine and share a filesystem: that is why npm ci in one step leaves node_modules/ available for the next. Two jobs run on different, clean machines, possibly at the same time, and share nothing except what is explicitly transferred. That is why in ci.yml each of the four jobs repeats its own npm ci: it is not an oversight, it is a consequence of the isolation.
"Stage" is a borrowed concept. GitLab CI and Jenkins have stages as a keyword, with the semantics "all the jobs of stage N finish before N+1 begins". GitHub Actions has no stages: it has a graph declared with needs. When we say "stage" we will mean the logical grouping, not a piece of syntax; the exact equivalence is covered in module 6.
- The canonical stages and each one's invariant
Almost any pipeline in the world, in any tool, is a variation of the same seven-stage sequence. The important thing is not to memorise them, but to understand what each one asserts: a stage that passes is a concrete promise about the change, and if you cannot write that promise in one sentence, that stage is probably surplus.
| Stage | Question it answers | Invariant it guarantees when it passes | At Reservalia |
|---|---|---|---|
| Validate | Is it well written? | It matches the format and has no lint or type errors | quality job |
| Test | Does it do what it should? | The observable behaviour is the expected one | test job |
| Build | Does it compile? | There is a dist/ reproducible from this commit |
npm run build in build |
| Package | Is it portable? | There is a self-contained image that starts without the machine that created it | docker build with apps/api/Dockerfile |
| Publish | Does it exist outside the runner? | The artifact is immutable and addressable by digest | publish job → ECR |
| Deploy | Is it running? | The environment declares that digest and no other | cd.yml over ECS |
| Verify | Does it actually work? | The service responds and serves the expected version | Smoke test against /version |
Three observations about the table. Build and package are usually merged into a single job — at Reservalia both live in build — but they are different invariants: the first says the code compiles, the second that the result runs on a machine that is not yours. Publish is the immutability frontier: before that stage everything is reproducible but volatile; afterwards there is an object with a cryptographic name that every environment will point at. And verify is not optional: without it, "deploy" only asserts that an AWS command did not error, which is exactly the diagnosis from lesson 03-02.
- Why the order is not arbitrary: fail fast
The question that orders a pipeline is a single one: if this change is going to fail, how do I make it fail as early as possible? From that comes the fail fast principle, with two criteria that sometimes conflict. Criterion 1: cheapest first. A 25-second tsc --noEmit before a three-minute integration suite, and that before a full deployment: it costs less to discard the change. Criterion 2: what fails most, first. If half the broken PRs are broken because of a business test, that test should run early even if it is expensive; a cheap stage that never catches anything only adds latency.
In a graph the conflict almost disappears, because everything that can run at the same time does, and the criteria only order things within each job. This is the order of the steps in quality, and it is not accidental:
steps:
- run: npx prettier --check . # 1 · ~5 s · formatting
- run: npm run lint # 2 · ~20 s · real errors
- run: npm run typecheck # 3 · ~25 s · the most expensive of the threeFormatting first because it is instantaneous and its failure is fixed with one command (npx prettier --write .); types last because it is the slowest. If a PR brings all three problems, Diego sees the cheapest one in five seconds. And there is a cost asymmetry that justifies the whole design: the price of catching a failure multiplies by ten with every stage it advances. A type error caught in quality costs a minute; the same error caught in prod costs an incident, a rollback, a business that could not accept bookings and a row in the incidents table.
The 10-minute rule that the team set in 02-01 deserves an exact formulation: the time from pushing to knowing whether your change is acceptable must not exceed 10 minutes. It is not a magic number, it is the threshold above which people go and do something else while they wait, lose context and come back half an hour later. All pipeline design is, at bottom, the management of that budget, and today Reservalia meets it comfortably: 50 s of validation, 3 min of tests, 4 min of building — the only one starting to squeeze — and 2 min of publishing, in parallel except for the last one.
- The orchestration models: sequential, graph and fan-out/fan-in
How the jobs are ordered relative to each other determines the total time and the quality of the signal.
Sequential. Each job waits for the previous one. It is what you get by default when somebody chains needs without thinking:
jobs:
quality: { runs-on: ubuntu-22.04 }
test: { needs: quality, runs-on: ubuntu-22.04 }
build: { needs: test, runs-on: ubuntu-22.04 }Total time = 50 s + 3 min + 4 min = 7 min 50 s. Real advantage: if quality fails, no runner minutes are spent on test or build. Drawback: if build is broken, the developer takes almost eight minutes to find out.
Dependency graph (DAG). Each job declares only what it genuinely depends on, and the engine runs in parallel everything it can. It is what ci.yml does:
jobs:
quality: { runs-on: ubuntu-22.04 } # no needs → starts right away
test: { runs-on: ubuntu-22.04 } # no needs → starts right away
build: { runs-on: ubuntu-22.04 } # no needs → starts right away
publish: { needs: [quality, test, build] } # waits for all threeTotal time = max(50 s, 3 min, 4 min) + 2 min = 6 min. And something more valuable than the minute and a half saved: if the PR has a lint failure and a broken test, both are visible in the same run, instead of fixing one, waiting and discovering the other.
Fan-out / fan-in. It is a pattern within the graph, not a separate model: fan-out is a point that opens up into several parallel jobs, fan-in the point where they converge. In ci.yml the fan-out happens at the trigger and the fan-in at publish. The automatic form of fan-out is matrix:
test:
strategy:
matrix:
shard: [1, 2, 3] # 1 · three real jobs, not three steps
fail-fast: false # 2 · one failure should not cancel the siblings
steps:
- run: npm run test:unit -- --shard=${{ matrix.shard }}/3matrixgenerates jobs, with everything that implies: different machines,npm cirepeated on each and no shared files.fail-fast: falseis almost always what you want for tests: you would rather know that shards 1 and 3 fail than have everything cancelled at the first failure.
Conditional jobs. The third shaping mechanism is if:, which decides whether a job exists in this run. publish carries if: github.ref == 'refs/heads/main'; the jobs in cd.yml carry if: github.event.workflow_run.conclusion == 'success'. A job skipped by if does not count as a failure, and that property is what underpins the selective execution in 04-04.
| Model | Total time | Cost in minutes | Signal quality | When to use it |
|---|---|---|---|---|
| Sequential | The sum of all | Minimum | Partial: only the first failure | Very expensive jobs with a real dependency |
| Graph (DAG) | The longest path | Higher: you spend on jobs that will fail | Complete | By default |
| Fan-out/fan-in | The slowest of the fan | The highest of all | Complete and granular | Long, divisible suites |
The practical conclusion: parallelise unless there is a real data dependency. One needs too many is a decision that costs minutes every day to every person on the team.
- Reservalia's complete graph
The four workflows have never been drawn together. This is the real pipeline, from commit to production:
flowchart TD
subgraph CI["ci.yml · pull_request and push to main"]
Q["quality ~50 s"] --> P["publish ~2 min<br/>main only"]
T["test ~3 min"] --> P
B["build ~4 min"] --> P
end
subgraph CD["cd.yml · workflow_run if conclusion == success"]
D1["deploy dev"] --> S1["smoke /version"]
S1 --> D2["deploy staging"]
D2 --> G["gate: approval<br/>prod environment"]
G --> C["canary 10 → 50 → 100%"]
C --> R["record in deployments"]
end
subgraph OTHERS["On demand"]
I["infra.yml<br/>plan → apply"]
RB["rollback.yml<br/>workflow_dispatch"]
end
P ==>|"digest of a3f9c21"| D1
R -.->|"DORA metrics"| Q
C -.->|"circuit breaker"| RB
Three properties of this graph are design decisions, not accidents. The link between ci.yml and cd.yml is a workflow_run, not a needs. needs only works inside a workflow. Splitting them into two files has a concrete reason: ci.yml runs on every pull request and must not have permissions over AWS; cd.yml runs only on main and does have them. The boundary between the two files is a privilege boundary, and that is the material of 04-03.
What travels along the thick arrow is a digest, not source code. The "build once, promote the same digest" principle takes concrete form here: cd.yml never rebuilds anything. infra.yml and rollback.yml hang outside the chain deliberately. They are not part of the path of a code change: one is triggered when infra/ changes, the other when a person decides to go back. A pipeline is not a single thread, it is a graph with entry points in several places.
- Ephemeral pipelines versus long-lived pipelines
A decision almost nobody makes consciously because modern tools have already made it for you: every run starts from a clean machine that is destroyed when it finishes.
| Ephemeral (Actions, GitLab SaaS) | Long-lived (classic Jenkins) | |
|---|---|---|
| Initial state | A new machine, a known image | Whatever the previous run left behind |
| System dependencies | Declared or installed | Installed by hand years ago |
| Reproducibility | High: two identical runs give the same result | Low: it depends on the agent's history |
| Typical failure | "X needs installing" from cold | "It fails on agent 3 and not on agent 4" |
The three arguments in favour of ephemeral are compelling. A green build is only worth something if it can be repeated: if a job passes because somebody installed a tool on that agent in 2021, you have not verified your code, you have verified that agent. A container left running, a temporary file or an exported variable contaminates the next run, and produces the worst possible failure: intermittent and dependent on which machine you land on. And on security, a reused runner that executed code from a fork's PR may have left something waiting for the next job, which may well have credentials. The price is the cold start: installing dependencies on every run. The right answer is not to preserve state, but to cache verifiable inputs — dependencies identified by the lockfile hash, Docker layers by their digest — which is rebuilding from something whose identity is checked. That nuance is the difference between a cache and a dirty agent, and it is developed in 04-04.
- Passing information between jobs
Since jobs do not share a disk, any data crossing the boundary must be transferred explicitly. There are four mechanisms, and choosing badly produces slow or broken pipelines.
| Mechanism | What it carries | Size | Persistence | Typical use |
|---|---|---|---|---|
outputs |
Short strings | Bytes | The run | A SHA, a digest, a boolean |
| Run artifacts | Files | Up to GB | Days (retention) | dist/, reports, screenshots |
| External registry | Images, packages | GB | Months or years | ECR, npm |
| Variables and secrets | Configuration | Bytes | Permanent | AWS_REGION, role ARNs |
build:
outputs:
digest: ${{ steps.image.outputs.digest }} # 1 · exposed to the graph
steps:
- id: image
uses: docker/build-push-action@v5
with: { context: ., file: apps/api/Dockerfile, push: true }
- uses: actions/upload-artifact@v4 # 2 · files, not strings
with: { name: web-dist, path: apps/web/dist, retention-days: 7 }
publish:
needs: [build]
steps:
- run: echo "Promoting ${{ needs.build.outputs.digest }}" # 1- A job
outputis declared at job level and is fed from theoutputof a step with anid: it is the right channel for the digest, a small piece of identity data. It is read withneeds.<job>.outputs.<name>, which implies a real dependency: you can only read the output of a job that is in yourneeds. - Artifacts are for files, and
retention-daysmatters: the default is 90 days, and in an active repository that means billable gigabytes ofdist/nobody will ever open again.
The practical rule: short strings through outputs, files through artifacts, and anything that must outlive the workflow into an external registry. Uploading a Docker image as a run artifact in order to "pass it" to the next job is an expensive anti-pattern: hundreds of megabytes up and down versus a docker pull from ECR, which also leaves the artifact where it belongs.
- The quality gate: block, inform or wait
Not everything a pipeline runs should have veto rights. Confusing these categories produces either a fragile main or a team that learns to work around the rules. And there are two distinct gates, with different criteria: the one that blocks the merge and the one that blocks the deployment.
| Check | Blocks the merge? | Blocks the deployment? | Effect if it fails |
|---|---|---|---|
quality (formatting, lint, types) |
Yes | Yes, indirectly | Merge button disabled |
test (unit + integration) |
Yes | Yes, indirectly | Merge button disabled |
build (compile + image) |
Yes | Yes, indirectly | Merge button disabled |
| SonarQube quality gate (new code) | Yes | No | Merge button disabled |
| Coverage or image size outside threshold | No | No | Comment on the PR |
Smoke test against /version |
— | Yes | Red deployment, rollback |
| Canary metrics outside threshold | — | Yes | The promotion stops |
prod environment approval |
— | Waits | Paused until Marta or Nuria approve |
| Full E2E suite (nightly) | No | No | Ticket the following morning |
The criterion for classifying is a single question: does a failure here always mean the code is wrong? If the answer admits an "it depends", it is not blocking. An E2E test that fails 3% of the time because of a network problem does not meet the criterion, and making it blocking teaches the team that "red sometimes does not matter", which destroys the value of every other check. And a job can be informative in two ways, which are not equivalent:
coverage:
continue-on-error: true # the job goes amber, the workflow stays green
steps:
- run: npm run test -- --coverage --coverage.thresholds.lines=70continue-on-error: true leaves the job marked but does not bring the workflow down. The alternative — simply not including it in the required checks of the branch protection rule — is cleaner, because red stays red and merely stops blocking. Prefer the second: continue-on-error on a check that should block is a silent way of disabling a rule without anybody noticing.
- Four pipelines, not one
A frequent mistake is to put everything into a single workflow that fires all the time. The result is twenty-minute PRs with verifications that only make sense once a day.
| Pull request | main |
Nightly | On demand | |
|---|---|---|---|---|
| Trigger | pull_request |
push to main |
schedule: cron |
workflow_dispatch |
| Goal | Is it safe to merge? | Produce and deploy | The slow and the non-blocking | One-off operations |
| Budget | < 10 min | < 20 min | No practical limit | Whatever it takes |
| E2E | Only the 5 critical ones | The 5 critical ones | All of them | — |
| Image and deployment | Built, not published | Published and deployed up to prod |
— | rollback.yml, infra.yml |
| Security scanning | Fast | Fast | Deep (04-03) | — |
The underlying logic is a trade-off between latency and coverage. In the PR you pay in latency: every minute is suffered by a person waiting, so only what answers "is it safe to merge?" gets in. In the nightly run latency is free — nobody is waiting — and a forty-minute E2E suite and a full scan both fit.
# .github/workflows/nightly.yml — Reservalia's fifth workflow
name: Nightly
on:
schedule: [{ cron: '0 2 * * 1-5' }] # cron is interpreted in UTC, not the repo's zone
workflow_dispatch: # and by hand, to reproduce without waiting for tomorrowThe nightly run has a well-known trap: if nobody looks at its results, it does not exist. Reservalia solves it with a rule Marta wrote in the README.md: a red nightly is reviewed first thing and, if it is not fixed the same day, a ticket with an owner is opened.
- Design anti-patterns
The monolithic single-job pipeline. Everything in consecutive steps: lint, tests, build, push, deployment. It arises naturally at the start, because it avoids the problem of sharing files between jobs. And it works… until it takes eighteen minutes, there is nothing to parallelise, a formatting failure in the second minute stops you knowing whether the tests pass and — most serious of all — the job running npm ci is the same one holding the production credentials. The cure is to split it by responsibility and accept the cost of transferring artifacts.
The job that does everything. A subtler variant of the previous one: the graph exists, but one of the jobs accumulates responsibilities because "the machine is warm anyway". A build that also deploys, notifies and updates a ticket is impossible to retry in parts: when the notification fails, you have to repeat the whole build. A job should be re-runnable on its own, and that requires it to have a single invariant.
Business logic hidden in the YAML. A seventy-line run: | of Bash that decides which environment to deploy to depending on the branch, computes the version number and applies business rules. It cannot be tested, it cannot be run locally, it has no type checking, nobody reviews it because it lives inside a configuration file, and the day you migrate tools it has to be rewritten from scratch. The rule: the YAML orchestrates, the scripts do. That is why Reservalia has infra/scripts/deploy.sh, canary-weight.sh and check-metrics.sh as versioned files runnable from a laptop, rather than as embedded blocks.
The pipeline only Nuria understands. Four hundred lines with nested conditions, unreadable expressions and names like job2. It works, but it is a single human point of failure: when Nuria is on holiday and the pipeline breaks, the team is stuck. The symptoms are easy to spot: nobody else has touched .github/ in six months and, when somebody tries, they experiment through repeated commits with the message "fix ci". The cure has three parts — explicit names, comments that explain the why and not the what, and a CODEOWNERS that spreads the knowledge — and it is developed in 04-05. One last anti-pattern closes the list: the pipeline that ends on a laptop, where the workflow builds and publishes but the final step is done by a person on their own machine. It is the Friday ritual from 01-04, and it comes back in disguise — the quick fix uploaded by hand, the migration Diego launches from psql. If a path to production does not go through the pipeline, the pipeline is not the source of truth.
Common Mistakes and Tips
Mistake 1: chaining needs out of habit. Every unnecessary needs turns a graph into single file: declare only the real data dependencies. Mistake 2: confusing a job with a step. Expecting a job to see the previous one's node_modules/ produces a baffling failure — "but I just installed it" — they are different machines.
Mistake 3: a single workflow for everything. It puts nightly verifications into every PR and production permissions into the PR pipeline; separate them by trigger and by privilege. Mistake 4: continue-on-error on a check that should block, which disables a protection rule without anybody noticing.
Mistake 5: passing container images as run artifacts. Hundreds of megabytes up and down where a docker pull would have done. Mistake 6: accepting a green that depends on the runner's state: if the job passes because of something somebody installed by hand on the agent, you have not verified your code. Tip 1: draw your pipeline; a twenty-line diagram reveals absurd dependencies that go unnoticed in YAML for months. Tip 2: put timeout-minutes on every job, at roughly double the usual duration: without it, a hung job burns six hours of billable minutes before giving up. Tip 3: write down next to each job how long it takes, which is the baseline without which 04-04 cannot even begin.
Exercises
Exercise 1
A team has a ci.yml with five jobs chained in sequence: install (1 min) → lint (40 s) → test (4 min) → build (3 min) → e2e (7 min). Each job repeats the npm ci because the previous one leaves it nothing. Calculate the total time, propose a redesign with the appropriate model stating the needs of each job and estimate the new time. Justify what you do with e2e.
Exercise 2
At Reservalia, somebody proposes adding to the build job a step that runs load tests against staging "since the image is already built". Argue whether it is a good idea using the concepts from this lesson, and say where you would place that verification and why.
Exercise 3
A pipeline publishes the image in the build job, and the deploy job rebuilds it from source because "that is simpler than passing the digest". Explain which invariant is broken, what specifically can go wrong and rewrite the information passing with the correct mechanism.
Solutions
Solution 1. Current time: 1 + 0.67 + 4 + 3 + 7 = 15 min 40 s, plus the npm ci repeated five times.
Redesign with a graph. install disappears as a job: installing on one machine is of no use on another, so each job does its own npm ci with a dependency cache (04-04). That leaves:
lint: { } # no needs
test: { } # no needs
build: { } # no needs
e2e: { needs: [build] } # it needs the image: a real data dependencyThe first three start at the same time. The critical path is build (3 min) + e2e (7 min) = 10 minutes, against 15:40. But 10 minutes is still on the edge of a PR's budget, and the culprit is e2e. The right decision is to take the full E2E suite out of the PR pipeline: keep the five critical scenarios (≈ 90 s) and move the rest to the nightly pipeline from section 9. The PR then drops to about 4 min 30 s and the full coverage is preserved, just with the latency shifted to where nobody suffers it.
Solution 2. It is not a good idea, for three reasons that rest on three different concepts. (1) It confuses categories: a load test does not answer "is it safe to merge this change?" but "does the system hold up?", and its result depends on the environment and on the traffic at that moment; it is informative, not blocking, so it does not belong on the path that blocks the merge. (2) It breaks the time budget: a meaningful load test lasts between ten and thirty minutes, multiplies the PR time by five and makes the team stop watching the pipeline. (3) It breaks the job's invariant and the privilege boundary: build has a single invariant — "there is an image that starts" — and pointing it at staging adds another; besides, build runs on every PR, so twelve people could be loading staging at once, no measurement would be reliable and the environment would be useless for validating deployments. And build does not have, and must not have, credentials over deployed environments.
Where it goes: in the nightly pipeline, against an idle staging, comparing with a historical baseline and opening a ticket if it degrades. How that test is designed is the material of 04-04.
Solution 3. The invariant of the publish stage is broken: "the artifact is immutable and it is the same in every environment", the build-once-and-promote-the-digest principle from 02-06. Rebuilding produces a different binary even when the source code is identical: a new version of an unpinned transitive dependency may get in, the base image node:20.11.0-bookworm-slim may have received patches under the same tag, or any detail of the build environment may change. The concrete consequence: what was verified in build is not what gets deployed, and /version may tell the truth about the commit while lying about the content. If the failure appears only in production, there is no way to reproduce it, because the verified artifact no longer exists.
The correct step is to propagate the immutable digest through outputs:
build:
outputs: { digest: '${{ steps.image.outputs.digest }}' }
steps:
- id: image
uses: docker/build-push-action@v5
with: { context: ., file: apps/api/Dockerfile, push: true,
tags: '${{ env.ECR }}/reservalia/api:${{ github.sha }}' }
deploy:
needs: [build]
steps:
- run: ./infra/scripts/deploy.sh dev "${{ needs.build.outputs.digest }}"The digest (sha256:…) is preferable to the tag even when the tag is the commit SHA: a tag can be rewritten to point at a different image, a digest cannot. It is the difference between a name and a fingerprint.
Conclusion
There is now an overall view where before there were four YAML files. We have the precise vocabulary — workflow, job, step, stage, gate, trigger, artifact — and we know the separation between jobs is physical and therefore costly to cross. We know the seven canonical stages and, more importantly, the invariant each one promises when it passes; we know why the order obeys fail fast and a cost asymmetry that multiplies the price of a failure by ten with every stage it advances. We know the orchestration models and why the graph is the default: it not only saves time, it delivers all the signal at once instead of drip by drip. We have drawn Reservalia's real graph and seen that the boundary between ci.yml and cd.yml is a privilege boundary and that what crosses it is a digest. We know why every run starts from a clean machine, how to move information between jobs with the right mechanism, and how to classify each check as blocking, informative or waiting, with two distinct gates: the merge one and the deployment one.
There is also an inventory of what a pipeline must not be: a single job that does everything with the production credentials in hand, a job that accumulates responsibilities and cannot be retried in parts, seventy lines of Bash with business logic inside a configuration file, four hundred lines only one person understands, or a chain that ends on somebody's laptop. With the shape of the pipeline now clear, the module tackles the four fronts one at a time, and the next one is the one that most quietly breaks the reproducibility that took so much work to achieve. The lesson Dependency Management goes into Reservalia's package-lock.json to answer the question Marta could not answer at the close of module 3: how many packages really get into reservalia/api:a3f9c21, who maintains them, what happens when one of them publishes a minor version with a behaviour change, and how all of that is kept up to date without the team spending Monday reviewing forty update pull requests.
CI/CD Course: Continuous Integration and Deployment
Module 1: Introduction to CI/CD
- Basic CI/CD Concepts
- Benefits of CI/CD
- Popular CI/CD Tools
- The Course Project: the Application We Are Going to Automate
- DORA Metrics: How Software Delivery Is Measured
Module 2: Continuous Integration (CI)
- Introduction to Continuous Integration
- Setting Up a CI Environment
- Build Automation
- Automated Testing
- Code Quality and Static Analysis
- Artifacts, Versioning and Promotion
- Integration with Version Control
Module 3: Continuous Deployment (CD)
- Introduction to Continuous Deployment
- Deployment Automation
- Infrastructure as Code and Reproducible Environments
- Deployment Strategies
- Feature Flags, Rollback and Failure Recovery
- Monitoring and Feedback
Module 4: Advanced CI/CD Practices
- CI/CD Pipelines
- Dependency Management
- Security in CI/CD
- Scalability and Performance
- Pipeline as Code: Templates, Reuse and Testing the Pipeline
- Databases in the Pipeline: Safe Migrations
Module 5: Implementing CI/CD in Real Projects
- Case Study: Web Project
- Case Study: Mobile Application
- Case Study: Microservices
- Case Study: Modernising a Legacy Project
Module 6: Tools and Technologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker and Kubernetes
- GitHub Actions in Depth
- Comparison and Criteria for Choosing a Tool
Module 7: Practical Exercises
- Exercise 1: Setting Up a Basic Pipeline
- Exercise 2: Integrating Automated Tests
- Exercise 3: Deploying to a Production Environment
- Exercise 4: Monitoring and Feedback
- Exercise 5: Hardening the Pipeline with Security and Secrets
- Final Project: A Complete End-to-End Pipeline
