Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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:
Expand Down Expand Up @@ -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:

Expand Down
24 changes: 20 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -280,13 +283,26 @@ async function main(): Promise<number> {
}
}

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));
});
12 changes: 12 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
24 changes: 23 additions & 1 deletion tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});
Loading