atmn 2.0.41 → 2.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +405 -392
- package/dist/index.js +20 -21
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/actions/pull/webhooks/appendWebhook.ts +6 -4
- package/src/actions/pull/webhooks/applyWebhooksPull.ts +28 -105
- package/src/actions/pull/webhooks/deleteWebhook.ts +59 -0
- package/src/actions/pull/webhooks/locateWebhook.ts +9 -1
- package/src/actions/pull/webhooks/readWebhookEnvs.ts +7 -12
- package/src/actions/pull/webhooks/types.ts +3 -2
- package/src/actions/pull/webhooks/updateWebhook.ts +37 -42
- package/src/actions/pull/webhooks/webhookEnvSkipWarning.ts +29 -0
- package/src/actions/pull/webhooks/webhookPullEnvs.ts +5 -5
- package/src/actions/pull.ts +1 -1
- package/src/actions/push.ts +30 -12
- package/src/actions/webhooks/applyWebhooks.ts +41 -10
- package/src/actions/webhooks/previewWebhookEnvs.ts +213 -0
- package/src/actions/webhooks/resolveWebhooksForEnv.ts +5 -16
- package/src/actions/webhooks/types/webhookEnv.ts +1 -3
- package/src/actions/webhooks/webhookEnvFailures.ts +27 -0
- package/src/actions/webhooks/webhookSecretName.ts +10 -9
- package/src/actions/webhooks/writeWebhookSecrets.ts +5 -2
- package/src/auth/runOAuthFlow.ts +1 -1
- package/src/cli.ts +23 -25
- package/src/generated/apiRoutes.ts +43 -12
- package/src/generated/client.ts +10 -10
- package/src/generated/emit.ts +4 -4
- package/src/generated/emitRuntime.ts +3 -3
- package/src/generated/lintRules.ts +16 -20
- package/src/generated/lintRuntime.ts +10 -0
- package/src/generated/skills.ts +10 -10
- package/src/generated/webhooks.ts +7 -9
- package/src/generated/wire.ts +0 -1
- package/src/render/renderPreview.ts +25 -9
- package/src/render/renderWebhooks.ts +9 -3
- package/src/actions/pull/webhooks/removeWebhookEnv.ts +0 -141
- package/src/actions/webhooks/isDashboardWebhook.ts +0 -9
- package/src/actions/webhooks/newWebhookId.ts +0 -4
- package/src/actions/webhooks/previewWebhooks.ts +0 -40
- package/src/actions/webhooks/resolveWebhookEnv.ts +0 -34
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AutumnApiError } from "../../../generated/client";
|
|
2
|
+
import { ForeignOrgKeyError, type WebhookPullEnv } from "./webhookPullEnvs";
|
|
3
|
+
|
|
4
|
+
/** Why a failure skips its env; null when it should fail the command. */
|
|
5
|
+
const skipReason = (error: unknown): string | null => {
|
|
6
|
+
if (error instanceof ForeignOrgKeyError) return "belongs to another org";
|
|
7
|
+
if (!(error instanceof AutumnApiError)) return null;
|
|
8
|
+
if (error.status === 401 || error.status === 403) return "was rejected";
|
|
9
|
+
if (error.status !== 404) return null;
|
|
10
|
+
const { message } = (error.body ?? {}) as { message?: unknown };
|
|
11
|
+
return `failed: ${typeof message === "string" ? message : error.status}`;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A rejected key, one for another org, or a 404 (a deleted sandbox) skips its
|
|
16
|
+
* env with this warning; anything else is a real failure.
|
|
17
|
+
*/
|
|
18
|
+
export const webhookEnvSkipWarning = ({
|
|
19
|
+
env,
|
|
20
|
+
error,
|
|
21
|
+
}: {
|
|
22
|
+
env: Pick<WebhookPullEnv, "label" | "keyName">;
|
|
23
|
+
error: unknown;
|
|
24
|
+
}): string | undefined => {
|
|
25
|
+
const reason = skipReason(error);
|
|
26
|
+
return reason === null
|
|
27
|
+
? undefined
|
|
28
|
+
: `⚠ webhooks: skipped ${env.label} (${env.keyName} ${reason})`;
|
|
29
|
+
};
|
|
@@ -4,11 +4,13 @@ import { sandboxSlug } from "../../../generated/sandboxName";
|
|
|
4
4
|
import type { OrgInfo } from "../../env/types/orgInfo";
|
|
5
5
|
import type { RemoteWebhook } from "./types";
|
|
6
6
|
|
|
7
|
-
/** One env a pull reads webhooks from, through that env's own key. */
|
|
7
|
+
/** One env a pull reads webhooks from, or a push syncs them to, through that env's own key. */
|
|
8
8
|
export type WebhookPullEnv = {
|
|
9
9
|
keyName: SecretKeyName;
|
|
10
10
|
/** How a skipped-env warning names it, before its key is known to work. */
|
|
11
11
|
label: string;
|
|
12
|
+
/** This env's own key: push syncs each env through it. */
|
|
13
|
+
secretKey: string;
|
|
12
14
|
/** The `url` map key; a named sandbox's slug costs one lookup. */
|
|
13
15
|
envKey: () => Promise<string>;
|
|
14
16
|
listWebhooks: () => Promise<{ list: RemoteWebhook[] }>;
|
|
@@ -21,10 +23,7 @@ export class ForeignOrgKeyError extends Error {}
|
|
|
21
23
|
const mainOrgIdOf = (info: OrgInfo): string =>
|
|
22
24
|
info.is_sandbox && info.created_by ? info.created_by : info.id;
|
|
23
25
|
|
|
24
|
-
/**
|
|
25
|
-
* Every env the loaded env files hold a key for. Live comes last, so when envs
|
|
26
|
-
* disagree on shared fields like `events`, prod's value is the one written.
|
|
27
|
-
*/
|
|
26
|
+
/** Every env the loaded env files hold a key for, live last. */
|
|
28
27
|
export const webhookPullEnvs = ({
|
|
29
28
|
env = process.env,
|
|
30
29
|
targetKeyName,
|
|
@@ -76,6 +75,7 @@ export const webhookPullEnvs = ({
|
|
|
76
75
|
{
|
|
77
76
|
keyName,
|
|
78
77
|
label,
|
|
78
|
+
secretKey,
|
|
79
79
|
envKey,
|
|
80
80
|
listWebhooks: () => listWebhooks({ secretKey }),
|
|
81
81
|
},
|
package/src/actions/pull.ts
CHANGED
|
@@ -375,7 +375,7 @@ export const runPull = async ({
|
|
|
375
375
|
|
|
376
376
|
const webhookWarnings = [...webhooks.skipped];
|
|
377
377
|
let stated = lists.webhooks as StatedWebhook[] | undefined;
|
|
378
|
-
// Only an env whose list succeeded may remove its
|
|
378
|
+
// Only an env whose list succeeded may remove its entries from the config.
|
|
379
379
|
for (const { envKey, list } of webhooks.read) {
|
|
380
380
|
const applied = applyWebhooksPull({
|
|
381
381
|
pull: { configPath, files },
|
package/src/actions/push.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type SettingsPreview,
|
|
13
13
|
settingsHaveWork,
|
|
14
14
|
} from "../render/renderPreview";
|
|
15
|
+
import type { WebhookPullEnv } from "./pull/webhooks/webhookPullEnvs";
|
|
15
16
|
import {
|
|
16
17
|
backfillInternalIds,
|
|
17
18
|
identityRowsFromApplied,
|
|
@@ -23,8 +24,11 @@ import {
|
|
|
23
24
|
import { PushLanesError, settleLanes } from "./push/pushLanesError";
|
|
24
25
|
import { withSettingsScopeHint } from "./sandbox/withSandboxScopeHint";
|
|
25
26
|
import { applyWebhooks } from "./webhooks/applyWebhooks";
|
|
26
|
-
import {
|
|
27
|
-
|
|
27
|
+
import {
|
|
28
|
+
previewWebhookEnvs,
|
|
29
|
+
type WebhookClientFor,
|
|
30
|
+
type WebhookLanes,
|
|
31
|
+
} from "./webhooks/previewWebhookEnvs";
|
|
28
32
|
|
|
29
33
|
export type PushResult = {
|
|
30
34
|
configPath: string;
|
|
@@ -61,8 +65,12 @@ export type PushOptions = {
|
|
|
61
65
|
/** Where to write progress. Injected so tests can capture it. */
|
|
62
66
|
write?: (text: string) => void;
|
|
63
67
|
migrationLinkBase?: string;
|
|
64
|
-
/**
|
|
65
|
-
|
|
68
|
+
/** `-p`: the catalog and webhooks go to live, and no sandbox is touched. */
|
|
69
|
+
prod?: boolean;
|
|
70
|
+
/** Every env with a key; listed only when the config states webhooks. */
|
|
71
|
+
webhookEnvs?: () => WebhookPullEnv[];
|
|
72
|
+
/** A client for one env's own key. */
|
|
73
|
+
webhookClientFor?: WebhookClientFor;
|
|
66
74
|
};
|
|
67
75
|
|
|
68
76
|
type RewardBody = { id?: string; internal_id?: string };
|
|
@@ -340,7 +348,9 @@ export const runPush = async ({
|
|
|
340
348
|
dryRun = false,
|
|
341
349
|
write = (text) => process.stdout.write(text),
|
|
342
350
|
migrationLinkBase,
|
|
343
|
-
|
|
351
|
+
prod = false,
|
|
352
|
+
webhookEnvs,
|
|
353
|
+
webhookClientFor,
|
|
344
354
|
}: PushOptions): Promise<PushResult> => {
|
|
345
355
|
const project = resolveProject({ cwd, configFlag });
|
|
346
356
|
const dirs = configSearchDirs({ cwd, configPath: configFlag });
|
|
@@ -362,14 +372,15 @@ export const runPush = async ({
|
|
|
362
372
|
const previews = await settleLanes<{
|
|
363
373
|
settings: SettingsPreview | undefined;
|
|
364
374
|
catalog: CatalogPreview;
|
|
365
|
-
webhooks:
|
|
375
|
+
webhooks: WebhookLanes | undefined;
|
|
366
376
|
}>({
|
|
367
377
|
settings: previewSettings({ client, body: settingsBody }),
|
|
368
378
|
catalog: client.previewUpdate(wire) as Promise<CatalogPreview>,
|
|
369
|
-
webhooks:
|
|
370
|
-
client,
|
|
379
|
+
webhooks: previewWebhookEnvs({
|
|
371
380
|
rows: lists.webhooks,
|
|
372
|
-
|
|
381
|
+
envs: webhookEnvs,
|
|
382
|
+
prod,
|
|
383
|
+
clientFor: webhookClientFor,
|
|
373
384
|
}),
|
|
374
385
|
});
|
|
375
386
|
const { settings, webhooks } = previews.values;
|
|
@@ -389,8 +400,16 @@ export const runPush = async ({
|
|
|
389
400
|
const preview: CatalogPreview = {
|
|
390
401
|
...catalogPreview,
|
|
391
402
|
settings,
|
|
392
|
-
...(webhooks === undefined
|
|
403
|
+
...(webhooks === undefined
|
|
404
|
+
? {}
|
|
405
|
+
: {
|
|
406
|
+
webhooks: webhooks.lanes.map((lane) => lane.preview),
|
|
407
|
+
productionWebhooks: webhooks.productionDiffers,
|
|
408
|
+
productionWebhooksUnchecked: webhooks.productionUnchecked,
|
|
409
|
+
}),
|
|
393
410
|
};
|
|
411
|
+
if (webhooks !== undefined && webhooks.skipped.length > 0)
|
|
412
|
+
write(`${webhooks.skipped.join("\n")}\n\n`);
|
|
394
413
|
write(`${renderPreview({ preview, migrationLinkBase })}\n`);
|
|
395
414
|
if (catalogDeferral !== undefined)
|
|
396
415
|
write(`\n${renderCatalogDeferral({ error: catalogDeferral.error })}\n`);
|
|
@@ -434,8 +453,7 @@ export const runPush = async ({
|
|
|
434
453
|
? settingsStep().then(catalogStep)
|
|
435
454
|
: catalogStep(settled),
|
|
436
455
|
webhooks: applyWebhooks({
|
|
437
|
-
|
|
438
|
-
lane: webhooks,
|
|
456
|
+
lanes: webhooks?.lanes ?? [],
|
|
439
457
|
envDirs: project.envDirs,
|
|
440
458
|
cwd,
|
|
441
459
|
write,
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import type { AutumnClient } from "../../generated/client";
|
|
2
1
|
import { webhooksHaveWork } from "../../render/renderWebhooks";
|
|
3
|
-
import type { WebhooksLane } from "./
|
|
2
|
+
import type { WebhooksLane } from "./previewWebhookEnvs";
|
|
3
|
+
import {
|
|
4
|
+
messageOf,
|
|
5
|
+
throwWebhookEnvFailures,
|
|
6
|
+
type WebhookEnvFailure,
|
|
7
|
+
} from "./webhookEnvFailures";
|
|
4
8
|
import { writeWebhookSecrets } from "./writeWebhookSecrets";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
11
|
* Sync, then save every secret it returned — before reporting any per-item
|
|
8
12
|
* failure, since a created webhook's secret is shown once and only here.
|
|
9
13
|
*/
|
|
10
|
-
|
|
11
|
-
client,
|
|
14
|
+
const applyLane = async ({
|
|
12
15
|
lane,
|
|
13
16
|
envDirs,
|
|
14
17
|
cwd,
|
|
15
18
|
write,
|
|
16
19
|
}: {
|
|
17
|
-
|
|
18
|
-
lane: WebhooksLane | undefined;
|
|
20
|
+
lane: WebhooksLane;
|
|
19
21
|
envDirs: string[];
|
|
20
22
|
cwd: string;
|
|
21
23
|
write: (text: string) => void;
|
|
22
24
|
}): Promise<void> => {
|
|
23
|
-
|
|
24
|
-
return;
|
|
25
|
-
const result = await client.syncWebhooks(lane.body);
|
|
25
|
+
const result = await lane.client.syncWebhooks(lane.body);
|
|
26
26
|
const saved = await writeWebhookSecrets({
|
|
27
27
|
secrets: result.secrets,
|
|
28
28
|
env: lane.env,
|
|
@@ -43,10 +43,41 @@ export const applyWebhooks = async ({
|
|
|
43
43
|
`Adopted ${change.id} (existing dashboard webhook); its signing secret is unchanged, nothing written`,
|
|
44
44
|
);
|
|
45
45
|
write(
|
|
46
|
-
`\nApplied webhooks.\n${[...adopted, ...saved].map((line) => `${line}\n`).join("")}`,
|
|
46
|
+
`\nApplied webhooks to ${lane.env.key}.\n${[...adopted, ...saved].map((line) => `${line}\n`).join("")}`,
|
|
47
47
|
);
|
|
48
48
|
if (result.errors.length > 0)
|
|
49
49
|
throw new Error(
|
|
50
50
|
result.errors.map(({ id, message }) => `${id}: ${message}`).join("\n"),
|
|
51
51
|
);
|
|
52
52
|
};
|
|
53
|
+
|
|
54
|
+
/** Every env with work syncs in parallel; one env failing never stops another saving its secrets. */
|
|
55
|
+
export const applyWebhooks = async ({
|
|
56
|
+
lanes,
|
|
57
|
+
envDirs,
|
|
58
|
+
cwd,
|
|
59
|
+
write,
|
|
60
|
+
}: {
|
|
61
|
+
lanes: WebhooksLane[];
|
|
62
|
+
envDirs: string[];
|
|
63
|
+
cwd: string;
|
|
64
|
+
write: (text: string) => void;
|
|
65
|
+
}): Promise<void> => {
|
|
66
|
+
const withWork = lanes.filter((lane) =>
|
|
67
|
+
webhooksHaveWork({ webhooks: lane.preview }),
|
|
68
|
+
);
|
|
69
|
+
const results = await Promise.allSettled(
|
|
70
|
+
withWork.map((lane) => applyLane({ lane, envDirs, cwd, write })),
|
|
71
|
+
);
|
|
72
|
+
const failures: WebhookEnvFailure[] = results.flatMap((result, index) =>
|
|
73
|
+
result.status === "rejected"
|
|
74
|
+
? [
|
|
75
|
+
{
|
|
76
|
+
env: withWork[index]?.env.key ?? "",
|
|
77
|
+
message: messageOf(result.reason),
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
: [],
|
|
81
|
+
);
|
|
82
|
+
throwWebhookEnvFailures({ failures, envCount: withWork.length });
|
|
83
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { AutumnClient, SyncWebhooksParams } from "../../generated/client";
|
|
2
|
+
import type { WebhooksPreview } from "../../render/renderWebhooks";
|
|
3
|
+
import { webhookEnvSkipWarning } from "../pull/webhooks/webhookEnvSkipWarning";
|
|
4
|
+
import type { WebhookPullEnv } from "../pull/webhooks/webhookPullEnvs";
|
|
5
|
+
import { resolveWebhooksForEnv } from "./resolveWebhooksForEnv";
|
|
6
|
+
import type { WebhookEnv } from "./types/webhookEnv";
|
|
7
|
+
import {
|
|
8
|
+
messageOf,
|
|
9
|
+
throwWebhookEnvFailures,
|
|
10
|
+
type WebhookEnvFailure,
|
|
11
|
+
} from "./webhookEnvFailures";
|
|
12
|
+
import { webhookSecretName } from "./webhookSecretName";
|
|
13
|
+
|
|
14
|
+
export type WebhookClient = Pick<
|
|
15
|
+
AutumnClient,
|
|
16
|
+
"previewSyncWebhooks" | "syncWebhooks"
|
|
17
|
+
>;
|
|
18
|
+
|
|
19
|
+
export type WebhookClientFor = (args: { secretKey: string }) => WebhookClient;
|
|
20
|
+
|
|
21
|
+
/** One env's share of a push: its preview, and exactly what the sync will send. */
|
|
22
|
+
export type WebhooksLane = {
|
|
23
|
+
preview: WebhooksPreview;
|
|
24
|
+
body: SyncWebhooksParams;
|
|
25
|
+
env: WebhookEnv;
|
|
26
|
+
client: WebhookClient;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type WebhookLanes = {
|
|
30
|
+
lanes: WebhooksLane[];
|
|
31
|
+
/** Live webhooks a `-p` push would change; a plain push only reads them. */
|
|
32
|
+
productionDiffers: string[];
|
|
33
|
+
/** The read-only live check failed, so the preview can't claim production matches. */
|
|
34
|
+
productionUnchecked: boolean;
|
|
35
|
+
/** One warning per env whose key was rejected or belongs to another org. */
|
|
36
|
+
skipped: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type EnvPreview =
|
|
40
|
+
| { lane: WebhooksLane; errors: { id: string; message: string }[] }
|
|
41
|
+
| { skipped: string }
|
|
42
|
+
| undefined;
|
|
43
|
+
|
|
44
|
+
const isLive = (env: WebhookPullEnv): boolean =>
|
|
45
|
+
env.keyName === "AUTUMN_PROD_SECRET_KEY";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Two slugs like `qa-team` and `qa_team` name one variable. A create in one
|
|
49
|
+
* refuses when another synced env states the same id: its saved secret would be overwritten.
|
|
50
|
+
*/
|
|
51
|
+
const assertDistinctSecretNames = ({
|
|
52
|
+
lanes,
|
|
53
|
+
}: {
|
|
54
|
+
lanes: WebhooksLane[];
|
|
55
|
+
}): void => {
|
|
56
|
+
const secretName = ({ lane, id }: { lane: WebhooksLane; id: string }) =>
|
|
57
|
+
webhookSecretName({
|
|
58
|
+
id,
|
|
59
|
+
...(lane.env.live ? {} : { envKey: lane.env.key }),
|
|
60
|
+
});
|
|
61
|
+
const clashes = new Map<string, string>();
|
|
62
|
+
for (const lane of lanes) {
|
|
63
|
+
for (const change of lane.preview.changes) {
|
|
64
|
+
if (change.action !== "create") continue;
|
|
65
|
+
const name = secretName({ lane, id: change.id });
|
|
66
|
+
for (const other of lanes) {
|
|
67
|
+
if (other === lane) continue;
|
|
68
|
+
const shares = other.body.webhooks.some(
|
|
69
|
+
(webhook) => secretName({ lane: other, id: webhook.id }) === name,
|
|
70
|
+
);
|
|
71
|
+
if (!shares) continue;
|
|
72
|
+
const [first, second] = [other.env.key, lane.env.key].sort();
|
|
73
|
+
clashes.set(
|
|
74
|
+
`${first}|${second}|${name}`,
|
|
75
|
+
`${first} and ${second} would both save ${change.id}'s signing secret as ${name}. Rename one of those sandboxes so their names differ by more than punctuation.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (clashes.size > 0) throw new Error([...clashes.values()].join("\n"));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Undefined when the config registers nothing in this env: there is nothing to preview. */
|
|
84
|
+
const previewEnv = async ({
|
|
85
|
+
env,
|
|
86
|
+
rows,
|
|
87
|
+
clientFor,
|
|
88
|
+
}: {
|
|
89
|
+
env: WebhookPullEnv;
|
|
90
|
+
rows: Record<string, unknown>[];
|
|
91
|
+
clientFor: WebhookClientFor;
|
|
92
|
+
}): Promise<EnvPreview> => {
|
|
93
|
+
try {
|
|
94
|
+
const key = await env.envKey();
|
|
95
|
+
const webhooks = resolveWebhooksForEnv({ rows, envKey: key });
|
|
96
|
+
if (webhooks.length === 0) return undefined;
|
|
97
|
+
const client = clientFor({ secretKey: env.secretKey });
|
|
98
|
+
const body = { webhooks };
|
|
99
|
+
const { changes, errors } = await client.previewSyncWebhooks(body);
|
|
100
|
+
return {
|
|
101
|
+
lane: {
|
|
102
|
+
preview: { env: key, changes },
|
|
103
|
+
body,
|
|
104
|
+
env: { key, live: isLive(env) },
|
|
105
|
+
client,
|
|
106
|
+
},
|
|
107
|
+
errors,
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const skipped = webhookEnvSkipWarning({ env, error });
|
|
111
|
+
if (skipped === undefined) throw error;
|
|
112
|
+
return { skipped };
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A plain push syncs every sandbox env with a key, and previews live read-only
|
|
118
|
+
* so a production diff is named rather than hidden; `-p` syncs live alone.
|
|
119
|
+
* Absent when the config states no `webhooks`.
|
|
120
|
+
*/
|
|
121
|
+
export const previewWebhookEnvs = async ({
|
|
122
|
+
rows,
|
|
123
|
+
envs,
|
|
124
|
+
prod,
|
|
125
|
+
clientFor,
|
|
126
|
+
}: {
|
|
127
|
+
rows: Record<string, unknown>[] | undefined;
|
|
128
|
+
envs: (() => WebhookPullEnv[]) | undefined;
|
|
129
|
+
prod: boolean;
|
|
130
|
+
clientFor: WebhookClientFor | undefined;
|
|
131
|
+
}): Promise<WebhookLanes | undefined> => {
|
|
132
|
+
if (rows === undefined) return undefined;
|
|
133
|
+
if (envs === undefined || clientFor === undefined)
|
|
134
|
+
throw new Error("webhooks need the env keys to pick each url.");
|
|
135
|
+
const all = envs();
|
|
136
|
+
const synced = all.filter((env) => isLive(env) === prod);
|
|
137
|
+
const probed = prod ? [] : all.filter(isLive);
|
|
138
|
+
const [syncedResults, probedResults] = await Promise.all([
|
|
139
|
+
Promise.allSettled(
|
|
140
|
+
synced.map((env) => previewEnv({ env, rows, clientFor })),
|
|
141
|
+
),
|
|
142
|
+
Promise.allSettled(
|
|
143
|
+
probed.map((env) => previewEnv({ env, rows, clientFor })),
|
|
144
|
+
),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const lanes: WebhooksLane[] = [];
|
|
148
|
+
const skipped: string[] = [];
|
|
149
|
+
// Live is only read here, so its failure is a warning, never a blocked sandbox push.
|
|
150
|
+
probedResults.forEach((result, index) => {
|
|
151
|
+
if (result.status === "rejected")
|
|
152
|
+
skipped.push(
|
|
153
|
+
`⚠ webhooks: couldn't check ${probed[index]?.label ?? "live"} for changes (${messageOf(result.reason)})`,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
// A skipped live key (rejected, deleted, another org's) leaves production unchecked too.
|
|
157
|
+
const productionUnchecked = probedResults.some(
|
|
158
|
+
(result) =>
|
|
159
|
+
result.status === "rejected" ||
|
|
160
|
+
(result.value !== undefined && "skipped" in result.value),
|
|
161
|
+
);
|
|
162
|
+
// Refused items (several dashboard webhooks on one URL, say) fail the lane
|
|
163
|
+
// here, beside every other lane's errors, rather than at apply.
|
|
164
|
+
const failures: WebhookEnvFailure[] = [];
|
|
165
|
+
syncedResults.forEach((result, index) => {
|
|
166
|
+
const label = synced[index]?.label ?? "";
|
|
167
|
+
if (result.status === "rejected") {
|
|
168
|
+
failures.push({ env: label, message: messageOf(result.reason) });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const preview = result.value;
|
|
172
|
+
if (preview === undefined) return;
|
|
173
|
+
if ("skipped" in preview) {
|
|
174
|
+
skipped.push(preview.skipped);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
lanes.push(preview.lane);
|
|
178
|
+
if (preview.errors.length > 0)
|
|
179
|
+
failures.push({
|
|
180
|
+
env: preview.lane.env.key,
|
|
181
|
+
message: preview.errors
|
|
182
|
+
.map(({ id, message }) => `${id}: ${message}`)
|
|
183
|
+
.join("\n"),
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
throwWebhookEnvFailures({
|
|
187
|
+
failures,
|
|
188
|
+
envCount: synced.length,
|
|
189
|
+
warnings: skipped,
|
|
190
|
+
});
|
|
191
|
+
assertDistinctSecretNames({ lanes });
|
|
192
|
+
|
|
193
|
+
const productionDiffers: string[] = [];
|
|
194
|
+
for (const result of probedResults) {
|
|
195
|
+
if (result.status === "rejected") continue;
|
|
196
|
+
const preview = result.value;
|
|
197
|
+
if (preview === undefined) continue;
|
|
198
|
+
if ("skipped" in preview) {
|
|
199
|
+
skipped.push(preview.skipped);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const changed = preview.lane.preview.changes.filter(
|
|
203
|
+
(change) => change.action !== "unmanaged",
|
|
204
|
+
);
|
|
205
|
+
productionDiffers.push(
|
|
206
|
+
...new Set([
|
|
207
|
+
...changed.map((change) => change.id),
|
|
208
|
+
...preview.errors.map((error) => error.id),
|
|
209
|
+
]),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return { lanes, productionDiffers, productionUnchecked, skipped };
|
|
213
|
+
};
|
|
@@ -3,7 +3,7 @@ import { SYNCED_LISTS } from "../../generated/emit";
|
|
|
3
3
|
|
|
4
4
|
type WebhookParams = SyncWebhooksParams["webhooks"][number];
|
|
5
5
|
|
|
6
|
-
/** The config's
|
|
6
|
+
/** The config's entries for one environment, as the sync body takes them. */
|
|
7
7
|
export const resolveWebhooksForEnv = ({
|
|
8
8
|
rows,
|
|
9
9
|
envKey,
|
|
@@ -11,19 +11,8 @@ export const resolveWebhooksForEnv = ({
|
|
|
11
11
|
rows: Record<string, unknown>[];
|
|
12
12
|
envKey: string;
|
|
13
13
|
}): WebhookParams[] => {
|
|
14
|
-
const {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
(field) => (row[field] as Record<string, unknown> | undefined)?.[envKey],
|
|
19
|
-
);
|
|
20
|
-
if (values.some((value) => typeof value !== "string")) continue;
|
|
21
|
-
webhooks.push({
|
|
22
|
-
...row,
|
|
23
|
-
...Object.fromEntries(
|
|
24
|
-
envKeyed.map((field, index) => [field, values[index]]),
|
|
25
|
-
),
|
|
26
|
-
} as WebhookParams);
|
|
27
|
-
}
|
|
28
|
-
return webhooks;
|
|
14
|
+
const { envField } = SYNCED_LISTS.webhooks;
|
|
15
|
+
return rows
|
|
16
|
+
.filter((row) => row[envField] === envKey)
|
|
17
|
+
.map(({ [envField]: _env, ...row }) => row as WebhookParams);
|
|
29
18
|
};
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
export type WebhookEnv = {
|
|
3
3
|
/** The `url` map key: `live`, `sandbox`, or the targeted sandbox's slug. */
|
|
4
4
|
key: string;
|
|
5
|
-
/** Prod secrets go to `.env.prod` without an
|
|
5
|
+
/** Prod secrets go to `.env.prod` without an env suffix. */
|
|
6
6
|
live: boolean;
|
|
7
|
-
/** The target org's id, looked up only when a secret needs naming. */
|
|
8
|
-
orgId: () => Promise<string>;
|
|
9
7
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** One env's failed webhook preview or sync. */
|
|
2
|
+
export type WebhookEnvFailure = { env: string; message: string };
|
|
3
|
+
|
|
4
|
+
/** Names each env only when several were in play, so a lone env reads as it always has. */
|
|
5
|
+
export const throwWebhookEnvFailures = ({
|
|
6
|
+
failures,
|
|
7
|
+
envCount,
|
|
8
|
+
warnings = [],
|
|
9
|
+
}: {
|
|
10
|
+
failures: WebhookEnvFailure[];
|
|
11
|
+
envCount: number;
|
|
12
|
+
/** Skipped-env warnings the failure would otherwise swallow. */
|
|
13
|
+
warnings?: string[];
|
|
14
|
+
}): void => {
|
|
15
|
+
if (failures.length === 0) return;
|
|
16
|
+
throw new Error(
|
|
17
|
+
[
|
|
18
|
+
...failures.map(({ env, message }) =>
|
|
19
|
+
envCount > 1 ? message.replace(/^/gm, `${env}: `) : message,
|
|
20
|
+
),
|
|
21
|
+
...warnings,
|
|
22
|
+
].join("\n"),
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const messageOf = (error: unknown): string =>
|
|
27
|
+
error instanceof Error ? error.message : String(error);
|
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
const envNameSegment = (value: string): string =>
|
|
3
3
|
value.toUpperCase().replace(/-/g, "_");
|
|
4
4
|
|
|
5
|
+
/** A sandbox slug as an env-var segment: every non-alphanumeric run becomes `_`. */
|
|
6
|
+
const envKeySegment = (envKey: string): string =>
|
|
7
|
+
envKey.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
|
-
* Prod: `AUTUMN_WEBHOOK_<ID>_SECRET`. A sandbox adds
|
|
7
|
-
*
|
|
10
|
+
* Prod: `AUTUMN_WEBHOOK_<ID>_SECRET`. A sandbox adds its `url` key (`SANDBOX`,
|
|
11
|
+
* or its slug), so every sandbox's secret can sit in one `.env`.
|
|
8
12
|
*/
|
|
9
13
|
export const webhookSecretName = ({
|
|
10
14
|
id,
|
|
11
|
-
|
|
15
|
+
envKey,
|
|
12
16
|
}: {
|
|
13
17
|
id: string;
|
|
14
18
|
/** Absent for prod. */
|
|
15
|
-
|
|
19
|
+
envKey?: string;
|
|
16
20
|
}): string => {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
? ""
|
|
20
|
-
: `_${envNameSegment(orgId.replace(/^org_/, "").slice(0, 4))}`;
|
|
21
|
-
return `AUTUMN_WEBHOOK_${envNameSegment(id)}${org4}_SECRET`;
|
|
21
|
+
const suffix = envKey === undefined ? "" : `_${envKeySegment(envKey)}`;
|
|
22
|
+
return `AUTUMN_WEBHOOK_${envNameSegment(id)}${suffix}_SECRET`;
|
|
22
23
|
};
|
|
@@ -35,9 +35,12 @@ export const writeWebhookSecrets = async ({
|
|
|
35
35
|
cwd: string;
|
|
36
36
|
}): Promise<string[]> => {
|
|
37
37
|
if (secrets.length === 0) return [];
|
|
38
|
-
const
|
|
38
|
+
const envKey = env.live ? undefined : env.key;
|
|
39
39
|
const values = Object.fromEntries(
|
|
40
|
-
secrets.map(({ id, secret }) => [
|
|
40
|
+
secrets.map(({ id, secret }) => [
|
|
41
|
+
webhookSecretName({ id, envKey }),
|
|
42
|
+
secret,
|
|
43
|
+
]),
|
|
41
44
|
);
|
|
42
45
|
let path: string;
|
|
43
46
|
if (env.live) {
|
package/src/auth/runOAuthFlow.ts
CHANGED
|
@@ -225,6 +225,6 @@ export const runOAuthFlow = async ({
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
throw new Error(
|
|
228
|
-
`All OAuth callback ports (${OAUTH_PORTS
|
|
228
|
+
`All OAuth callback ports (${OAUTH_PORTS[0]}-${OAUTH_PORTS[OAUTH_PORTS.length - 1]}) are in use.`,
|
|
229
229
|
);
|
|
230
230
|
};
|
package/src/cli.ts
CHANGED
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
staleSkillsHint,
|
|
33
33
|
updateSkills,
|
|
34
34
|
} from "./actions/skills/skills";
|
|
35
|
-
import { resolveWebhookEnv } from "./actions/webhooks/resolveWebhookEnv";
|
|
36
35
|
import { configPackageName } from "./config/configPackageName";
|
|
37
36
|
import { assertSandboxTarget } from "./env/assertSandboxTarget";
|
|
38
37
|
import { loadEnvFiles } from "./env/loadEnv";
|
|
@@ -206,18 +205,26 @@ const pinnedSandboxName = async ({
|
|
|
206
205
|
}
|
|
207
206
|
};
|
|
208
207
|
|
|
209
|
-
/**
|
|
210
|
-
const
|
|
211
|
-
({ target
|
|
208
|
+
/** A client for one env's own key, at the target's base URL. */
|
|
209
|
+
const webhookClientFor =
|
|
210
|
+
({ target }: { target: Target }) =>
|
|
211
|
+
({ secretKey }: { secretKey: string }) =>
|
|
212
|
+
createClient({
|
|
213
|
+
secretKey,
|
|
214
|
+
fetch: autumnFetch,
|
|
215
|
+
...(target.baseUrl ? { baseUrl: target.baseUrl } : {}),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
/** Every env the env files hold a key for, listed only when a webhook lane asks. */
|
|
219
|
+
const webhookEnvsFor =
|
|
220
|
+
({ target }: { target: Target }) =>
|
|
212
221
|
() =>
|
|
213
|
-
|
|
214
|
-
target,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
secretKey: requireSecretKey({ target }),
|
|
220
|
-
}),
|
|
222
|
+
webhookPullEnvs({
|
|
223
|
+
targetKeyName: target.secretKeyName,
|
|
224
|
+
listWebhooks: ({ secretKey }) =>
|
|
225
|
+
webhookClientFor({ target })({ secretKey }).listWebhooks({}),
|
|
226
|
+
fetchOrgInfo: ({ secretKey }) =>
|
|
227
|
+
fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey }),
|
|
221
228
|
});
|
|
222
229
|
|
|
223
230
|
/** `/organization/me` for the main key: what `sandbox use` and `init` report the org as. */
|
|
@@ -405,7 +412,9 @@ Linking a keyless org to an account:
|
|
|
405
412
|
client: clientFor({ target }),
|
|
406
413
|
configPath: configFlagOf({ command }),
|
|
407
414
|
dryRun: !apply,
|
|
408
|
-
|
|
415
|
+
prod: command.optsWithGlobals<GlobalFlags>().prod === true,
|
|
416
|
+
webhookEnvs: webhookEnvsFor({ target }),
|
|
417
|
+
webhookClientFor: webhookClientFor({ target }),
|
|
409
418
|
});
|
|
410
419
|
// A dry run that never saw the catalog must not read as clean.
|
|
411
420
|
process.exitCode = pushExitCode({ apply, result });
|
|
@@ -501,18 +510,7 @@ Linking a keyless org to an account:
|
|
|
501
510
|
overwrite: options.overwrite === true,
|
|
502
511
|
yes: options.yes === true,
|
|
503
512
|
prompter: prompterFor({ command }),
|
|
504
|
-
webhookEnvs: ()
|
|
505
|
-
webhookPullEnvs({
|
|
506
|
-
targetKeyName: target.secretKeyName,
|
|
507
|
-
listWebhooks: ({ secretKey }) =>
|
|
508
|
-
createClient({
|
|
509
|
-
secretKey,
|
|
510
|
-
fetch: autumnFetch,
|
|
511
|
-
...(target.baseUrl ? { baseUrl: target.baseUrl } : {}),
|
|
512
|
-
}).listWebhooks({}),
|
|
513
|
-
fetchOrgInfo: ({ secretKey }) =>
|
|
514
|
-
fetchOrgInfo({ baseUrl: targetBaseUrl({ target }), secretKey }),
|
|
515
|
-
}),
|
|
513
|
+
webhookEnvs: webhookEnvsFor({ target }),
|
|
516
514
|
});
|
|
517
515
|
writeStaleSkillsHint({ command });
|
|
518
516
|
},
|