CI/CD
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against NestJS 12.1 (npm @nestjs/core 12.1.2, CLI 12.0.8), TypeScript 6 (6.0.x), Node.js 22 and 24 LTS, Express 5 and Fastify 5, and was checked on 2026-10-02. Action versions are the latest GitHub releases on that date. The npm steps of every workflow (npm ci, npm run lint, npm run test:cov, npm run test:e2e, npm run build, npx nest upgrade --dry-run) are the scripts of a project generated by the Nest 12 CLI (ESM template, Vitest, oxlint). The behaviour of GitHub’s runners, registry and servers is described from the official action and Nest documentation.
Continuous integration (CI) means that every change is built and tested automatically by a machine that is not yours, so that "it works on my machine" is replaced by a shared, repeatable verdict. Continuous delivery and deployment (CD) extends the same pipeline: a change that passes is packaged and, after the gates you choose, released. For a Nest application the pipeline is short, because Nest ships the pieces: a linter and test runner preinstalled by nest new, a build script, and a CLI that can preview framework upgrades. This page builds that pipeline for the Bookshelf application that grows across this section: a verify workflow, an image workflow, a gated deploy, the Devtools graph report on pull requests, and a scheduled upgrade preview. For Docker and GitHub Actions in depth, see Docker CI/CD with GitHub Actions; for the Git and GitHub basics behind branches and pull requests, see Git & GitHub.
The pipeline at a glance
lint, test:cov, test:e2e, build"] pr --> dt["Devtools: publish graph"] push["Push to main"] --> verify verify --> image["image: build + push to GHCR"] image --> gate{"production
environment gate"} gate --> deploy["deploy: SSH, hook or Mau"] cron["Weekly schedule"] --> upgrade["nest upgrade --dry-run"]
Pull requests run the verification and publish a Devtools graph, so a reviewer sees the result before merging. Only a push to main continues to the image and deploy jobs. The scheduled job is independent: it tells you what the next framework upgrade would change.
Choosing a CI tool
Nothing in Nest ties you to a CI product: the pipeline is npm ci, npm run lint, npm run test:cov, npm run build. Choose the tool that already hosts your code.
| Tool | Strength | Consider it when |
|---|---|---|
GitHub Actions |
Workflows live in the repository, a large marketplace of reusable actions, hosted runners, first-class integration with pull requests, environments and the GitHub container registry. |
The code is on GitHub. This page uses it. |
GitLab CI/CD |
A single |
The code is on GitLab. The Devtools documentation includes a ready-made GitLab pipeline for graph publishing. |
Jenkins |
Self-hosted, very flexible, huge plugin ecosystem, pipelines as code in a |
You must run on your own hardware or in an air-gapped network, and accept maintaining the server and its plugins. |
CircleCI |
Hosted, fast caching and parallelism, reusable configuration packages (orbs). |
You want a hosted tool independent of your Git host. The Devtools documentation lists its built-in variables as input to the graph publisher. |
The rest of this page uses GitHub Actions. The shape (install, lint, test, build, package, deploy) carries over to the other tools unchanged; only the YAML differs.
What the pipeline runs: the generated npm scripts
A project generated by nest new already has the scripts a pipeline needs. The official documentation describes npm run lint (oxlint) and npm run format (Prettier) as the headless commands for continuous integration, and the generated package.json of the ESM template also defines the test and build scripts:
{
"scripts": {
"build": "nest build",
"lint": "oxlint --type-aware src/ test/",
"test": "vitest run",
"test:cov": "vitest run --coverage",
"test:e2e": "vitest run --config ./vitest.config.e2e.ts"
}
}
Source: NestJS: First steps (linting and formatting); the test and build scripts are the ones the Nest 12 CLI generates (CLI overview).
The CommonJS template uses Jest instead of Vitest, with the same script names, so the workflow below works for both. Two details matter in CI. The linter exits with a non-zero status when it finds an error, which fails the step. And nest build runs the TypeScript compiler (or the builder configured in nest-cli.json), so a type error fails the build even when the tests passed.
Coverage thresholds
npm run test:cov only measures coverage; it does not fail when coverage is low unless you configure thresholds. The @vitest/coverage-v8 package is already in the generated devDependencies. This configuration limits the report to application code and makes the command fail below the numbers:
import { defineConfig } from 'vitest/config';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
globals: true,
root: './',
include: ['**/*.spec.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/main.ts', 'src/**/*.module.ts', 'src/**/*.spec.ts'],
reporter: ['text-summary', 'lcov', 'json-summary'],
thresholds: { statements: 80, branches: 50, functions: 80, lines: 80 },
},
},
});
Source: Vitest: Coverage.
With the template’s single sample test, running npm run test:cov printed the summary below. With every threshold at 80 the same run failed with the three ERROR: Coverage for … does not meet global threshold lines and exit status 1, which is what turns a coverage drop into a red build. The values in the file above are the ones that pass for the generated project; choose your own and raise them over time.
=============================== Coverage summary ===============================
Statements : 100% ( 7/7 )
Branches : 50% ( 1/2 )
Functions : 100% ( 3/3 )
Lines : 100% ( 7/7 )
================================================================================
The verify workflow
The workflows below are files under .github/workflows/ in the repository. The first job runs on every pull request and on every push to main. It checks out the code, installs Node.js with the npm cache enabled, installs dependencies from the lockfile, and runs lint, unit tests with coverage, end-to-end tests and the build, once per Node.js version in the matrix.
name: CI
on:
push:
branches: [main]
pull_request:
# The default token is read-only; jobs that need more ask for it explicitly.
permissions:
contents: read
# A newer push to the same branch or pull request cancels the older run.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
name: Verify (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [22, 24]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Unit tests with coverage
run: npm run test:cov
- name: End-to-end tests
run: npm run test:e2e
- name: Build
run: npm run build
- name: Upload coverage
if: matrix.node-version == 24
uses: actions/upload-artifact@v7
with:
name: coverage
path: coverage/
retention-days: 14
The pieces, in the order they matter:
-
permissions: contents: readat the top gives the job token the least access it needs; jobs that need more (the image job) ask for it themselves. -
concurrencycancels an older run of the same branch when a newer commit arrives, which saves minutes and avoids two deploys racing. -
The matrix runs the same steps on Node.js 22 and 24, the two LTS lines that Nest 12 supports (v12 apps run on Node.js 20.19+ or 22.12+; the CLI generators and
nest upgradeneed 22.22.3+, 24.15+ or 26+, whichsetup-nodeprovides because a major resolves to the latest minor). Node.js 20 reached end of life on 2026-04-30 and is no longer in the matrix; Node.js 26 becomes an LTS line on 2026-10-28, and you add it to the matrix at that point.fail-fast: falselets the other version finish so you see whether a failure is version-specific. -
cache: npmmakessetup-nodecache the npm download cache, keyed on the lockfile.npm cithen installs exactly whatpackage-lock.jsonsays and fails if the lockfile andpackage.jsondisagree, which is the behaviour you want in CI;npm installwould silently update the lockfile. -
Lint, test and build are separate steps so the log shows which one failed. End-to-end tests run before the build in this workflow; they build the test module in memory with
Test.createTestingModuleand need nodistfolder (see Testing). -
The coverage artifact is uploaded from one matrix entry only, so the two entries do not try to publish the same name.
If you target the CommonJS template, nothing in the workflow changes; only the test runner does (Jest instead of Vitest). The migration guide notes that Jest can load the ESM-only v12 packages only on Node.js 24.9 or newer, so a Jest project should run on Node.js 24 or move to Vitest.
Keeping the workflow current
Actions are versioned dependencies like any package. On 2026-10-02 the latest releases were:
| Action | Version used | Latest release checked |
|---|---|---|
|
|
v7.0.1 (2026-07-20) |
|
|
v7.0.0 (2026-07-14) |
|
|
v7.0.1 (2026-04-10) |
|
|
v4.4.1 (2026-09-16) |
|
|
v4.6.0 (2026-07-29) |
|
|
v6.2.0 (2026-07-02) |
|
|
v7.4.0 (2026-09-15) |
The tags were read from the GitHub releases API and confirmed to exist. A major tag such as v7 moves with every compatible release; for a stricter supply chain, pin the full commit SHA and let Dependabot propose updates. Dependabot can update the actions, the npm packages (grouping all @nestjs/* packages in one pull request, so versions that must move together do) and the Docker base image:
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
groups:
nestjs:
patterns: ['@nestjs/*']
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
Secrets and configuration
A workflow needs three kinds of values: the token GitHub gives every run, secrets you create (SSH keys, API keys), and non-secret settings.
-
GITHUB_TOKENis created for each run and expires when the run ends. Its permissions come from thepermissionskey; the image job above asks forpackages: writeto push to the GitHub container registry and nothing else. Prefer it over a long-lived personal access token. -
Repository and environment secrets are available in the
secretscontext. They are masked in logs, but masking is best-effort: never print them, never pass them on a command line that is echoed, and prefer theenv:key of a step (as the deploy job does) to interpolating them into a script, so a value containing quotes cannot change the script. -
Variables (the
varscontext) are for non-secret settings such as a registry user name. -
Workflows triggered by pull requests from forks, and by Dependabot, do not receive repository secrets. Jobs that need them, such as the Devtools publish below, skip those actors or run only on pushes.
-
Cloud credentials are better obtained with OpenID Connect than stored as secrets: the workflow requests a short-lived token from the cloud provider after proving its identity, so no static key exists to leak.
The application reads its own configuration from the environment at start-up through ConfigService, and the same rules apply there: no secret is committed to the repository, to the workflow file or to the image. CI injects test-only values; production values live in the platform that runs the container. See Configuration for validating the environment at start so a missing value fails fast. Environments (production, staging) hold secrets and approval rules that apply only to jobs that target them, which is how the deploy job below is protected.
Building and pushing the image
The Nest documentation’s deployment page shows a single-stage Dockerfile; for CI it is worth using two stages so the final image contains only the compiled output and the production dependencies. The build stage installs everything, runs nest build and then removes the development dependencies with npm prune --omit=dev; the final stage copies the result and runs as the unprivileged node user.
FROM node:24-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:24-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/package.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/main"]
Source: NestJS: Deployment - Dockerizing your application. For layer caching, multi-stage builds and image hardening, see Build Cache and Multi-stage Builds and Image Best Practices and Security.
The copied package.json, node_modules and dist are all the application needs to start with node dist/main. For a freshly generated project the production dependency tree is 21 MB, against 161 MB with the development dependencies. The COPY lines have no --chown, so the files stay owned by root and the node user can read but not modify them; Deployment explains the choice.
The image job builds that Dockerfile and pushes it to the GitHub container registry. It runs only after verify succeeded and only for pushes to main:
jobs:
# ...verify job above...
image:
name: Build and push image
needs: verify
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=sha
type=raw,value=latest
- id: push
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
Source: docker/build-push-action, docker/metadata-action and NestJS: Deployment. The inputs are explained in Docker CI/CD with GitHub Actions.
metadata-action computes the tags (sha- plus the short commit, and latest) and the OCI labels; build-push-action builds, pushes and reports the image digest, which the job exposes as an output. The gha cache keeps layers between runs. The digest matters for the next job: a tag such as latest can be moved, a digest cannot, so the deploy job rolls out exactly the image that this run built and that passed verification.
Deploy jobs
A deploy job should be separate from the build, depend on it with needs, and target an environment. In the repository settings you create the production environment, add required reviewers and (if wanted) restrict it to the main branch; the job then pauses until a reviewer approves, and its secrets are only exposed after approval.
Deploying over SSH
The simplest target is a virtual machine that runs the container with Docker Compose. The job writes the key from a secret, pins the server’s host key from a second secret (so the connection fails if the host is impersonated rather than trusting the first key it sees), and tells the server to pull and start the image by digest:
jobs:
# ...verify and image jobs above...
deploy:
name: Deploy to production
needs: image
runs-on: ubuntu-latest
# Required reviewers and the environment's own secrets are configured on
# the "production" environment in the repository settings.
environment: production
permissions:
contents: read
steps:
- name: Configure SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
SSH_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- name: Roll out the image by digest
env:
DEPLOY_TARGET: ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}
DIGEST: ${{ needs.image.outputs.digest }}
run: |
REPO=$(printf '%s' "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]')
IMAGE="ghcr.io/${REPO}@${DIGEST}"
ssh -i ~/.ssh/id_deploy "$DEPLOY_TARGET" \
"IMAGE=$IMAGE docker compose -f /srv/bookshelf/compose.yaml up -d --pull always"
Source: GitHub: Managing environments for deployment. For Compose files in general, see Compose in Practice.
The server needs Docker, a compose.yaml at the path used above whose service takes its image from the IMAGE environment variable, and a login to ghcr.io with a read-only token, because the package is private when the repository is private. The step that builds the image reference lowercases the repository name because registry paths must be lowercase, so a mixed-case repository name still produces a valid reference. This pattern replaces the book’s approach of copying files to the server and running the process under PM2: with a container, the unit that was tested is the unit that runs, rollbacks are a matter of starting the previous digest, and there is no dependency drift on the server.
Deploying to a platform
Platforms that build or run your repository for you usually expose a deploy hook, an HTTPS URL that triggers a deploy when called. Store the URL as a secret and call it after verification has passed; this is the shape of the step used for Render:
deploy-platform:
needs: verify
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- name: Trigger the deploy hook
env:
DEPLOY_HOOK_URL: ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
run: curl --fail --silent --show-error --request POST "$DEPLOY_HOOK_URL"
Source: Render: Deploy hooks. The hook URL is itself a credential: anyone who has it can trigger a deploy, which is why it belongs in a secret. If the platform deploys automatically on every push, disable that setting and let the hook decide, otherwise unverified commits are deployed.
Deploying with Mau
Nest’s own platform, Mau, deploys a Nest application to AWS. The CLI wraps it as nest deploy, which forwards every argument to mau deploy. The CLI documentation says that in a non-interactive environment such as CI the command fails instead of prompting when Mau is not installed, so install it explicitly first, and the deployment chapter recommends the globally installed Mau CLI for CI:
npm install --global @nestjs/mau
mau deploy
Source: NestJS: CLI usage - nest deploy and NestJS: Deployment - Easy deployment with Mau. How the CLI authenticates to Mau in a pipeline is described in the Mau documentation and is not covered here; both commands deploy to a real account. @nestjs/mau is a pre-1.0 package (0.2.x at the time of writing).
Other targets
A Kubernetes cluster is usually updated by committing the new image digest to a manifest or Helm values file and letting a GitOps controller apply it; see GitOps and CI/CD and Deployment for the NestJS-specific runtime concerns (probes, graceful shutdown, environment variables). The Cloud section covers the managed services of AWS, Azure and Google Cloud, where the deploy job is replaced by that provider’s action authenticated with OpenID Connect.
The Devtools CI/CD integration
Nest Devtools (@nestjs/devtools-integration) can publish a snapshot of your application’s dependency graph on every build. The registry keeps a history of the graph and, for a pull request whose target branch already has a snapshot, builds a report of what changed structurally: a provider whose scope changed, a guard that disappeared from a route, a module that became global. The documentation lists the CI/CD integration under the Enterprise plan of the Devtools service; the graph explorer itself works locally (see Debugging & Devtools).
The integration has two parts: a switch in main.ts that publishes instead of listening when CI asks for it, and a workflow that runs the application in that mode. The application options are snapshot: true, which makes Nest collect the graph metadata while it bootstraps, and preview: true, which bootstraps the application without running the constructors and lifecycle hooks of controllers, enhancers and providers, so the publish job needs no database. This is the bootstrap with the publish options read from the environment of GitHub Actions:
import { NestFactory } from '@nestjs/core';
import { GraphPublisher } from '@nestjs/devtools-integration';
import type { INestApplication, Type } from '@nestjs/common';
export function publishOptionsFromGitHub(env: NodeJS.ProcessEnv) {
return {
apiKey: env.DEVTOOLS_API_KEY ?? '',
repository: env.REPOSITORY_NAME ?? '',
owner: env.GITHUB_REPOSITORY_OWNER ?? '',
sha: env.COMMIT_SHA ?? '',
target: env.TARGET_SHA,
trigger: env.GITHUB_BASE_REF ? ('pull' as const) : ('push' as const),
branch: env.BRANCH_NAME ?? '',
};
}
export async function bootstrap(
appModule: Type<unknown>,
env: NodeJS.ProcessEnv = process.env,
): Promise<INestApplication | undefined> {
const shouldPublishGraph = env.PUBLISH_GRAPH === 'true';
const app = await NestFactory.create(appModule, {
snapshot: true,
preview: shouldPublishGraph,
});
if (shouldPublishGraph) {
await app.init();
const graphPublisher = new GraphPublisher(app);
await graphPublisher.publish(publishOptionsFromGitHub(env));
await app.close();
return undefined;
}
await app.listen(env.PORT ?? 3000);
return app;
}
Source: NestJS Devtools: CI/CD integration. In your application this body is the bootstrap() function of main.ts; it was extracted into a function that takes the module and the environment only so a test can call it.
The workflow checks out the code, installs dependencies and starts the application with PUBLISH_GRAPH=true. The API key is a secret created on the Devtools settings page, and TARGET_SHA is the base commit of the pull request, which is what the new graph is compared against:
name: Devtools
on:
push:
branches: [main]
pull_request:
branches: ['*']
permissions:
contents: read
jobs:
publish:
# Dependabot and fork pull requests do not receive repository secrets.
if: github.actor != 'dependabot[bot]'
name: Publish graph
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- name: Setup environment (pull request)
if: github.event_name == 'pull_request'
run: echo "COMMIT_SHA=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_ENV"
- name: Setup environment (push)
if: github.event_name == 'push'
run: echo "COMMIT_SHA=${GITHUB_SHA}" >> "$GITHUB_ENV"
- name: Publish
run: npm run start
env:
PUBLISH_GRAPH: 'true'
DEVTOOLS_API_KEY: ${{ secrets.DEVTOOLS_API_KEY }}
REPOSITORY_NAME: ${{ github.event.repository.name }}
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
TARGET_SHA: ${{ github.event.pull_request.base.sha }}
Source: NestJS Devtools: GitHub Actions integration. The official example uses actions/checkout@v4, actions/setup-node@v4 and Node.js 22; the workflow above uses the current majors and Node.js 24, and stores the key in a secret instead of the placeholder the official example shows.
The fields of the publish options are apiKey, owner (organisation or user), repository, sha (the commit being built), target (the commit to compare with, or targetBranch when your CI does not provide it), trigger (push or pull) and branch. The documentation recommends publishing on pushes only for branches that represent a deployment environment (main, staging, production) and always, or at least when the target is such a branch, for pull requests: that keeps a snapshot for every deployable state, so each pull request has something to be compared with. For GitLab and other CI systems the same fields come from their built-in variables; the official page has a GitLab pipeline and notes that Bitbucket Pipelines and CircleCI work the same way.
You can test the publisher without a Devtools account: start the application module with PUBLISH_GRAPH=true against a local HTTP server standing in for the registry (the package reads its target address from a DEVTOOLS_API_URL environment variable when set). Such a test shows that a provider whose constructor throws is never constructed in preview mode, that the request is a POST to /organizations/<owner>/projects/<repository>/builds with an Authorization: ApiKey … header and a compressed graph containing the route controller, and that a missing owner throws GraphPublisher: "owner" parameter is required. It also shows a behaviour to plan for: when the registry answers 401, the publisher prints an error and the process still exits normally, so a wrong API key does not turn the step red. If a failed publish must block the pipeline, check the output or the Devtools GitHub application’s status on the pull request.
Framework upgrades with nest upgrade --dry-run
nest upgrade applies the mechanical parts of a major-version upgrade to an existing project: it bumps every recognised @nestjs/* package to its v12 major, raises TypeScript to v6, migrates nest-cli.json away from the deprecated webpack options, renames the GraphQL playground option to graphiql, switches subscriptions to graphql-ws, and prints notes about behaviour changes it cannot migrate. With --dry-run (alias -d) it reports what it would change and writes nothing, which makes it safe to run in CI. --skip-install skips the package installation and --no-observe skips the prompt that offers to set up @nestjs/observe; --tag next previews against an npm dist-tag instead of the stable ranges.
A scheduled workflow can run the preview once a week and put the report in the run summary, so the team learns about the cost of an upgrade before anyone starts it:
name: Framework upgrade check
on:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: read
jobs:
upgrade-dry-run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- name: Preview the upgrade
run: |
npx nest upgrade --dry-run --skip-install --no-observe | tee upgrade-report.txt
{
echo '## nest upgrade --dry-run'
echo '```text'
cat upgrade-report.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
Source: NestJS: CLI usage - nest upgrade and NestJS: Migration guide.
Running the same command locally on a project whose @nestjs/common, @nestjs/core, @nestjs/platform-express and @nestjs/testing were set back to ^11.0.0 and TypeScript to ^5.7.0 printed:
🚀 Upgrading your project to NestJS v12...
Changes applied:
✔ Updated NestJS packages: @nestjs/common@^12.0.0, @nestjs/core@^12.0.0, @nestjs/testing@^12.0.0, @nestjs/platform-express@^12.0.0, @nestjs/cli@^12.0.0, @nestjs/schematics@^12.0.0
✔ Updated "typescript" from "^5.7.0" to "^6.0.0" (required by @nestjs/cli and @nestjs/schematics v12)
Please review:
âš TypeScript 6 deprecates several legacy "tsconfig.json" options (e.g. "baseUrl", "esModuleInterop" defaults changed). Run "npx tsc --noEmit" after installing and adjust your configuration if needed.
Action required:
➜ Dependencies were not installed (--skip-install). Run your package manager's install command to finish the upgrade.
Full migration guide: https://docs.nestjs.com/migration-guide
UPDATE package.json (1427 bytes)
Dry run enabled. No files written to disk.
Command has been executed in dry run mode, nothing changed!
Three facts about the command shape how to use it in CI. The command exits with status 0 in a dry run even when it reports changes, so the job is informational unless you parse the output. On a project that is already on v12, it reports the v12 package ranges and a note that the migrations are idempotent, so the weekly job is only informative on a project that has not yet upgraded, or with --tag pointing at a prerelease. And the command only touches the local @nestjs/cli: a globally installed CLI has to be updated first with npm i -g @nestjs/cli@latest, which is not an issue in CI where the CLI comes from node_modules. The upgrade itself, as opposed to the preview, belongs on a branch: run nest upgrade, review the diff and the printed report, and let the verify workflow above judge the result.
Best practices
-
Keep the pipeline fast: cache dependencies, cancel superseded runs and run independent jobs in parallel. A pipeline people wait for is a pipeline people bypass.
-
Make the pipeline the only way to ship: protect
main, require theverifychecks and a review, and deploy only frommain. -
Build once, deploy the same artifact: the image built by the pipeline is promoted through environments by digest, never rebuilt per environment.
-
Use the lockfile (
npm ci), pin the Node.js major, and let Dependabot keep actions, packages and the base image current. -
Grant the least privilege:
contents: readby default, per-job escalations, environment secrets, OpenID Connect instead of static cloud keys. -
Treat the logs as public: never print secrets, never run
npm run startwith production credentials in a build job. -
Fail loudly and early: lint and type errors before tests, tests before packaging, coverage thresholds that are enforced.
-
Keep a way back: deployment by digest makes rollback a one-line change, and
terminationGracePeriodSecondsplus graceful shutdown (see Lifecycle Events & Shutdown) make rollouts safe for in-flight requests. -
Measure the pipeline itself: duration, flaky tests, queue time.
Where the book is outdated, and what changed recently
-
The book’s chapter on DevOps builds a GitHub Actions workflow and deploys to a hosting provider or to a server running PM2. The workflow above uses the current majors of every action, a Node.js LTS matrix,
npm ciwith the built-in cache, container images and a gated deployment. -
Linting in a generated project is now oxlint, not ESLint, and
nest newoffers ESM with Vitest or CommonJS with Jest; the scripts are stilllint,test,test:cov,test:e2eandbuild. See Getting Started & CLI. -
nest upgrade(with--dry-run) andnest deployare new in the v12 CLI; the book predates both. -
The Devtools package is
@nestjs/devtools-integration, andGraphPublisheris only useful withsnapshot: true; the CI/CD integration documentation was published after the book. -
Node.js 20 left LTS support on 2026-04-30; a v12 application needs Node.js 20.19+ or 22.12+ to run and the CLI generators need 22.22.3+, 24.15+ or 26+, so a CI matrix should list 22 and 24 (and 26 from its LTS date).