Practical PlaybooksTerms & contact

Free guides

Nightly builds fail for boring reasons far more often than interesting ones. The Node version on the runner drifts from what the project expects, a stale node_modules hides a dependency change, or a build log gets overwritten before anyone reads it. Shell aliases are a lightweight way to fix this: you encode the steps once, in your shell config, and every run follows the same sequence.

This article teaches one complete technique: building a small set of aliases that pin the environment, run the build with timing and a persistent log, and fail loudly when something goes wrong. You can use it without any external tooling beyond bash or zsh, nvm, and npm.

The goal: one command, one predictable result

A good nightly alias should do four things:

  1. Select the same Node version every time.
  2. Start from a clean dependency state, so results don't depend on leftovers.
  3. Record timing, exit status, and output to a dated file.
  4. Propagate the real exit code so cron, CI wrappers, or your own scripts can react.

Put the aliases in ~/.bashrc, ~/.zshrc, or ~/.config/fish/config.fish (fish uses alias with slightly different quoting, so test it). After editing, run source ~/.bashrc or open a new terminal.

Step 1: Pin the Node version

The most common nightly failure is an environment mismatch. Commit an .nvmrc file to the project root containing the exact version you target, for example 20.11.1. Then write:

alias nvm-use='nvm use $(cat .nvmrc 2>/dev/null || echo 20)'

The fallback matters. If .nvmrc is missing, the alias silently chooses a default, so it is worth adding a check that prints a warning in that case. A simple version:

nvm-use() {
  if [ ! -f .nvmrc ]; then
    echo "warning: no .nvmrc found, falling back to Node 20" >&2
  fi
  nvm use "$(cat .nvmrc 2>/dev/null || echo 20)"
}

Using a function instead of an alias gives you room for logic. Prerequisites: nvm installed, and the version listed in .nvmrc actually available locally (nvm install it once).

Step 2: Clean install for reproducibility

A nightly build should not trust whatever is sitting in node_modules. A clean install with the lockfile is the standard approach:

npm-clean-install() {
  rm -rf node_modules
  npm ci
}

Note the choice of npm ci over npm install. npm ci installs exactly what package-lock.json specifies and fails if the lockfile and package.json disagree, which is what you want from an unattended job. Avoid deleting the lockfile in the nightly path; if you delete it, you lose the reproducibility you are trying to build. Clearing the npm cache is rarely necessary and can slow runs down, so reserve it for debugging a corrupted cache.

Step 3: Build with timing and a dated log

The build alias should run the script, capture its real exit status, and write everything to a file named by date. Writing a function keeps the logic readable:

build-nightly() {
  local logfile="nightly-build-$(date +%Y%m%d).log"
  local start end status
  start=$(date +%s)
  {
    echo "=== Nightly build started $(date) ==="
    npm run build
    status=$?
    end=$(date +%s)
    echo "=== Finished $(date) (duration: $((end - start))s, exit: ${status}) ==="
  } 2>&1 | tee -a "$logfile"
  # PIPESTATUS keeps the npm exit code, not tee's
  return ${PIPESTATUS[0]:-$status}
}

Two details are easy to get wrong. First, tee masks failures in pipelines, so PIPESTATUS is needed to recover the exit code of the command that actually ran. Second, log to a dated file with tee -a (append) rather than overwriting, so a rerun on the same day does not destroy earlier evidence. If your build tool writes its own log file, such as build.log, append its tail to the nightly file explicitly, and check that the file exists first.

Step 4: Run tests and fail on any problem

The test alias follows the same pattern, but the important rule is that it must return a non-zero status when tests fail, so that schedulers notice:

test-nightly() {
  npm test
  local status=$?
  if [ "$status" -ne 0 ]; then
    echo "Nightly tests failed on $(hostname) at $(date)" >&2
  fi
  return $status
}

If you want notifications, keep the endpoint out of your config file. Read it from an environment variable such as SLACK_WEBHOOK_URL and skip the call when it is unset, so the alias works on machines without that variable.

Step 5: Compose the nightly run

With the pieces in place, a single entry point ties them together and stops at the first failure:

nightly() {
  nvm-use || return 1
  npm-clean-install || return 1
  build-nightly || return 1
  test-nightly || return 1
  echo "Nightly run completed successfully at $(date)"
}

The || return 1 chain is the key safety property. Without it, a failed install would still attempt a build against a broken tree and produce misleading logs.

Schedule it with cron if you want it unattended. Cron does not load your interactive shell config by default, so call the function through a login shell:

0 2 * * * bash -lc 'cd /path/to/project && source ~/.bashrc && nightly' >> ~/nightly-cron.log 2>&1

Common pitfalls

Checklist before you rely on it

Run the deliberate-failure check once. A nightly job you have never seen fail is one you cannot trust. Once the aliases behave correctly on your own machine, the same file can be copied to a second machine and should produce the same sequence of steps and the same logs, which is the real measure of reproducibility.

If you are setting this up for a Node project for the first time, it helps to audit the project's scripts, lockfile health, and Node version policy before you automate anything. The steps above assume those basics are already sound.


Written with AI assistance and reviewed for accuracy before publishing.

Want the complete version? This guide draws on Nightly Build Automation Bundle for Node Projects (EUR 39, free sample chapter on the page).

This guide was researched and written with the help of AI and reviewed by an automated quality check. It is general educational information, not professional advice.

Rather have it done? Heracles' processing desk can summarise long docs or logs (€3), pull fields out to JSON (€5), clean a CSV (€6), review an AI answer (€5) - AI-performed, QA-checked, refunded automatically if it cannot be done well.