{"id":55,"date":"2026-08-01T02:12:22","date_gmt":"2026-07-31T23:12:22","guid":{"rendered":"https:\/\/john-nessime.com\/blog\/?p=55"},"modified":"2026-08-01T02:12:23","modified_gmt":"2026-07-31T23:12:23","slug":"fail-fast-pipeline","status":"publish","type":"post","link":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/","title":{"rendered":"Green Check, Broken Build: How to Build a Fail Fast Pipeline That Explains Why"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The deploy went out at the end of the day. Pipeline green, every stage a tick, nobody thought about it again. Twenty minutes later support asks why checkout is returning 502.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You go back through the logs and there it is, four hundred lines up, in a step that reported success: a download that returned an HTML error page, got piped into <code>tar<\/code>, and produced an archive containing nothing useful. The shell reported the exit status of <code>tar<\/code>, not of <code>curl<\/code>. The build carried on. The pipeline was never lying on purpose. It just never asked the right question.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two things go wrong with pipelines, and only one of them gets talked about. The famous one is slow feedback: thirty-four minutes of build and integration tests before a linter tells you a file is missing a trailing newline. The expensive one is a pipeline that passes when it should have stopped. A <strong>fail fast pipeline<\/strong> fixes the first problem. Making it explain itself fixes the second, and that is the half most teams skip.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This post covers both: how to order stages so real failures surface in seconds, how to stop the shell from hiding non-zero exits, how to design failure output that hands the next engineer the fix, and the troubleshooting steps for when a pipeline stubbornly refuses to fail at the right moment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The failure that actually bites: a pipeline that lies<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start here, because everything else is optimisation on top. A slow pipeline wastes your afternoon. A silently passing pipeline ships a broken artifact and burns a day of debugging in production, usually while somebody is on a call with a customer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The shell is where most of this happens. Bash gives you three defaults that are wrong for CI, and one line fixes all three.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env bash\nset -euo pipefail\n\n# Without pipefail this line exits 0 even when curl fails,\n# because the shell reports the status of the LAST command\n# in the pipe, which is tar.\ncurl -sSL https:\/\/example.com\/artifact.tar.gz | tar -xz<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">What each flag actually protects<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>-e<\/code><\/strong> stops the script when a command exits non-zero. Without it, a failed database migration is followed by a successful <code>echo<\/code>, and the script exits 0 because the last command succeeded.<\/li>\n<li><strong><code>-u<\/code><\/strong> turns an unset variable into an error. This is the one that saves you from <code>rm -rf \"$PREFIX\/build\"<\/code> expanding to <code>rm -rf \"\/build\"<\/code> when someone renames an environment variable in the pipeline settings and forgets the script.<\/li>\n<li><strong><code>-o pipefail<\/code><\/strong> makes a pipeline return the first non-zero status instead of the last one. Every <code>curl | tar<\/code>, <code>cat | jq<\/code>, and <code>docker logs | grep<\/code> in your scripts is currently reporting only the exit code of the thing on the right.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The exit status is only half the story though. Plenty of tools return 0 while doing something useless, and <code>curl<\/code> is the classic offender.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># curl exits 0 here even when the server returns 500,\n# and you end up untarring an HTML error page.\ncurl -sSL \"$ARTIFACT_URL\" | tar -xz\n\n# --fail          makes curl exit non-zero on 4xx and 5xx\n# --show-error    keeps the reason visible even with --silent\n# --location      follows redirects instead of saving a 302 body\ncurl --fail --silent --show-error --location \"$ARTIFACT_URL\" | tar -xz<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now the download failing stops the build, and <code>pipefail<\/code> makes sure that failure propagates past the pipe. The first fixes the check, the second makes sure the check is heard.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Where <code>set -e<\/code> quietly gives up<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This trips people up constantly. <code>set -e<\/code> is suspended whenever a command&#8217;s exit status is being consumed by something else. It is not a safety net, it is a default.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>set -e\n\n# -e does NOT stop the script here.\n# The if statement consumes the exit status.\nif grep -q \"TODO\" src\/main.go; then\n  echo \"found\"\nfi\n\n# -e does NOT stop the script here either.\n# The left side of || is explicitly allowed to fail.\nrun_migrations || echo \"migrations failed, continuing anyway\"\n\n# -e DOES stop the script here.\nrun_migrations<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The same trap applies inside containers. On Debian-based images <code>\/bin\/sh<\/code> is <code>dash<\/code>, which has no <code>pipefail<\/code> option at all, so putting <code>set -o pipefail<\/code> inside a <code>RUN<\/code> line fails outright. Set the shell at the top of the Dockerfile instead. <a href=\"https:\/\/github.com\/hadolint\/hadolint\" target=\"_blank\" rel=\"noreferrer noopener\">Hadolint<\/a> flags this as DL4006 and it is worth wiring into your static stage.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Set this before any RUN instruction that contains a pipe.\nSHELL [\"\/bin\/bash\", \"-o\", \"pipefail\", \"-c\"]\n\nRUN curl --fail --silent --show-error --location \n      https:\/\/example.com\/tool.tar.gz | tar -xz -C \/usr\/local\/bin<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Order stages by cost, not by category<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most pipelines are laid out the way the team thinks about work: tests, then build, then security, then deploy. That grouping is comfortable and it is the wrong axis. Order by two things instead, cost and failure probability.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The rule I use: <strong>every stage should be cheaper than the one after it and more likely to fail than the one after it.<\/strong> If a check catches ten percent of your broken commits and takes fifteen seconds, it goes in front of the check that catches two percent and takes nine minutes. That is the whole design principle behind a fail fast pipeline.<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Syntax and static analysis.<\/strong> <code>shellcheck<\/code>, <code>hadolint<\/code>, <code>yamllint<\/code>, <code>terraform validate<\/code>, <code>tflint<\/code>, formatter checks, type checks. Seconds, no dependencies, no network. This stage catches an embarrassing share of broken pushes.<\/li>\n<li><strong>Unit tests.<\/strong> No database, no containers, no network. If they need any of those, they are integration tests and belong further down.<\/li>\n<li><strong>Secrets and dependency scanning.<\/strong> Fast, and a leaked credential is worth stopping before you build an image around it.<\/li>\n<li><strong>Build.<\/strong> This is where you start spending real minutes and cache space.<\/li>\n<li><strong>Integration and end-to-end.<\/strong> Slow, flaky, expensive to run. Everything above should have already caught the boring failures.<\/li>\n<li><strong>Deploy and post-deploy verification.<\/strong><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">A note on the trade-off, because this ordering costs something. Splitting the fast checks into their own stage adds a checkout and a container pull before your unit tests even start. On a busy shared runner that overhead is real. If your static stage takes twelve seconds of work and forty seconds of setup, merge it into the test job and run it as the first step instead. The principle is failure order, not job count.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The other thing that keeps the fast lane fast is caching, and it is the piece people get wrong in the opposite direction: they cache so aggressively that a stale dependency tree hides a genuine break. Cache the package manager&#8217;s download directory, not the resolved dependency tree, and key the cache on the lockfile hash.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Let the platform kill work you already know is doomed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ordering stages is only useful if the platform actually stops when something breaks. Three settings do most of the work, and they exist under different names on every runner.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GitHub Actions<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>concurrency:\n  # A new push to the same branch cancels the run that is\n  # already in flight. Nobody needs results for a commit\n  # that has been superseded.\n  group: ci-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  static:\n    runs-on: ubuntu-latest\n    timeout-minutes: 5          # a hung job is worse than a failed one\n    steps:\n      - uses: actions\/checkout@v4   # pin to a full commit SHA in production\n      - run: shellcheck scripts\/*.sh\n      - run: hadolint Dockerfile\n\n  test:\n    needs: static               # will not start until static passes\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    strategy:\n      fail-fast: false          # see note below\n      matrix:\n        suite: [unit, contract]\n    steps:\n      - uses: actions\/checkout@v4\n      - run: make test-${{ matrix.suite }}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That <code>fail-fast: false<\/code> looks like it contradicts the whole post, so it is worth explaining. Matrix fail-fast defaults to true: one leg fails, the rest get cancelled. That is right when the legs are redundant and you only need to know that something broke. It is wrong when the legs tell you different things. If your matrix is three runtime versions and only one fails, cancelling the others hides whether this is a version-specific problem or a general one, and you will burn a rerun finding out. Fail fast on the pipeline, not on the diagnosis.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>timeout-minutes<\/code> is the setting people skip and regret. A job waiting on an interactive prompt, a dead package mirror, or a container that never becomes healthy will sit there consuming a runner until someone notices. Set a timeout on every job, generous enough that a slow day does not trip it and tight enough that a hang gets caught inside one coffee break.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">GitLab CI<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>stages: [static, test, build, deploy]\n\ndefault:\n  interruptible: true     # allows auto-cancel when a newer pipeline starts\n  retry:\n    max: 2                # capped at 2 by GitLab, so 3 attempts total\n    when:\n      # Retry only on infrastructure problems.\n      # Never retry script_failure: that is your code failing,\n      # and retrying it just hides a flaky test.\n      - runner_system_failure\n      - stuck_or_timeout_failure\n      - api_failure\n\nlint:\n  stage: static\n  timeout: 5m\n  script:\n    - shellcheck scripts\/*.sh\n\nintegration:\n  stage: test\n  needs: [lint]           # DAG: starts as soon as lint passes\n  script:\n    - make integration<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>retry: when<\/code> list is the important bit and it is the one people get lazy about. A bare <code>retry: 2<\/code> retries on everything including a genuine test failure, which converts an intermittent bug into an invisible one. Scope retries to the failure classes that are genuinely the infrastructure&#8217;s fault. Jenkins has <code>failFast true<\/code> for parallel stages, Buildkite and CircleCI have their own equivalents, and the reasoning transfers cleanly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A failure should hand you the answer, not the search<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the test I apply to a pipeline failure. Can someone who did not write the change, at an inconvenient hour, fix it from the failure output alone without opening the source? If not, the failure message is incomplete.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Four things need to be in there:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>What failed<\/strong>, named precisely. Not &#8220;build failed&#8221; but the command and its exit code.<\/li>\n<li><strong>Why<\/strong>, in terms of the input rather than the internals. &#8220;lockfile is out of date with package.json&#8221;, not &#8220;npm ci exited 1&#8221;.<\/li>\n<li><strong>Where<\/strong>, meaning the file, the line, or the config key that caused it.<\/li>\n<li><strong>What to do next.<\/strong> One concrete action, even if it is just a runbook link.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">In shell, a small helper plus an <code>ERR<\/code> trap gets you most of the way there.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env bash\nset -euo pipefail\n\nfail() {\n  echo \"ERROR: $1\" &gt;&amp;2\n  echo \"  fix: $2\" &gt;&amp;2\n  exit 1\n}\n\n# Catches anything you did not write an explicit check for,\n# and tells you the line and the command that blew up.\ntrap 'echo \"FAILED at line $LINENO: $BASH_COMMAND\" &gt;&amp;2' ERR\n\n[[ -f config\/app.yaml ]] || fail \n  \"config\/app.yaml is missing\" \n  \"copy config\/app.yaml.example and set DATABASE_URL\"\n\n[[ -n \"${IMAGE_TAG:-}\" ]] || fail \n  \"IMAGE_TAG is empty\" \n  \"check the tag job output; it sets IMAGE_TAG from the git SHA\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note <code>${IMAGE_TAG:-}<\/code> rather than <code>$IMAGE_TAG<\/code>. Under <code>set -u<\/code> the bare form aborts with a shell error before your nice message ever prints, which is technically correct and practically useless.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On GitHub Actions you can go one step further and attach the error to the file and line, so it shows up as an annotation on the pull request instead of something to be dug out of a log.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Renders as a red annotation on that exact line in the diff view.\necho \"::error file=deploy\/values.yaml,line=42::image tag is empty\"\n\n# Visible, but does not fail the job.\necho \"::warning file=Dockerfile,line=7::apt cache not cleaned\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Two more habits worth building. Upload logs, test reports, and screenshots as artifacts on failure, conditionally, so the evidence survives the job that produced it. And emit test results as JUnit XML so the platform renders a list of failed test names instead of asking people to scroll. Both are cheap and both cut the time between &#8220;it broke&#8221; and &#8220;I know why&#8221; more than any clever retry logic will.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Exit codes are a contract, so use more than one<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most scripts have two states: 0 and &#8220;something went wrong&#8221;. That collapses distinctions the pipeline needs. Terraform gets this right and it is a good model to copy.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># -detailed-exitcode changes the meaning of the exit code:\n#   0 = succeeded, no changes\n#   1 = error\n#   2 = succeeded, changes present\nset +e\nterraform plan -detailed-exitcode -out=tfplan\ncode=$?\nset -e\n\ncase \"$code\" in\n  0) echo \"No drift. Skipping apply.\" ;;\n  2) echo \"Changes detected. Holding for approval.\" ;;\n  *) echo \"Plan failed. See log above.\" &gt;&amp;2; exit \"$code\" ;;\nesac<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>set +e<\/code> around it is deliberate: with <code>-e<\/code> active, exit code 2 would kill the script before you got to interpret it. This is one of the few places where turning the flag off is the correct move, and it is worth a comment in the file explaining why so nobody &#8220;fixes&#8221; it later.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do the same in your own scripts. Reserve a code for &#8220;the thing you asked for is not possible&#8221; versus &#8220;I could not reach the API&#8221; versus &#8220;your input is wrong&#8221;. The pipeline can then retry one and refuse to retry the others, which is the difference between a retry that absorbs a runner hiccup and a retry that hides a race condition for six months.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Gate on what you care about, not what is easy to check<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This is the deploy-stage version of the same lie. Plenty of commands confirm that a request was accepted, not that the thing you wanted happened.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># Returns 0 the moment the API server accepts the manifest,\n# even if every pod goes into CrashLoopBackOff a second later.\nkubectl apply -f k8s\/\n\n# This is the step that actually waits for the rollout,\n# and fails the job if it does not converge.\nkubectl rollout status deployment\/api --timeout=180s\n\n# And this proves the application answers a request,\n# not just that a pod reports Ready.\ncurl --fail --silent --show-error https:\/\/api.example.com\/healthz<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run that last check against the public hostname, through your CDN, rather than against the pod directly. If you front the app with Cloudflare or similar, a smoke test that skips the edge will happily pass while real users hit a cached error page or a misrouted origin. Same reasoning applies to the origin host itself: if the app runs on a VPS from a provider like InterServer behind a reverse proxy, verify through the proxy, because that is the path traffic takes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then track how long each stage takes over time. Pipeline duration and failure rate per stage are perfectly ordinary metrics to ship into Prometheus and graph in Grafana, or into whatever platform you already use for application monitoring. When the fast lane creeps from forty seconds to four minutes, that is a slow regression nobody will notice from a single run.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Troubleshooting<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The job passed but the artifact is wrong<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Look for pipes first. Grep the scripts for <code>|<\/code> and check whether <code>pipefail<\/code> is set in that shell. Then check for tools that return 0 on failure: <code>curl<\/code> without <code>--fail<\/code>, <code>docker run<\/code> where the entrypoint swallows errors, anything wrapped in <code>|| true<\/code> that somebody added during an incident and never removed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>set -e<\/code> is set but the script keeps going<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The failing command is almost certainly inside a condition, on the left of <code>||<\/code> or <code>&amp;&amp;<\/code>, or in a function that is itself being called from an <code>if<\/code>. Add <code>set -x<\/code> temporarily and read the trace to see exactly where control flow continued. Then use an explicit check with your <code>fail<\/code> helper rather than relying on <code>-e<\/code> at that point.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Works locally, fails in CI (or the reverse)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Usually a shell difference. Your terminal runs bash or zsh, the runner may execute the script with <code>sh<\/code>. Add an explicit shebang, and on GitHub Actions set the shell on the step or at the job level rather than assuming. Inside containers, remember <code>dash<\/code> has no <code>pipefail<\/code> and no <code>[[ ]]<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The job hangs until the platform kills it<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Something is waiting on stdin. Package managers prompting for confirmation, <code>ssh<\/code> asking about a host key, a database client waiting for a password. Add the non-interactive flags, set <code>CI=true<\/code> where tools respect it, and put a timeout on the job so the next occurrence fails in ten minutes instead of sixty.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Fails on the first run, passes on rerun<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Resist the retry. Log the failure with enough context to identify it later, quarantine the test into a non-blocking job if it is genuinely blocking the team, and give it an owner and a date. A blanket retry turns a flaky test into a permanent tax on everyone&#8217;s confidence in the pipeline.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The failure output is a wall of noise<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Suppress progress bars and spinners in CI, they render as thousands of unreadable lines. Then print a short summary block at the very end of the job, after the noise, listing what failed and what to do. People read the bottom of a log first.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Common mistakes<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Writing <code>set -e<\/code> without <code>-o pipefail<\/code> and assuming the script is covered.<\/li>\n<li>Using <code>|| true<\/code> to silence a step that fails intermittently, instead of finding out why.<\/li>\n<li>Running the slowest, flakiest checks before the ones that take fifteen seconds.<\/li>\n<li>Retrying on every failure class rather than only infrastructure faults.<\/li>\n<li>No job timeouts, so a hang consumes a runner until someone notices.<\/li>\n<li>Treating <code>kubectl apply<\/code> or a deploy API call as proof the deploy worked.<\/li>\n<li>Failure messages that name the internal function that threw, and nothing the reader can act on.<\/li>\n<li>Caching so aggressively that a stale dependency tree hides a real break.<\/li>\n<li>Discarding logs and test reports when the job fails, which is exactly when you need them.<\/li>\n<li>Turning off matrix fail-fast everywhere by reflex, so every broken commit costs the full matrix.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Best practices<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Put <code>set -euo pipefail<\/code> at the top of every shell script the pipeline runs, and set the Dockerfile <code>SHELL<\/code> before any piped <code>RUN<\/code>.<\/li>\n<li>Order stages so each one is cheaper and more likely to fail than the next.<\/li>\n<li>Give every job a timeout, and cancel superseded runs on the same branch.<\/li>\n<li>Use explicit checks with actionable messages instead of relying on <code>-e<\/code> to catch everything.<\/li>\n<li>Distinguish exit codes so the pipeline can react differently to different failures.<\/li>\n<li>Verify deploys with a rollout status check plus a real HTTP request through the public path.<\/li>\n<li>Upload artifacts and structured test reports on failure.<\/li>\n<li>Pin action and image references to immutable digests so a green pipeline stays reproducible.<\/li>\n<li>Track stage duration and failure rate over time, the same way you track application metrics.<\/li>\n<li>Fix the failure message the first time somebody has to ask what it means.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">FAQ<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">What does &#8220;fail fast&#8221; actually mean in a CI\/CD pipeline?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Two things. First, the pipeline stops as soon as it knows the result will be a failure, rather than finishing work whose outcome no longer matters. Second, the cheapest checks that are most likely to catch a problem run first, so the average failure surfaces in seconds. The second half is a design choice about ordering, not a platform setting.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Is <code>set -e<\/code> enough on its own?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">No. It is suspended inside conditions, on the left side of <code>&amp;&amp;<\/code> and <code>||<\/code>, and after <code>!<\/code>. It also does nothing about pipes, which is why <code>-o pipefail<\/code> matters, or about commands that return 0 while failing, which is why explicit checks matter. Use it as a baseline, not as your only line of defence.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Should I disable matrix fail-fast?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Only when the legs give you different diagnostic information. If the matrix is runtime versions or operating systems and you need to know whether the break is universal or specific, let all legs run. If the legs are interchangeable shards of the same test suite, leave fail-fast on and save the compute.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How do I stop a pipeline from passing when a step really failed?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Work through three things in order. Set <code>pipefail<\/code> so pipes propagate failure. Add the flags that make tools return non-zero on failure, such as <code>curl --fail<\/code>. Then replace implicit trust with explicit assertions: after a deploy, check that the rollout converged and that the service answers a request. Most silent passes come from one of those three gaps.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Are automatic retries a good idea?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Only for failures that are the infrastructure&#8217;s fault: runner crashes, image pull errors, API timeouts from the CI platform itself. Never retry a script failure by default. And make retries visible, because a job that quietly succeeds on the third attempt is telling you something important that nobody sees.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">How long should a fast feedback stage take?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Short enough that an engineer waits for it rather than switching context. Somewhere under a couple of minutes for the combined lint, format, and unit stage is a reasonable target for most codebases. Past that, people push and walk away, and you lose the benefit of fast feedback even though the pipeline is technically quick.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Does this apply to self-hosted runners too?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">All of it, and timeouts matter more. On hosted runners a stuck job eventually hits the platform&#8217;s own ceiling. On your own machine it holds a slot until you intervene, so a hung job blocks the queue for everyone. Set explicit timeouts and make sure the runner cleans up its workspace between jobs.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The one thing to take away<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Speed is the part everyone optimises and honesty is the part that actually costs money. A fail fast pipeline that finishes in ninety seconds is worth very little if one of those seconds contains a swallowed exit code. Fix the lying first: <code>pipefail<\/code>, tools that return non-zero when they fail, and assertions that check the outcome rather than the request. Then reorder for speed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And judge every failure by one question. Could someone else fix this from the output alone, without reading the source? If the answer is no, the pipeline has not finished its job, no matter how quickly it went red.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Need help fixing a pipeline that lies to you?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If any of this sounds like your CI, this is the kind of work I take on. Typical engagements look like:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Auditing an existing GitHub Actions, GitLab CI, or Jenkins pipeline for silently swallowed failures and false-green stages.<\/li>\n<li>Restructuring stage order and caching so the common failure surfaces in seconds instead of after the build.<\/li>\n<li>Hardening deploy verification: rollout gates, smoke tests through the real public path, and automatic rollback on a failed health check.<\/li>\n<li>Rewriting pipeline shell scripts with proper error handling, exit code contracts, and failure messages an on-call engineer can act on.<\/li>\n<li>Wiring pipeline duration and failure metrics into Prometheus and Grafana so regressions get caught early.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Send me the workflow file and a link to a failing run, and I will tell you what I would change first.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-layout-flex wp-block-buttons-is-layout-flex\">\n<div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/www.upwork.com\/freelancers\/~01f15a912ad84a6620\" target=\"_blank\" rel=\"noreferrer noopener\">Work with me on Upwork<\/a><\/div>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>A green pipeline that ships a broken build is worse than a red one. Here is how to order stages so failures surface in seconds, stop shell pipes from swallowing exit codes, and write failure output that tells the next engineer exactly what to fix.<\/p>\n","protected":false},"author":1,"featured_media":56,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24,52,31],"tags":[94,120,95,3,8,122,101,119,116,121,124,10,123,118,88,4],"class_list":["post-55","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-technical-guides","category-troubleshooting","tag-automation","tag-bash","tag-ci-cd","tag-devops","tag-docker","tag-exit-codes","tag-github-actions","tag-gitlab-ci","tag-logging","tag-pipefail","tag-pipeline-design","tag-production","tag-shell-scripting","tag-sre","tag-terraform","tag-troubleshooting","entry","has-media"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Fail Fast Pipeline Design: Break Early, Explain Why<\/title>\n<meta name=\"description\" content=\"A fail fast pipeline saves hours only if it says why it broke. Stage order, pipefail, exit codes, timeouts and error output that points at the fix.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Fail Fast Pipeline Design: Break Early, Explain Why\" \/>\n<meta property=\"og:description\" content=\"A fail fast pipeline saves hours only if it says why it broke. Stage order, pipefail, exit codes, timeouts and error output that points at the fix.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/\" \/>\n<meta property=\"og:site_name\" content=\"John Nessime\" \/>\n<meta property=\"article:published_time\" content=\"2026-07-31T23:12:22+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-31T23:12:23+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/fail-fast-pipeline.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"800\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"John Nessime\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"John Nessime\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"14 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/\"},\"author\":{\"name\":\"John Nessime\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"headline\":\"Green Check, Broken Build: How to Build a Fail Fast Pipeline That Explains Why\",\"datePublished\":\"2026-07-31T23:12:22+00:00\",\"dateModified\":\"2026-07-31T23:12:23+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/\"},\"wordCount\":3028,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/fail-fast-pipeline.png\",\"keywords\":[\"Automation\",\"Bash\",\"CI\\\/CD\",\"DevOps\",\"Docker\",\"Exit Codes\",\"GitHub Actions\",\"GitLab CI\",\"Logging\",\"Pipefail\",\"Pipeline Design\",\"Production\",\"Shell Scripting\",\"SRE\",\"Terraform\",\"Troubleshooting\"],\"articleSection\":[\"DevOps\",\"Technical Guides\",\"Troubleshooting\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/\",\"name\":\"Fail Fast Pipeline Design: Break Early, Explain Why\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/fail-fast-pipeline.png\",\"datePublished\":\"2026-07-31T23:12:22+00:00\",\"dateModified\":\"2026-07-31T23:12:23+00:00\",\"description\":\"A fail fast pipeline saves hours only if it says why it broke. Stage order, pipefail, exit codes, timeouts and error output that points at the fix.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#primaryimage\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/fail-fast-pipeline.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/08\\\/fail-fast-pipeline.png\",\"width\":1200,\"height\":800,\"caption\":\"Diagram of a fail fast CI pipeline: stage order with the build failing at exit 1 and later stages cancelled, beside a panel breaking a good failure message into what, why, where and fix.\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/devops\\\/fail-fast-pipeline\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Green Check, Broken Build: How to Build a Fail Fast Pipeline That Explains Why\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/\",\"name\":\"John Nessime\",\"description\":\"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained\",\"publisher\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/#\\\/schema\\\/person\\\/ede0b56d0c808f123f57d5d796902105\",\"name\":\"John Nessime\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"contentUrl\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\",\"width\":512,\"height\":512,\"caption\":\"John Nessime\"},\"logo\":{\"@id\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/cropped-jn.png\"},\"description\":\"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.\",\"sameAs\":[\"https:\\\/\\\/john-nessime.com\\\/blog\",\"https:\\\/\\\/www.linkedin.com\\\/in\\\/john-m-nessime\"],\"url\":\"https:\\\/\\\/john-nessime.com\\\/blog\\\/author\\\/johnnessime\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Fail Fast Pipeline Design: Break Early, Explain Why","description":"A fail fast pipeline saves hours only if it says why it broke. Stage order, pipefail, exit codes, timeouts and error output that points at the fix.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/","og_locale":"en_US","og_type":"article","og_title":"Fail Fast Pipeline Design: Break Early, Explain Why","og_description":"A fail fast pipeline saves hours only if it says why it broke. Stage order, pipefail, exit codes, timeouts and error output that points at the fix.","og_url":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/","og_site_name":"John Nessime","article_published_time":"2026-07-31T23:12:22+00:00","article_modified_time":"2026-07-31T23:12:23+00:00","og_image":[{"width":1200,"height":800,"url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/fail-fast-pipeline.png","type":"image\/png"}],"author":"John Nessime","twitter_card":"summary_large_image","twitter_misc":{"Written by":"John Nessime","Est. reading time":"14 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#article","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/"},"author":{"name":"John Nessime","@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"headline":"Green Check, Broken Build: How to Build a Fail Fast Pipeline That Explains Why","datePublished":"2026-07-31T23:12:22+00:00","dateModified":"2026-07-31T23:12:23+00:00","mainEntityOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/"},"wordCount":3028,"commentCount":0,"publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/fail-fast-pipeline.png","keywords":["Automation","Bash","CI\/CD","DevOps","Docker","Exit Codes","GitHub Actions","GitLab CI","Logging","Pipefail","Pipeline Design","Production","Shell Scripting","SRE","Terraform","Troubleshooting"],"articleSection":["DevOps","Technical Guides","Troubleshooting"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/","url":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/","name":"Fail Fast Pipeline Design: Break Early, Explain Why","isPartOf":{"@id":"https:\/\/john-nessime.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#primaryimage"},"image":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#primaryimage"},"thumbnailUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/fail-fast-pipeline.png","datePublished":"2026-07-31T23:12:22+00:00","dateModified":"2026-07-31T23:12:23+00:00","description":"A fail fast pipeline saves hours only if it says why it broke. Stage order, pipefail, exit codes, timeouts and error output that points at the fix.","breadcrumb":{"@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#primaryimage","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/fail-fast-pipeline.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/08\/fail-fast-pipeline.png","width":1200,"height":800,"caption":"Diagram of a fail fast CI pipeline: stage order with the build failing at exit 1 and later stages cancelled, beside a panel breaking a good failure message into what, why, where and fix."},{"@type":"BreadcrumbList","@id":"https:\/\/john-nessime.com\/blog\/devops\/fail-fast-pipeline\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/john-nessime.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Green Check, Broken Build: How to Build a Fail Fast Pipeline That Explains Why"}]},{"@type":"WebSite","@id":"https:\/\/john-nessime.com\/blog\/#website","url":"https:\/\/john-nessime.com\/blog\/","name":"John Nessime","description":"Cloud, DevOps, Data &amp; AI \u2014 Built, Tested, Explained","publisher":{"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/john-nessime.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/john-nessime.com\/blog\/#\/schema\/person\/ede0b56d0c808f123f57d5d796902105","name":"John Nessime","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","url":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","contentUrl":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png","width":512,"height":512,"caption":"John Nessime"},"logo":{"@id":"https:\/\/john-nessime.com\/blog\/wp-content\/uploads\/2026\/07\/cropped-jn.png"},"description":"AWS Certified Solutions Architect helping businesses build reliable cloud, data, reporting, and automation solutions. I help startups, agencies, and growing businesses replace manual processes and disconnected data with practical AWS architectures, clean data pipelines, useful dashboards, and maintainable automation.","sameAs":["https:\/\/john-nessime.com\/blog","https:\/\/www.linkedin.com\/in\/john-m-nessime"],"url":"https:\/\/john-nessime.com\/blog\/author\/johnnessime\/"}]}},"_links":{"self":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/55","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/comments?post=55"}],"version-history":[{"count":1,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/55\/revisions"}],"predecessor-version":[{"id":57,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/posts\/55\/revisions\/57"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media\/56"}],"wp:attachment":[{"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/media?parent=55"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/categories?post=55"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/john-nessime.com\/blog\/wp-json\/wp\/v2\/tags?post=55"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}