跳到正文
DEV Community· Quinn Wang·· 4 小時前AI 評分45

哪個設定步驟真正阻礙首次執行?

Which Setup Step Actually Blocks Your First Run?

AI 導讀

作者以 Bash 計時每個設定步驟,並用 JSONL 記錄耗時及成敗,找出既慢又不可靠的步驟。一次排查中,7 次失敗有 5 次源於 Node 版本不符;加入版本檢查後,可在兩秒內報錯,避免先花三分鐘下載依賴項。文章建議把環境要求設為執行昂貴步驟前的檢查,並以基準環境比對結果。

正文

The step that decides whether a new contributor finishes setup is rarely the step your README spends the most words on. I learned this the boring way: I timed every step of a first run, wrote one JSON line per step, and only then saw which step was really eating the first fifteen minutes. The fix that mattered was not better prose for that step, it was a two-second version check that failed loudly before the expensive work started. What follows is the harness I use now, plus the two habits that stopped me from guessing when a run failed.

Aggregate setup time is a polite fiction

When someone tells you setup takes ten minutes, ask them what they actually measured. They usually measured their own last successful run, on a warm cache, on the machine they have been using for a year. That number is not a distribution, it is a memory, and it says nothing about the person whose first run ends in a stack trace about engines. Would you rather fix a step that is slow, or a step that quietly decides whether people finish at all?

Averages hide the shape of the problem, which is why I stopped reporting totals. A step that passes for eight people in two minutes and fails for two people in forty is not a two-minute step; it is a fork in the road with the sign pointing the wrong way. What I want to know is narrow and answerable: which step is both slow and unreliable, because that is the only one worth changing this week.

A preflight timer that fits in one file

The harness is deliberately dull: bash, one JSONL file per run, no dashboard, no SDK, no telemetry I have to defend in a review. Each step records its wall time and a pass or fail, and the script keeps going after a failure, because the failure you did not record is the one that ruins your afternoon.

#!/usr/bin/env bash
# preflight.sh — time each setup step, record pass/fail as JSONL.
# Unexecuted template: replace the step list with what your repo actually needs.
set -uo pipefail

OUT="${1:-/tmp/preflight-$(date -u +%Y%m%dT%H%M%SZ).jsonl}"
LOG="${OUT%.jsonl}.log"
: > "$OUT"; : > "$LOG"

now_ms() { python3 -c 'import time; print(int(time.time()*1000))'; }

run_step() {
  local id="$1"; shift
  local start end status
  start=$(now_ms)
  if "$@" >>"$LOG" 2>&1; then status=pass; else status=fail; fi
  end=$(now_ms)
  printf '{"step":"%s","ms":%d,"status":"%s"}\n' \
    "$id" "$((end - start))" "$status" >> "$OUT"
}

run_step versions bash -lc 'node --version && python3 --version'
run_step clone    bash -lc 'git clone --depth 1 "$REPO_URL" /tmp/pf-repo'
run_step install  bash -lc 'cd /tmp/pf-repo && npm ci --no-audit --no-fund'
run_step build    bash -lc 'cd /tmp/pf-repo && npm run build --if-present'
run_step smoke    bash -lc 'cd /tmp/pf-repo && npm test -- --bail'

Running it five times on cold caches gives you something to argue with, and jq turns the pile into one sorted answer per step. The blocking field is my own blunt weighting: median milliseconds multiplied by the failure rate plus a small floor, so a fast step that always fails still surfaces near the top.

jq -s '
  group_by(.step)
  | map({step: .[0].step,
         runs: length,
         fails: (map(select(.status=="fail")) | length),
         p50_ms: (map(.ms) | sort | .[length/2 | floor])})
  | map(. + {blocking: (.p50_ms * ((.fails / .runs) + 0.1) | floor)})
  | sort_by(-.blocking)
' /tmp/preflight-*.jsonl

I do not treat that score as science, because it is not; it is a way to end a meeting where everyone has a different favourite step. If the top row is your install step and it fails half the time, you have learned more in one minute than a week of reading complaints. Where is the signal in your own setup, honestly?

The one fix that mattered was a version check

On one repo, five of seven failed runs died in the same place, and it was not the build. People were installing dependencies against a Node major that the lockfile did not want, and the error only appeared after a three-minute download. We had written three careful paragraphs about the right version in the README, which is a lovely artefact that nobody reads at nine in the morning. The fix was a step zero that costs two seconds and names the culprit out loud.

# step 0 — fail in two seconds instead of twenty minutes
want=$(awk '/^nodejs/ {print $2}' .tool-versions)
have=$(node --version 2>/dev/null | sed 's/^v//')
if [ "$have" != "$want" ]; then
  echo "node $have != pinned $want" >&2
  exit 64
fi

Wrap that in run_step versions and it lands in the log with almost no milliseconds and a clean fail, which is exactly what you want from a gate. The lesson generalises beyond Node: move the constraint out of prose and into a check that runs before the expensive work. A fast loud failure is a gift, while a slow quiet one is a support ticket with extra steps.

Is it me, or is it the repo?

The hardest question in a first fifteen minutes is whether the breakage belongs to the repository or to the laptop. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option are supplied by the operator and are the two pieces I reach for here; the terms, quotas and availability change over time, so check the current documentation before you plan around them. The server option is the useful one for triage: run the identical preflight.sh inside a known-good baseline, then diff two files instead of reading tea leaves.

# local run, then the same script against the baseline host
./preflight.sh /tmp/pf-local.jsonl
# run it again in the baseline environment you have access to

diff <(jq -r '"\(.step) \(.status)"' /tmp/pf-local.jsonl | sort) \
     <(jq -r '"\(.step) \(.status)"' /tmp/pf-baseline.jsonl | sort)

If the baseline passes everything and your laptop fails at install, stop rewriting the README and go look at your toolchain. The second piece is smaller: when a log is genuinely ambiguous, a model call can sort it into toolchain, network, permissions or unknown. I keep a keyword classifier as the default path and treat the model as an optional hook, so the script still works offline and on a locked-down runner.

classify() {
  local log="$1"
  grep -qiE 'EACCES|permission denied' "$log" && { echo permissions; return; }
  grep -qiE 'ECONNRESET|ETIMEDOUT|ENOTFOUND' "$log" && { echo network; return; }
  grep -qiE 'engine|unsupported|version' "$log" && { echo toolchain; return; }
  echo unknown   # swap for a model call; read endpoint and model id from current docs
}

What the numbers cannot tell you

One run is an anecdote, so I only trust a row after about five cold-cache runs, and I only trust a failure rate after roughly eight fresh machines across at least two operating systems. Wall clock on a shared CI box is noisy, and a baseline environment drifts away from your users' laptops over time, which makes it a reference point rather than proof. The score also cannot see humiliation: a step that takes four seconds and prints a wall of red text will lose you a contributor just as fast as a slow one.

So who should skip all of this? If your setup is already one command that passes on a clean container, you have solved the problem this article measures, and a devcontainer or Nix flake probably buys you more than a timing harness. If your dependencies are private or your network policy forbids an external baseline, you can still run the per-step timing locally, but the diff loses most of its value. Small teams of two or three contributors may also find that a pinned toolchain file alone gets them most of the way, with no JSONL at all.

If you want a baseline machine without provisioning one, the free server option is the cheapest way I have found to answer the is-it-me question, so run the script twice and compare two boring files. Then ask the only question that matters: which single step would you delete from a stranger's first fifteen minutes today?

來源:DEV Community · dev.to