diff --git a/README.md b/README.md index 3d665d6..cf7cad6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.): -> 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. +> 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. ## Quick start (zero configuration) @@ -33,11 +33,13 @@ Then wire it into builds: // package.json { "scripts": { - "prebuild": "patchstack-connect scan" + "prebuild": "PATCHSTACK_SOFT_FAIL=1 patchstack-connect scan" } } ``` +> **`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. + ## Quick start (existing site) If you already created an "Application" site in the Patchstack dashboard, pre-seed the UUID: @@ -78,6 +80,7 @@ Environment variables: - `PATCHSTACK_SITE_UUID` — the site UUID from your Patchstack dashboard - `PATCHSTACK_ENDPOINT` — override the API endpoint (default `https://api.patchstack.com/monitor/pulse/manifest`) - `PATCHSTACK_TIMEOUT_MS` — request timeout in milliseconds (default `30000`) +- `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 `.patchstackrc.json` example: diff --git a/src/cli.ts b/src/cli.ts index 5833bbf..e31f839 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,7 +4,7 @@ import { scanLockfile } from './parsers/index.js'; import { buildWirePayload } from './normalize.js'; import { computeManifestChecksum } from './checksum.js'; import { buildClaimUrl, postManifest } from './client.js'; -import { persistSiteUuid, resolveConfig, writeConfigFile } from './config.js'; +import { persistSiteUuid, resolveConfig, softFailEnabled, writeConfigFile } from './config.js'; import { buildInjectionSnippet, findHtmlFiles, @@ -40,6 +40,9 @@ Environment: PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest) PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000) PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production) + PATCHSTACK_SOFT_FAIL Exit 0 even when a command fails, so a connector + problem never breaks the build that invokes it + (set to 1 in build pipelines / publish hooks) Precedence: CLI flag > environment variable > .patchstackrc.json. @@ -280,13 +283,26 @@ async function main(): Promise { } } +function finalExitCode(code: number): number { + if (code === 0) { + return 0; + } + if (!softFailEnabled(process.env.PATCHSTACK_SOFT_FAIL)) { + return code; + } + console.error( + 'PATCHSTACK_SOFT_FAIL is set — exiting 0 despite the failure above so the build can continue.', + ); + return 0; +} + main() - .then((code) => process.exit(code)) + .then((code) => process.exit(finalExitCode(code))) .catch((err: unknown) => { if (err instanceof PatchstackError) { console.error(`Error (${err.code}): ${err.message}`); - process.exit(1); + process.exit(finalExitCode(1)); } console.error('Unexpected error:', err); - process.exit(2); + process.exit(finalExitCode(2)); }); diff --git a/src/config.ts b/src/config.ts index 8172d80..75b82ee 100644 --- a/src/config.ts +++ b/src/config.ts @@ -142,6 +142,18 @@ function readEnv(): ConfigFile { }; } +/** + * PATCHSTACK_SOFT_FAIL makes the CLI exit 0 even when a command fails, so a + * connector problem can never break the build that invokes it. Any non-empty + * value enables it except explicit opt-outs ("0", "false", "no", "off"). + */ +export function softFailEnabled(value: string | undefined): boolean { + if (value === undefined || value.length === 0) { + return false; + } + return !['0', 'false', 'no', 'off'].includes(value.toLowerCase()); +} + function isUuid(value: string): boolean { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); } diff --git a/tests/config.test.ts b/tests/config.test.ts index f9b9844..b96c87a 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { persistSiteUuid, resolveConfig, writeConfigFile } from '../src/config.js'; +import { persistSiteUuid, resolveConfig, softFailEnabled, writeConfigFile } from '../src/config.js'; import { readFile } from 'node:fs/promises'; import { DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS } from '../src/client.js'; import { PatchstackError } from '../src/types.js'; @@ -125,3 +125,25 @@ describe('resolveConfig', () => { }); }); }); + +describe('softFailEnabled', () => { + it('is off when the variable is unset or empty', () => { + expect(softFailEnabled(undefined)).toBe(false); + expect(softFailEnabled('')).toBe(false); + }); + + it('is on for any truthy-looking value', () => { + expect(softFailEnabled('1')).toBe(true); + expect(softFailEnabled('true')).toBe(true); + expect(softFailEnabled('yes')).toBe(true); + expect(softFailEnabled('anything')).toBe(true); + }); + + it('respects explicit opt-outs regardless of case', () => { + expect(softFailEnabled('0')).toBe(false); + expect(softFailEnabled('false')).toBe(false); + expect(softFailEnabled('FALSE')).toBe(false); + expect(softFailEnabled('no')).toBe(false); + expect(softFailEnabled('off')).toBe(false); + }); +});