At the close of module 3 one question was left unanswered: how many packages really get into reservalia/api:a3f9c21, who maintains them and which exact version was used. It is the second of the four outstanding fronts, and the quietest of them all: nobody opens a ticket saying "the dependencies are wrong", but on any given Monday CI goes red without anybody having touched the code, or a minor update changes the behaviour of a function and bookings for businesses with split opening hours start being calculated wrongly. This lesson turns that blind spot into something managed: what exactly a lockfile guarantees and what it does not, how version ranges are chosen, how updates are automated without drowning the team in pull requests, how transitive dependencies are audited, how they are cached in the pipeline without breaking reproducibility, and when an organisation ends up needing its own registry. What we will not cover here are vulnerabilities or supply chain attacks — malicious packages, npm audit as a quality gate, artifact signing: all of that is lesson 04-03, and it builds on what we construct today.

Contents

  1. Reservalia's real inventory
  2. What exactly a lockfile guarantees
  3. npm ci versus npm install, and what happens if somebody edits the lockfile by hand
  4. Lockfiles in other ecosystems
  5. Version ranges and the trade-off between patches and reproducibility
  6. Transitive dependencies: seeing them, understanding them and forcing them
  7. Automated updating: Reservalia's dependabot.yml
  8. Update strategy: what merges itself and what gets reviewed
  9. Dependency caching in the pipeline and its invalidation
  10. Private registries and mirrors
  11. Choosing a library and retiring abandoned ones
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion

  1. Reservalia's real inventory

Marta asks for the figure and Diego produces it in thirty seconds:

npm ls --all --workspace apps/api | wc -l                # 1 · the full tree
npm ls --omit=dev --all --parseable | sort -u | wc -l    # 2 · only what reaches production
  1. npm ls --all unfolds the entire tree, including the dependencies of the dependencies. Without --all you would only see the first level, which is the part you already knew about.
  2. --omit=dev discards development tooling, which does not travel inside the image. --parseable prints paths rather than a tree, so they can be sorted and counted without duplicates.

The result surprises everybody the first time:

apps/api apps/web Monorepo total
Direct production dependencies 14 9 23
Development dependencies 21 18 39
Total packages in the tree 612 891 1,147
Packages that end up inside the image 418 418
Distinct maintainers involved ~330

Twenty-three conscious decisions turn into more than a thousand packages written by people the team does not know, published at hours it does not control and executed with the same permissions as Reservalia's own code. The risk is not hypothetical: in module 3 we saw that the change failure rate stalled at 6.5%, and one of the identified causes was exactly this — a minor update of a date library that changed the behaviour of parseISO with time zones and broke calculateSlots for businesses with split opening hours, with no test catching it because none covered that case. The rule that orders the rest of the lesson: a dependency is not free code, it is code you have adopted. If it fails, the customer calls Reservalia, not the maintainer.

  1. What exactly a lockfile guarantees

A package.json declares intentions ("express": "^4.19.2" means "any 4.x from 4.19.2 onwards"). A package-lock.json records facts: the exact version of every package in the tree, where it was downloaded from and its cryptographic fingerprint.

{
  "node_modules/express": {
    "version": "4.19.2",
    "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
    "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
    "engines": { "node": ">= 0.10.0" }
  }
}

Three fields and three different guarantees. version pins which version is installed, so two installations six months apart get the same tree. resolved pins where it comes from, which matters when several registries are configured. And integrity is a hash of the package's contents: if what is downloaded does not match, npm aborts the installation. It is the difference between "I asked for version 4.19.2" and "I received exactly these bytes". Now the part almost nobody explains: what a lockfile does NOT guarantee.

It does guarantee It does not guarantee
The same version of every package That the package will still be available (it can be removed from the registry)
The same content, hash-verified That install scripts do the same thing (they compile according to the machine)
The same resolution tree The same version of Node, of the system or of the OS libraries
Reproducibility of the dependencies Reproducibility of the complete build

That is why the lockfile is a necessary but not sufficient condition, and why Reservalia accompanies it with an .nvmrc that pins the Node version and a pinned base image (node:20.11.0-bookworm-slim). The three pieces together — lockfile, .nvmrc, base image — are what make a3f9c21 mean something concrete.

  1. npm ci versus npm install, and what happens if somebody edits the lockfile by hand

npm install npm ci
Reads the lockfile Yes, but it can modify it Yes, and it never modifies it
If package.json and the lockfile disagree It resolves and rewrites the lockfile It fails with an error
Pre-existing node_modules/ It reuses and patches it It deletes it and reinstalls
Speed in CI Lower and variable Higher and constant
Where to use it On your laptop, when adding a package In the pipeline, always

The decisive row is the second one. npm ci requires the lockfile to be consistent with the package.json, and that behaviour is exactly what you want in CI: if Diego adds "zod": "^3.23.0" to the package.json and forgets to commit the updated lockfile, the quality job fails in twenty seconds with a clear message, instead of quietly installing a version nobody has recorded. The check is free and eliminates a whole category of "it works on my machine". Editing the lockfile by hand is a temptation that shows up when resolving a merge conflict, and it deserves an explicit warning. A package-lock.json is not a list, it is a resolved graph: changing a version by hand does not recompute that dependency's own dependencies, so you can leave the tree in a state npm would never have produced — a library declaring it needs >=5.0.0 coexisting with 4.8. The result is run-time failures that are impossible to reproduce. The right way to resolve a lockfile conflict is to regenerate it:

git checkout --theirs package-lock.json   # 1 · keep either version, it does not matter
npm install                                # 2 · let npm recompute the graph
git add package-lock.json
  1. The specific content is irrelevant, because it is going to be rewritten entirely.
  2. npm install — here yes, not ci — recomputes the tree from the already merged package.json files. Afterwards it is worth looking at the resulting diff: if fifty unexpected changes appear, somebody had pinned something by hand.

  1. Lockfiles in other ecosystems

The concept is universal even though the names change. If you work outside Node, this table translates everything above:

Ecosystem Intentions file Lockfile Reproducible command in CI
npm package.json package-lock.json npm ci
Python + Poetry pyproject.toml poetry.lock poetry install --sync
Python + pip requirements.in requirements.txt with hashes pip install --require-hashes -r requirements.txt
Maven pom.xml None: exact versions in the POM mvn -B verify with fixed versions
Gradle build.gradle gradle.lockfile (must be enabled) gradle build after --write-locks
Go go.mod go.sum (hashes) go build -mod=readonly
Rust Cargo.toml Cargo.lock cargo build --locked
PHP composer.json composer.lock composer install --no-dev

Two useful observations. Maven is the exception: it has no lockfile of its own, and reproducibility depends on you not using LATEST or RELEASE versions and on the dependencyManagement blocks being complete; that is why serious Java teams add a plugin that pins the tree. And the pattern in the right-hand column is always the same: there is an "install exactly what the lockfile says and fail if it does not match" mode. That is the command that goes in the pipeline; anything else is npm install in disguise.

  1. Version ranges and the trade-off between patches and reproducibility

Semantic versioning (semver) gives MAJOR.MINOR.PATCH a contractual meaning: major = incompatible change, minor = new compatible functionality, patch = compatible fix.

Range Meaning Accepts When to use it
^4.19.2 Compatible with 4.19.2 4.19.3, 4.20.0 · not 5.0.0 The default in an application
~4.19.2 Patches only 4.19.3 · not 4.20.0 Sensitive or unstable dependencies
4.19.2 Exact Only that one Tools whose behaviour must be identical
* or latest Anything Everything Never

The point that confuses many people: with a lockfile, the range hardly matters for reproducibility. The lockfile pins the installed version, so ^4.19.2 and 4.19.2 produce exactly the same tree today. What the range decides is what is accepted when the lockfile is regenerated: on running npm update, on adding a package or when Dependabot proposes a bump. And there is where the real trade-off lies. A wide range (^) lets fixes arrive without friction, but it bets on the maintainer respecting semver, which is not always the case — the parseISO incident in section 1 was precisely a minor version with a behaviour change. A narrow range removes surprises but turns every fix into manual work, and a project with 23 direct dependencies pinned to exact versions is out of date within six months.

Reservalia's stance, advisable for most teams: ^ by default in the package.json, the lockfile always committed to git, and updates arriving through a reviewed pull request with green CI, never through automatic resolution at install time. That way the range describes what is acceptable and the lockfile describes what is used, and moving from one to the other is always a visible commit.

  1. Transitive dependencies: seeing them, understanding them and forcing them

A transitive dependency is one you did not ask for: it comes in because one of your dependencies needs it. Of Reservalia's 1,147 packages, 1,124 are transitive. They do not appear in the package.json, nobody chose them and they are the overwhelming majority of the code that runs.

npm ls date-fns      # 1 · who brings this package in and in which version
npm why date-fns     # 2 · the chain of reasons, in npm 9 and later
npm outdated         # 3 · what is behind and by how much
  1. npm ls <package> prints the path from your direct dependencies down to it. It is the tool that answers "and where did this come from?", which is the first question of almost any investigation.
  2. npm why gives the same thing in explanation form. If two different versions of the same package appear, npm has installed both — one at the root and one nested — and that causes baffling errors when the package holds global state.
  3. npm outdated compares what is installed with what is published and separates "what your range allows" from "the latest that exists". It is the best thermometer of dependency debt in ten seconds.

When a transitive version has to be forced — typically because 2.3.1 has a bug and you need 2.3.2, but whoever brings it in has not updated yet — npm offers overrides:

{
  "overrides": {
    "semver": "7.6.2",
    "some-package": { "minimist": "1.2.8" }
  }
}

The first form replaces that version across the whole tree, wherever it comes from; the second limits the replacement to the subtree of one specific package, which is safer when you do not know whether the other consumers tolerate the change. An override is declared technical debt and must be treated as such: you are asserting that a version different from the one the maintainer tested works the same, and nobody has verified that beyond your tests. Reservalia imposes two rules on itself: every entry carries a comment with the reason and a link to the original issue, and the list is reviewed once a quarter to remove what is no longer needed. The equivalent in other ecosystems is resolutions (Yarn), dependencyManagement (Maven) or replace (Go).

  1. Automated updating: Reservalia's dependabot.yml

With 1,147 packages, keeping up to date by hand is impossible; not doing it is accumulating a giant migration for two years' time. The solution is a bot that opens update pull requests: Dependabot (built into GitHub) or Renovate (more configurable and available on any platform). Reservalia chooses Dependabot because it is already in the house:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: "/"                       # 1 · one lockfile: the root is enough
    schedule:
      interval: weekly
      day: tuesday                        # 2 · Tuesday, not Monday or Friday
      time: "06:00"
      timezone: Europe/Madrid
    open-pull-requests-limit: 5           # 3 · a ceiling on PRs open at once
    groups:                               # 4 · group them so as not to drown the team
      dev-minor:
        dependency-type: development
        update-types: [minor, patch]
      prod-patches:
        dependency-type: production
        update-types: [patch]
    ignore:
      - dependency-name: "typescript"     # 5 · majors by hand
        update-types: [version-update:semver-major]
    labels: [dependencies]
    commit-message: { prefix: "chore(deps)" }

  - package-ecosystem: github-actions      # 6 · actions are dependencies too
    directory: "/"
    schedule: { interval: monthly }
    groups:
      actions: { patterns: ["*"] }
  1. directory: "/" points at where the package-lock.json lives. Since Reservalia uses npm workspaces with a single lockfile at the root, one entry covers apps/api, apps/web and packages/shared-types. With separate lockfiles you would need one entry per folder.
  2. Tuesday first thing. On Monday the team clears the weekend's inbox and on Friday it leaves PRs open until Monday. It is a minor detail that changes the real review rate a great deal.
  3. open-pull-requests-limit is the protection against the avalanche effect. Without it, the first run on a neglected repository opens forty pull requests, the team is overwhelmed and ignores them all: the bot goes from helping to being background noise.
  4. groups is the most valuable option in the file: instead of one PR per package, it bundles several into one. Reservalia puts all the minors and patches of development tooling into one weekly PR, and the production patches into another. Twenty PRs become two or three.
  5. ignore for TypeScript majors: a major bump of tsc can throw up hundreds of new errors, and that is planned work, not a Tuesday PR.
  6. GitHub actions are dependencies like any other, and they are usually forgotten. uses: actions/checkout@v4 is third-party code executed with access to the repository; how they are pinned safely is the business of 04-03.

One detail decides whether all of this works: an automatic update is only safe if the CI from module 2 is trustworthy. The bot does not read the changelog and does not understand your business; the only thing separating "an update applied without drama" from "a regression in production" is that the tests cover what matters. If the suite is weak, automating dependencies does not reduce the risk: it accelerates it. That is why this front comes after module 2 and not before.

  1. Update strategy: what merges itself and what gets reviewed

A bot that opens PRs with no policy about what to do with them merely moves the work elsewhere. This is the table Reservalia writes in the README.md:

Type of update Action Who Rationale
Patch to a development dependency Auto-merge with green CI Nobody Risk close to nil: it does not reach production
Minor to a development dependency Auto-merge with green CI Nobody Same; they come bundled in the weekly PR
Patch to a production dependency Quick review and merge Weekly rota It is usually a fix; CI covers the rest
Minor to production A real review: read the changelog Weekly rota This is where the parseISO incident lives
Major of any kind A planned ticket Marta assigns Incompatible changes: it is work
Security update Top priority Nuria See 04-03

Three decisions deserve an explanation. Auto-merge is not sloppiness: it rests on the pipeline blocking the merge if anything fails — the quality gate from 04-01 — and on those dependencies not travelling inside the image. It is enabled with one line:

gh pr merge --auto --squash "$PR"    # waits for the checks and merges only if they are green

Production minors get a human review precisely because of the incident in section 1. Semver is a promise, not a mechanism: a behaviour change with no API change is formally "minor" and functionally a bomb. Reading a package's changelog costs two minutes and is the only real defence. There is a weekly rota with a name on it. Without an assigned person, updates are everybody's responsibility, which is the same as nobody's. Reservalia rotates the duty every week with a time box — half an hour on Tuesdays; what does not fit waits for the next one, but it never piles up in silence.

  1. Dependency caching in the pipeline and its invalidation

Downloading 1,147 packages in every job is the slowest and least interesting part of CI. The cache removes it, but it introduces an obvious risk: if it serves the wrong cache, the pipeline verifies something that is not your code. The solution is for the cache key to be a hash of the lockfile.

      - uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
          cache: npm                      # 1 · shortcut: it already uses the lockfile hash
      - run: npm ci

      # The explicit equivalent, to understand what the shortcut does:
      - uses: actions/cache@v4
        with:
          path: ~/.npm                                                     # 2
          key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }}   # 3
          restore-keys: npm-${{ runner.os }}-                               # 4
  1. cache: npm in setup-node does exactly what is written below it. It is what Reservalia has used since 02-02; here we see why it works.
  2. ~/.npm is cached, not node_modules/. The first is npm's download cache, platform-independent; the second contains binaries compiled for a specific system and internal links, and restoring it on another machine produces failures that are hard to diagnose. Restoring ~/.npm and running npm ci is fast and correct.
  3. The key contains the lockfile hash. If the lockfile changes by a single byte, the key changes and the previous cache is not used: invalidation is automatic and does not depend on anybody remembering.
  4. restore-keys is the safety net: if there is no exact match, the most recent cache starting with the same prefix is restored and npm only downloads what is missing. That way a dependency update does not force everything to be fetched from scratch.

The general rule, valid for any pipeline cache: the key must derive from the content of the inputs. If the key is fixed (key: npm-cache), the cache is never invalidated and you end up running tests against old dependencies — a false green, the cross-cutting risk developed in 04-04. And a rule of hygiene: the cache is an accelerator, never a source of truth; the pipeline must work correctly if it is emptied entirely, just more slowly.

  1. Private registries and mirrors

Reservalia installs directly from registry.npmjs.org, and with 340 paying businesses that is starting to be uncomfortable for three concrete reasons: if the public registry has an outage, you cannot deploy — and responding to an incident may require deploying; there is nowhere to publish @reservalia/shared-types the day it has to be shared with the mobile application in module 5; and there is no single point at which to apply a policy of the sort "this library is not used at this company".

Option What it gives you Cost When it makes sense
Public registry directly Total simplicity Zero Small teams with no packages of their own
GitHub Packages Private packages with the repository's permissions Very low if you already use GitHub Reservalia's natural next step
Artifactory / Nexus Public mirror + private registry + policies High: it is infrastructure to maintain Several dozen developers or audit requirements
Read-only mirror Isolates you from outages and withdrawn packages Medium When deployment cannot depend on a third party

The configuration is one file, and it is worth understanding what each line does:

# .npmrc
@reservalia:registry=https://npm.pkg.github.com     # 1 · only our own scope
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}        # 2 · from a variable, never literal
registry=https://registry.npmjs.org                  # 3 · everything else, from the public one
  1. Only @reservalia/* packages are looked up in the private registry. Redirecting everything to a private registry with no mirror configured breaks the installation of the other 1,100 packages.
  2. The token is read from an environment variable. An .npmrc with a literal token committed to git is a credential leak, and it is surprisingly common.
  3. The order matters: the general line goes last and acts as the default.

And a warning about the well-known "package that disappears": a maintainer can withdraw a version from the public registry and break builds that had been working for years. The lockfile's integrity does not help — it verifies the content, it does not guarantee availability. Only a mirror with a local copy protects against that, and it is the main reason a mid-sized organisation eventually sets one up.

  1. Choosing a library and retiring abandoned ones

The best dependency management is not having the dependency. Before adding one, Reservalia asks itself five questions in this order:

  1. Can I solve it with the standard library? Node 20 ships fetch, a test runner, crypto.randomUUID() and a good deal more. Many historical dependencies are surplus today.
  2. How much code am I adopting? A twenty-line utility copied in with its own tests is preferable to a package that drags in another thirty.
  3. Is it alive? Recent commits, issues that get answered, more than one maintainer, a stable major version.
  4. How much would it cost to get out? A library encapsulated behind a module of your own is replaced in a day; one whose types appear in two hundred files is a marriage.
  5. What licence does it have? It usually gets looked at too late, and it is a legal problem, not a technical one.

At the other extreme are abandoned dependencies, with clear symptoms: no releases in two years, unanswered issues, a deprecation warning on install or incompatibility with new versions of the language. The right reaction is not urgency — a stable, unchanging library may simply be finished — but record-keeping: note it on a risk list, encapsulate it behind an interface of your own so it can be swapped without touching two hundred files, and plan the replacement for when it blocks something. What you must not do is discover it the day a security flaw forces an update and it turns out there is no version to update to.

Common Mistakes and Tips

Mistake 1: not committing the lockfile to git. Without it, every installation resolves on its own and "it works on my machine" becomes a valid argument again. Mistake 2: using npm install in the pipeline, which can rewrite the lockfile inside the runner and make CI verify a different tree from the one you reviewed.

Mistake 3: editing the lockfile by hand to resolve a conflict, leaving a graph npm would never have produced; regenerate it. Mistake 4: pinning everything to exact versions "to be safe", which freezes the project and turns every fix into manual work.

Mistake 5: enabling Dependabot with no groups and no PR limit. Forty pull requests on the first Tuesday, and the team learns to ignore the dependencies label for ever. Mistake 6: automating updates with a weak test suite: you do not reduce the risk, you accelerate it. Mistake 7: caching node_modules/ instead of ~/.npm, or using a fixed cache key that is never invalidated; both produce false greens. Tip 1: look at the tree once a quarter. npm ls --all and npm outdated in ten minutes give a very clear idea of where the next scare will come from. Tip 2: encapsulate the important dependencies behind a module of your own, so replacing them is a day's work. Tip 3: treat overrides as debt, with a comment, a reason and a quarterly review.

Exercises

Exercise 1

A team's CI goes red on a Monday morning without anybody having touched the code: the last commit is from Friday and it was green then. In the pipeline they use npm install. Explain in detail what has happened, why the package-lock.json did not prevent it and which two changes prevent it for good.

Exercise 2

Design the dependency update policy for a team of 4 people with a suite covering 45% of the code and no integration tests. State what you would change relative to the table in section 8 and why, and what you would do first.

Exercise 3

Reservalia needs version 7.6.2 of semver because 7.5.4 has a bug, but it is brought in transitively by some-linter and its maintainer has not published the fix yet. Write the solution, explain its risks and say how you would stop that workaround sitting in the repository for ten years.

Solutions

Solution 1. What happened: npm install is not obliged to respect the lockfile. If the package.json declares ^ ranges and new versions were published over the weekend, npm install can resolve them and install a different tree from the recorded one, rewriting the lockfile inside the runner — where nobody sees the diff. All it takes is for one dependency, direct or transitive, to have published a minor version with a behaviour change for the suite to fail. The lockfile did not prevent it because it was there, but it was not being used as the rule: it described a tree the command was under no obligation to reproduce. It is exactly the parseISO incident scenario. The two changes: (1) replace npm install with npm ci in every job, which installs the lockfile's tree without deviating and fails if the lockfile does not match the package.json; and (2) channel updates through a pull request with Dependabot, so that every change to the tree is a reviewable commit, with its own CI and with the lockfile diff in front of you. A third complementary change, cheaper than it sounds: pin .nvmrc and the base image too, because a Node version change produces the same symptom — "red without touching anything" — and gets diagnosed just as badly.

Solution 2. The figure that governs the answer is the quality of the suite: with 45% coverage and no integration tests, CI is not a sufficient safety net, and the policy in section 8 — which rests precisely on that net — cannot be copied as it stands. Changes relative to the table: remove auto-merge for production entirely, patches included; keep auto-merge only for development dependencies, whose failure shows up in the pipeline itself and not in production; group everything into a single weekly PR so the review fits into the real time of a team of four; and reduce the frequency to fortnightly if the team cannot keep up, consciously accepting the delay instead of accumulating ignored PRs.

What I would do first: not touch the bot's configuration, but the test suite. Specifically, write integration tests over the three or four critical business paths, which is what turns a decorative CI into a gate. Automating dependencies with a net full of holes is swapping a slow, visible risk for a fast, invisible one. In the meantime, a cheap and highly effective measure: enable Dependabot for security alerts only, which are the ones that cannot be postponed, and leave the maintenance ones until the net exists.

Solution 3. The solution is an override in the root package.json:

{
  "overrides": {
    "some-linter": { "semver": "7.6.2" }
  }
}

The nested form is used rather than the global one on purpose: it limits the replacement to some-linter's subtree, so that if another package depends on an older semver for a legitimate reason, it is not affected. The global form would be more convenient and riskier. Risks: you are running some-linter with a version of semver its maintainer has not tested; if 7.6.2 changed some subtle behaviour, the failure will show up inside a dependency, which is the worst place to diagnose it. On top of that the override is silent: it does not start failing when it stops being necessary, it simply sits there pinning an ever older version and blocking future updates of that entire subtree.

How to stop it fossilising, with three measures of almost no cost: a comment next to the entry with the date, the reason and a link to the issue opened on some-linter; a quarterly review of the full overrides list within the dependency rota from section 8; and, the most effective, subscribing to the issue, so that when the maintainer publishes the fix a notification arrives and the workaround is removed the same day. An override with no expiry date is indistinguishable from a permanent decision nobody made.

Conclusion

The blind spot is no longer blind. Reservalia knows that 418 packages from around 330 maintainers travel inside reservalia/api:a3f9c21, and it knows how to see them, pin them and update them. It is clear about what a lockfile guarantees — version, origin and hash — and what it does not — availability, environment, the complete build — and that is why it accompanies it with .nvmrc and a pinned base image. It uses npm ci in the pipeline, so an inconsistent lockfile fails in twenty seconds instead of quietly installing something nobody reviewed. It knows that a lockfile is regenerated and never edited by hand, and it knows the equivalent of the pattern in Poetry, Gradle, Go, Cargo and Composer. It keeps its ranges at ^ so fixes can arrive, relying on the lockfile to decide what actually gets installed. And it sees its 1,124 transitive dependencies with npm ls and npm why, using overrides with a comment and a review date only when there is no alternative. Above all, updates have stopped being a traumatic annual event and become a flow: a .github/dependabot.yml with grouping, a PR limit and a sensible schedule; a written policy separating what merges itself from what requires reading a changelog; a weekly rota with a name and a bounded half hour; and a cache whose key is the lockfile hash, so it invalidates itself and never produces a false green. The parseISO incident that was polluting the change failure rate now has two defences: human review of production minors and a test covering the case that broke.

One flank remains uncovered, and it is the one that turns all of the above into a security problem and not merely a maintenance one. Those 1,147 dependencies are downloaded from a public registry, executed with the pipeline's permissions and end up inside an image that is deployed to production; and the pipeline itself, meanwhile, holds credentials over AWS, runs third-party actions pinned by tag and publishes the artifacts everybody trusts. The lesson Security in CI/CD tackles both halves of the problem — the security of the software passing through the pipeline and the security of the pipeline itself — adds a security job to Reservalia with a severity policy the team can actually sustain, and finishes where this chain of trust ends: the signed inventory of what each artifact contains.

CI/CD Course: Continuous Integration and Deployment

Module 1: Introduction to CI/CD

Module 2: Continuous Integration (CI)

Module 3: Continuous Deployment (CD)

Module 4: Advanced CI/CD Practices

Module 5: Implementing CI/CD in Real Projects

Module 6: Tools and Technologies

Module 7: Practical Exercises

Module 8: Additional Resources

© Copyright 2026. All rights reserved