Skip to content

Commit ff2eaac

Browse files
ejntaylorclaude
andcommitted
Add PATCHSTACK_SOFT_FAIL so connector errors never break builds
A Lovable publish failed because the project's postbuild script invoked the CLI with an unrecognized subcommand: the CLI printed usage to stderr and exited 1, which failed the entire deploy even though the app build itself succeeded. AI coding tools wire the connector into build scripts, and they sometimes get the invocation wrong — a security monitor should degrade gracefully there, not take the site down with it. PATCHSTACK_SOFT_FAIL (any value except 0/false/no/off) makes the CLI print the error as before but exit 0, covering both bad usage and runtime failures (network, config). Manual runs are unaffected unless the variable is set. The README install prompt now recommends it for build wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent fbe3a6f commit ff2eaac

4 files changed

Lines changed: 60 additions & 7 deletions

File tree

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc
88

99
Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):
1010

11-
> Add `@patchstack/connect` to this project as a dev dependency, then add `"prebuild": "patchstack-connect scan"` to `package.json` `scripts`. The first time the script runs, Patchstack provisions a site automatically from the lockfile and saves the new site UUID to `.patchstackrc.json` — no dashboard signup or manual UUID required. Commit `.patchstackrc.json` so every developer / CI run targets the same site.
11+
> Add `@patchstack/connect` to this project as a dev dependency, then add `"prebuild": "PATCHSTACK_SOFT_FAIL=1 patchstack-connect scan"` to `package.json` `scripts`. Use exactly this command — do not invent other subcommands. The first time the script runs, Patchstack provisions a site automatically from the lockfile and saves the new site UUID to `.patchstackrc.json` — no dashboard signup or manual UUID required. Commit `.patchstackrc.json` so every developer / CI run targets the same site.
1212
1313
## Quick start (zero configuration)
1414

@@ -33,11 +33,13 @@ Then wire it into builds:
3333
// package.json
3434
{
3535
"scripts": {
36-
"prebuild": "patchstack-connect scan"
36+
"prebuild": "PATCHSTACK_SOFT_FAIL=1 patchstack-connect scan"
3737
}
3838
}
3939
```
4040

41+
> **`PATCHSTACK_SOFT_FAIL=1` keeps a connector problem from breaking your build.** Without it, any connector failure — a network blip, a mistyped subcommand — exits non-zero, and on platforms that run scripts as part of publishing (Lovable, Bolt, CI pipelines) that fails the whole deploy. With it, the connector still prints the error but exits 0. Recommended for any hands-off build; leave it off when running commands manually so you actually see failures.
42+
4143
## Quick start (existing site)
4244

4345
If you already created an "Application" site in the Patchstack dashboard, pre-seed the UUID:
@@ -78,6 +80,7 @@ Environment variables:
7880
- `PATCHSTACK_SITE_UUID` — the site UUID from your Patchstack dashboard
7981
- `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://api.patchstack.com/monitor/pulse/manifest`)
8082
- `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`)
83+
- `PATCHSTACK_SOFT_FAIL` — when set (any value except `0`/`false`/`no`/`off`), the CLI prints errors but always exits 0, so a connector problem never fails the build that invokes it
8184

8285
`.patchstackrc.json` example:
8386

src/cli.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { scanLockfile } from './parsers/index.js';
44
import { buildWirePayload } from './normalize.js';
55
import { computeManifestChecksum } from './checksum.js';
66
import { buildClaimUrl, postManifest } from './client.js';
7-
import { persistSiteUuid, resolveConfig, writeConfigFile } from './config.js';
7+
import { persistSiteUuid, resolveConfig, softFailEnabled, writeConfigFile } from './config.js';
88
import {
99
buildInjectionSnippet,
1010
findHtmlFiles,
@@ -40,6 +40,9 @@ Environment:
4040
PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
4141
PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
4242
PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production)
43+
PATCHSTACK_SOFT_FAIL Exit 0 even when a command fails, so a connector
44+
problem never breaks the build that invokes it
45+
(set to 1 in build pipelines / publish hooks)
4346
4447
Precedence: CLI flag > environment variable > .patchstackrc.json.
4548
@@ -280,13 +283,26 @@ async function main(): Promise<number> {
280283
}
281284
}
282285

286+
function finalExitCode(code: number): number {
287+
if (code === 0) {
288+
return 0;
289+
}
290+
if (!softFailEnabled(process.env.PATCHSTACK_SOFT_FAIL)) {
291+
return code;
292+
}
293+
console.error(
294+
'PATCHSTACK_SOFT_FAIL is set — exiting 0 despite the failure above so the build can continue.',
295+
);
296+
return 0;
297+
}
298+
283299
main()
284-
.then((code) => process.exit(code))
300+
.then((code) => process.exit(finalExitCode(code)))
285301
.catch((err: unknown) => {
286302
if (err instanceof PatchstackError) {
287303
console.error(`Error (${err.code}): ${err.message}`);
288-
process.exit(1);
304+
process.exit(finalExitCode(1));
289305
}
290306
console.error('Unexpected error:', err);
291-
process.exit(2);
307+
process.exit(finalExitCode(2));
292308
});

src/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,18 @@ function readEnv(): ConfigFile {
142142
};
143143
}
144144

145+
/**
146+
* PATCHSTACK_SOFT_FAIL makes the CLI exit 0 even when a command fails, so a
147+
* connector problem can never break the build that invokes it. Any non-empty
148+
* value enables it except explicit opt-outs ("0", "false", "no", "off").
149+
*/
150+
export function softFailEnabled(value: string | undefined): boolean {
151+
if (value === undefined || value.length === 0) {
152+
return false;
153+
}
154+
return !['0', 'false', 'no', 'off'].includes(value.toLowerCase());
155+
}
156+
145157
function isUuid(value: string): boolean {
146158
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
147159
}

tests/config.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
22
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
33
import { tmpdir } from 'node:os';
44
import path from 'node:path';
5-
import { persistSiteUuid, resolveConfig, writeConfigFile } from '../src/config.js';
5+
import { persistSiteUuid, resolveConfig, softFailEnabled, writeConfigFile } from '../src/config.js';
66
import { readFile } from 'node:fs/promises';
77
import { DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS } from '../src/client.js';
88
import { PatchstackError } from '../src/types.js';
@@ -125,3 +125,25 @@ describe('resolveConfig', () => {
125125
});
126126
});
127127
});
128+
129+
describe('softFailEnabled', () => {
130+
it('is off when the variable is unset or empty', () => {
131+
expect(softFailEnabled(undefined)).toBe(false);
132+
expect(softFailEnabled('')).toBe(false);
133+
});
134+
135+
it('is on for any truthy-looking value', () => {
136+
expect(softFailEnabled('1')).toBe(true);
137+
expect(softFailEnabled('true')).toBe(true);
138+
expect(softFailEnabled('yes')).toBe(true);
139+
expect(softFailEnabled('anything')).toBe(true);
140+
});
141+
142+
it('respects explicit opt-outs regardless of case', () => {
143+
expect(softFailEnabled('0')).toBe(false);
144+
expect(softFailEnabled('false')).toBe(false);
145+
expect(softFailEnabled('FALSE')).toBe(false);
146+
expect(softFailEnabled('no')).toBe(false);
147+
expect(softFailEnabled('off')).toBe(false);
148+
});
149+
});

0 commit comments

Comments
 (0)