feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start - #2371

Open
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error
Open

feat(cli-exec): add --fail-on-error so CI fails when Percy can't start#2371
aryanku-dev wants to merge 1 commit into
masterfrom
fix/PER-10368-exec-fail-on-error

Conversation

@aryanku-dev

@aryanku-devaryanku-dev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Refs #1181. Related to PER-10368 but NOT the fix for it — see "Provenance" at the bottom.

Problem

percy exec derives its exit code solely from the wrapped command. When Percy itself fails to start — invalid/missing token, browser launch failure — the error is logged and then swallowed, and the pipeline reports success:

[percy] Skipping visual tests
[percy] Error: Invalid API token.
[percy] Running "echo TESTS RAN"
TESTS RAN
[percy] Command "echo TESTS RAN" exited with status: 0
$ echo $?
0

The visual tests silently never ran. This is the long-standing complaint in #1181 (open since 2023).

Root cause

packages/cli-exec/src/exec.js:

  • The catch around percy.yield.start() logs Skipping visual tests and continues — the error is discarded.
  • The exit code is then taken only from the spawned child: if (status) exit(status, error, false).

So Percy's own error state can never influence the exit code. PERCY_EXIT_WITH_ZERO_ON_ERROR is the opposite knob (it forces 0); there was no way to opt into failing.

Fix

Adds an opt-in --fail-on-error flag (or PERCY_FAIL_ON_ERROR=true) that exits 1 when Percy failed to start.

Default behavior is unchanged — without the flag, the wrapped command's status is still the only input to the exit code, so this is not a breaking change for existing pipelines.

Two deliberate design points:

  1. Keyed off the thrown exception, not error-level logs. Scraping logger.query(l => l.level === 'error') looked tempting but would false-positive: percy.js:869 logs Unable to analyze error logs at error level from the error-analysis side channel, and that fires on healthy runs. Verified: a real successful build with --fail-on-error still exits 0.
  2. The wrapped command's status wins. If the child exits 3 and Percy also failed to start, the exit code is 3 — the more specific signal.

Scope

This covers startup failures (Percy never started → zero visual coverage). It deliberately does not cover:

  • Builds that were created but failed server-side — already covered by percy build:wait.
  • Per-snapshot capture failures (e.g. Could not take DOM snapshot). These still produce a finished build with fewer snapshots and exit 0. That is a real remaining gap, called out here so it is not mistaken for covered.

Testing

yarn workspace @percy/cli-exec test — 78/78 pass, including 5 new specs:

  • exits non-zero when percy fails to start
  • exits non-zero when PERCY_FAIL_ON_ERROR is set without the flag
  • exits zero when percy starts successfully
  • forwards the command status ahead of a percy start failure
  • does not affect the exit code when the flag is not set

Also verified end-to-end against the built CLI:

scenarioexit
invalid token, no flag (default, unchanged)0
invalid token, --fail-on-error1
invalid token, PERCY_FAIL_ON_ERROR=true1
child exits 3 + --fail-on-error3
valid token, successful build, --fail-on-error0

Provenance / honest scoping

This started as an investigation into PER-10368 ("Percy errors not failing pipeline"). The ticket's attached screenshot later revealed that customer's actual failure was a @percy/dom bug dropping individual snapshots — fixed separately in #2372 — not a startup failure. So this PR does not resolve PER-10368.

It stands on its own as a fix for #1181, which is a distinct and independently-reported problem. Reviewers should judge it on that basis alone.

Note for reviewers

#1181 was previously answered with "it's by design; we'll evaluate the need for this in the future." This PR keeps that default intact and only adds an opt-in, but the flag name / whether this should eventually become the default is a product call worth confirming.

🤖 Generated with Claude Code

`percy exec` derives its exit code solely from the wrapped command, so a
Percy startup failure (invalid/missing token, browser launch failure) is
logged as "Skipping visual tests" and the pipeline still reports success.
The visual tests silently never run.
Adds an opt-in `--fail-on-error` flag (also `PERCY_FAIL_ON_ERROR=true`)
that exits 1 when Percy failed to start. Default behavior is unchanged.
The check keys off the exception thrown by `percy.yield.start()` rather
than scraping error-level logs, because some error-level entries are
non-fatal diagnostics (e.g. "Unable to analyze error logs" from the
error-analysis side channel) that would otherwise fail healthy runs.
The wrapped command's own non-zero status still takes priority, as it is
the more specific signal.
Refs #1181, PER-10368
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aryanku-dev
aryanku-dev requested a review from a team as a code ownerAugust 7, 2026 04:42
@github-actions

Copy link
Copy Markdown
Contributor

This PR is stale because it has been open for more than 14 days with no activity. Remove stale label or comment or this will be closed in 14 days.

@github-actionsgithub-actionsBot added the 🍞 stale Closed due to inactivity label Aug 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🍞 staleClosed due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@aryanku-dev