CI/CD automation

CI/CD best practices: from pull request to production

Build a pipeline that catches failures early, releases the version you tested, and gives your team a dependable way to recover.

By Published 17 June 2026Updated 7 October 202612 minute read

A reliable release starts here

Check every change, build from a known commit, control access to production, and confirm that the application works after deployment.

Continuous integration checks changes as developers combine their work. Continuous delivery keeps a tested version ready for release, with a production approval where your team needs one. Continuous deployment takes that final step automatically after the required checks pass.

Start with the part of delivery that causes the most friction: slow feedback, unreliable tests, manual configuration, or difficult recovery. If you need help choosing and implementing the changes, explore our CI/CD automation services.

1. Give each pipeline stage a clear job

A useful pipeline moves from inexpensive checks to more demanding ones. Catch syntax and type errors before starting a long integration suite. Reserve production access for the deployment stage.

  1. Check the change. Review the diff, lint the code, check types where applicable, and run focused unit tests.
  2. Test the connections. Exercise the database, API contracts, authentication, and critical dependencies in an isolated environment.
  3. Build the release. Create the application package or container image and record its commit and digest.
  4. Verify in staging. Use production-like configuration and test the user journeys affected by the change.
  5. Deploy and observe. Release the approved version, run smoke checks, and watch application health before declaring success.

Keep releases small enough to understand and reverse. Infrastructure changes belong in version control too: review a Terraform plan before applying it, and coordinate changes to application code and infrastructure.

2. Make checks fast enough to use and dependable enough to trust

Keep the runtime version and dependency lockfile consistent between developer machines and CI. For npm projects, npm ci installs from the lockfile and fails when it disagrees with package.json. Commit the matching lockfile instead of regenerating it during a build. See the npm ci reference for installation options.

  • Run tests without watch mode so the job finishes with a clear pass or fail.
  • Use isolated test data and predictable clocks where time affects behavior.
  • Give flaky tests an owner and a fix date. Repeatedly rerunning a failure until it passes hides the problem.
  • Cache dependency downloads, then measure whether the cache reduces runtime. Keep credentials and production data out of caches.
  • Set timeouts and keep useful failure logs. Split independent suites only when their runtime justifies the extra jobs.

Require the checks that protect your application before merging. Include dependency and security checks suited to your stack, with a clear process for investigating findings. A build passing by itself says little about whether sign-in, payments, or background jobs still work.

3. Build and test a Node.js app with GitHub Actions

Save this workflow as .github/workflows/node-ci.yml in a Node.js 24 project. It expects a committed package-lock.json, scripts named lint, test:ci, and build, and build output in dist/. For a Vitest project, test:ci can run vitest run; use your test runner's equivalent for other frameworks.

Pull requests targeting main run the same checks as pushes to main. A successful push also saves the build as an artifact named with its commit SHA. The workflow uses GitHub-hosted runners and public npm dependencies; private packages need separately scoped registry access.

name: Node CI

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  verify:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false
      - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
        with:
          node-version: '24'
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run test:ci
      - run: npm run build
      - name: Save the main-branch build
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: site-${{ github.sha }}
          path: dist/
          if-no-files-found: error
          retention-days: 14

The action references pin specific releases of checkout, setup-node, and upload-artifact. Review updates to those references as part of dependency maintenance. The npm cache stores package downloads; dependencies are still installed with npm ci on each run.

Check the workflow before relying on it

  1. Run npm ci, npm run lint, npm run test:ci, and npm run build locally with Node.js 24. Confirm the output directory matches the workflow.
  2. Open a test pull request. Introduce a failing test and confirm the job fails; fix it and confirm the job passes.
  3. After merging, download the artifact from the successful main-branch run. Check that it contains the expected files and no credentials.
  4. Make the job a required check in your repository rules, then connect the successful build to your deployment platform.

The 14-day artifact retention is useful for testing this setup. Choose release storage and retention that cover your recovery needs before using these artifacts for production. GitHub's Node.js workflow guide covers additional package managers and build configurations.

4. Control what can reach production

Configure your deployment platform to accept only the intended branch, successful checks, and approved build. A separate hosting integration may start deploying before GitHub Actions finishes; check its settings so a failed test cannot be followed by a production release.

Promote the same tested artifact where your platform supports it. If a frontend embeds environment settings at build time, test the exact production-configured build before promotion. Record its commit and release identifier so the running version is easy to trace.

Approvals and deployment order

For a GitHub Actions deployment job, reference a production environment and configure its allowed branches and required reviewers where your GitHub plan supports them. An environment name alone does not create an approval requirement. See GitHub's environment settings.

Canceling an outdated test run can save time. Production deployments need a different policy: serialize changes to the same environment, avoid interrupting a migration halfway through, and check that an older run cannot overwrite a newer release. GitHub provides workflow and job concurrency controls for managing overlapping runs.

Keep deployment credentials out of build jobs

Give each job only the permissions it needs. Keep secrets out of source code, logs, browser bundles, and uploaded artifacts. Run untrusted pull request code without production credentials or access to privileged self-hosted runners. Follow GitHub's Actions security guidance when choosing triggers and third-party actions.

For AWS deployments, use short-lived credentials through OIDC where possible. Restrict the role's trust policy to the intended repository and branch or environment, using the subject format your repository actually issues. GitHub's AWS OIDC setup explains the configuration.

5. Make recovery part of the release

Keep a known-good application version available and decide what would trigger a rollback: failed smoke checks, elevated errors, slow critical requests, or a growing queue. Assign someone to watch these signals during the rollout and give them a clear recovery procedure.

  • Check compatibility. The previous application must still work with the current database schema, configuration, and external APIs.
  • Separate destructive migrations. Add compatible schema changes first, migrate usage, and remove old structures only after recovery no longer depends on them.
  • Test the recovery path. Rehearse reverting a release in staging, including application health checks and background processing.
  • Plan data recovery separately. Reverting code does not undo writes or restore deleted data. Understand backup restoration and any acceptable data-loss window.

Canary and blue-green releases can reduce exposure to a bad deployment, but both need working health checks, enough capacity, and compatible data changes. Our fintech case study explains how blue-green delivery and Terraform helped improve a payments platform's release process.

6. Find the cause of recurring pipeline failures

Start with the first failing stage and compare it with the last successful run. On smaller screens, scroll the table to see every column.

ProblemWhat to inspectNext step
Works locally, fails in CIRuntime, lockfile, file-name casing, environment variables, and working directoryReproduce with a clean install and the same commands and runtime.
Tests hang or pass intermittentlyWatch mode, shared data, external services, timeouts, and timing assumptionsUse a single-run test command and isolate the unstable dependency.
Builds wait longer than they runRunner queue time, parallel job limits, and unnecessary duplicate runsMeasure queue and execution time separately before adding runners.
Deployment is blockedEnvironment reviewers, allowed branches, token permissions, and cloud trust policyFind the specific failed requirement and correct it without broadening access.
Deployment passes, application failsDeployed commit, configuration, readiness checks, migrations, and dependency healthStop the rollout and use the tested recovery procedure if health limits are exceeded.

If Kubernetes workloads become unhealthy during releases, use the Kubernetes availability checks to review readiness, rollout capacity, and autoscaling together.

7. Measure the whole delivery process

A faster build helps only if the change reaches users safely. Track time waiting for runners, test execution, review, approval, and deployment. Then compare similar applications and release types over the same period.

  • Feedback time: How long developers wait for the checks they need to act on.
  • Time to production: Time from a recorded code change to that version running in production.
  • Release frequency: Successful production releases per application over a defined period.
  • Failed releases and recovery: Releases needing intervention, and time from failure detection to restored service.
  • Cost per successful run: Runner, test environment, storage, and related infrastructure costs.

Agree the start and end points for each measure so comparisons stay useful. Review faster delivery alongside error rates and recovery time. For expensive build or test environments, the AWS cleanup checklist can help identify resources left running between jobs.

8. Before your next release

  • Required checks pass for the commit being released.
  • The artifact, runtime settings, and destination are identifiable.
  • Deployment access and any required approvals are configured.
  • Only the intended release can update production at that time.
  • Database and configuration changes support the recovery plan.
  • Smoke checks cover a critical user journey, not just an HTTP response.
  • Someone is watching health signals and can stop or reverse the rollout.
  • The previous release remains available for the agreed recovery period.

Improve your release process

Spend less time fixing deployments.

Anrotex helps teams improve GitHub Actions, GitLab CI/CD, and Jenkins pipelines, from automated checks to rollout and recovery. See what our CI/CD consulting service includes, or tell us about the release problem you want to solve.

Discuss your pipeline