@orchestraworks/cli 0.5.8 → 0.7.0
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/README.md +1 -1
- package/dist/cli/api/approval.js +3 -3
- package/dist/cli/commands/outpost/local.js +3 -3
- package/dist/cli/commands/outpost/machine-names.d.ts +17 -0
- package/dist/cli/commands/outpost/machine-names.js +22 -0
- package/dist/cli/commands/outpost/up.js +7 -5
- package/dist/cli/commands/worker/enroll.d.ts +4 -3
- package/dist/cli/commands/worker/enroll.js +4 -3
- package/dist/cli/core/auth.d.ts +11 -1
- package/dist/cli/core/auth.js +21 -4
- package/dist/cli/core/session-body.d.ts +1 -1
- package/dist/cli/core/session-body.js +1 -1
- package/dist/cli/firstRun.js +2 -2
- package/dist/cli/main.js +4 -3
- package/dist/constructs/environment.d.ts +24 -11
- package/dist/constructs/environment.js +76 -12
- package/dist/constructs/secret.d.ts +17 -7
- package/dist/constructs/secret.js +24 -16
- package/dist/core/labels.d.ts +32 -6
- package/dist/core/labels.js +70 -8
- package/dist/engine/diff.js +38 -11
- package/dist/engine/gateway-key-reconcile.d.ts +1 -2
- package/dist/engine/gateway-key-reconcile.js +19 -15
- package/dist/engine/pipeline-reconcile.js +2 -2
- package/dist/engine/pipeline-workflow-reconcile.js +15 -9
- package/dist/engine/reconcile.js +48 -15
- package/dist/engine/refusals.d.ts +0 -5
- package/dist/engine/refusals.js +1 -5
- package/dist/engine/secrets-apply.d.ts +10 -14
- package/dist/engine/secrets-apply.js +12 -17
- package/dist/engine/workflow-reconcile.d.ts +1 -1
- package/dist/generated/cli-commands.json +170 -34
- package/dist/generated/providers.d.ts +40 -0
- package/dist/generated/providers.js +30 -0
- package/docs/REFERENCE.md +9 -7
- package/node_modules/@orchestraworks/client/dist/esm/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentCredential.js +45 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/esm/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/esm/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/esm/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/dist/models/ApiErrorDetail.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/CreateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/Environment.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/Environment.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.d.ts +38 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentCredential.js +52 -0
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpec.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentProvisionSpecRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/EnvironmentSnapshot.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.d.ts +6 -0
- package/node_modules/@orchestraworks/client/dist/models/InjectionRecipe.js +2 -0
- package/node_modules/@orchestraworks/client/dist/models/OAuthTokenRequest.d.ts +1 -1
- package/node_modules/@orchestraworks/client/dist/models/Secret.d.ts +1 -7
- package/node_modules/@orchestraworks/client/dist/models/Secret.js +0 -4
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.d.ts +7 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateEnvironmentRequest.js +3 -0
- package/node_modules/@orchestraworks/client/dist/models/UpdateSecretRequest.d.ts +4 -3
- package/node_modules/@orchestraworks/client/dist/models/index.d.ts +1 -0
- package/node_modules/@orchestraworks/client/dist/models/index.js +1 -0
- package/node_modules/@orchestraworks/client/package.json +1 -1
- package/package.json +4 -4
- package/skills/orchestra/SKILL.md +132 -18
- package/skills/orchestra-fdk/references/authoring.md +511 -20
- package/skills/orchestra-fdk/references/environment.md +49 -12
- package/skills/orchestra-fdk/references/integrations/code-repo.md +26 -5
- package/skills/orchestra-fdk/references/phases/1-discover.md +1 -1
- package/skills/orchestra-fdk/references/phases/2-plan.md +1 -1
- package/skills/orchestra-fdk/references/troubleshooting.md +3 -2
- package/templates/bug-fix/main.ts +7 -4
- package/templates/engineering-team/lib/environment.ts +6 -3
- package/templates/engineering-team/package.json +1 -1
- package/templates/todo-factory/main.ts +7 -4
- package/templates/todo-factory/package.json +1 -1
|
@@ -77,6 +77,7 @@ export function cloneInjectionRecipe(recipe) {
|
|
|
77
77
|
return {
|
|
78
78
|
headers: (recipe.headers ?? []).map((h) => ({ name: h.name, value: h.value })),
|
|
79
79
|
removeHeaders: [...recipe.removeHeaders],
|
|
80
|
+
...(recipe.hosts !== undefined ? { hosts: [...recipe.hosts] } : {}),
|
|
80
81
|
};
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
@@ -319,16 +320,25 @@ function isIpv6Literal(host) {
|
|
|
319
320
|
/**
|
|
320
321
|
* The client-side mirror of the platform's write-time `hosts` checks, so a bad list fails at synth: each
|
|
321
322
|
* entry is an exact host, `*.` and a suffix, or `*`, and a secret that covers a host needs a recipe header to
|
|
322
|
-
* be presented by. The
|
|
323
|
+
* be presented by. The deprecated top-level `hosts` is moved into the recipe, and refused when the recipe
|
|
324
|
+
* declares a different list, an empty one included. The platform-origin and object-store refusals need the deployment's own addresses, so they
|
|
323
325
|
* stay the platform's `400`.
|
|
324
326
|
*/
|
|
325
|
-
function normalizeSecretHosts(
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
function normalizeSecretHosts(recipeHosts, deprecatedHosts, injection, label) {
|
|
328
|
+
if (recipeHosts !== undefined)
|
|
329
|
+
normalizeCredentialHosts(recipeHosts, `${label}: injection`);
|
|
330
|
+
if (deprecatedHosts !== undefined)
|
|
331
|
+
normalizeCredentialHosts(deprecatedHosts, label);
|
|
332
|
+
if (recipeHosts !== undefined &&
|
|
333
|
+
deprecatedHosts !== undefined &&
|
|
334
|
+
(recipeHosts.length !== deprecatedHosts.length || recipeHosts.some((host, index) => host !== deprecatedHosts[index]))) {
|
|
335
|
+
throw new Error(`${label}: 'hosts' and 'injection.hosts' name different hosts; declare 'injection.hosts' alone.`);
|
|
336
|
+
}
|
|
337
|
+
const hosts = recipeHosts ?? deprecatedHosts ?? [];
|
|
329
338
|
if (hosts.length > 0 && (injection === undefined || (injection.headers ?? []).length === 0)) {
|
|
330
|
-
throw new Error(`${label}: a secret with
|
|
331
|
-
`is presented, such as injection: { headers: [{ name: "Authorization", value: "Bearer \${secret}" }]
|
|
339
|
+
throw new Error(`${label}: a secret with hosts needs an 'injection' recipe with at least one header, which is how it ` +
|
|
340
|
+
`is presented, such as injection: { headers: [{ name: "Authorization", value: "Bearer \${secret}" }], ` +
|
|
341
|
+
`hosts: ["api.example.com"] }.`);
|
|
332
342
|
}
|
|
333
343
|
return [...hosts];
|
|
334
344
|
}
|
|
@@ -337,7 +347,6 @@ export class Secret extends Construct {
|
|
|
337
347
|
secretId;
|
|
338
348
|
#valueFrom;
|
|
339
349
|
#injection;
|
|
340
|
-
#hosts;
|
|
341
350
|
explicitSlug;
|
|
342
351
|
displayName;
|
|
343
352
|
description;
|
|
@@ -347,8 +356,9 @@ export class Secret extends Construct {
|
|
|
347
356
|
validateExplicitSlug(this, props.slug, "secret");
|
|
348
357
|
const label = `secret '${this.node.path.join("/")}'`;
|
|
349
358
|
this.#valueFrom = props.value._toValueFrom();
|
|
350
|
-
|
|
351
|
-
|
|
359
|
+
const recipe = props.injection === undefined ? undefined : normalizeInjectionRecipe(props.injection, label);
|
|
360
|
+
const hosts = normalizeSecretHosts(props.injection?.hosts, props.hosts, recipe, label);
|
|
361
|
+
this.#injection = recipe === undefined ? undefined : { ...recipe, hosts };
|
|
352
362
|
this.explicitSlug = props.slug;
|
|
353
363
|
this.displayName = props.name ?? id;
|
|
354
364
|
this.description = props.description ?? "";
|
|
@@ -368,7 +378,7 @@ export class Secret extends Construct {
|
|
|
368
378
|
}
|
|
369
379
|
/** @internal — a fresh copy of the declared hosts, for synth checks that read a declared secret. */
|
|
370
380
|
_hosts() {
|
|
371
|
-
return [...this.#hosts];
|
|
381
|
+
return [...(this.#injection?.hosts ?? [])];
|
|
372
382
|
}
|
|
373
383
|
/** @internal — whether the declared recipe has a header to present this secret by. */
|
|
374
384
|
_presentsHeader() {
|
|
@@ -385,16 +395,14 @@ export class Secret extends Construct {
|
|
|
385
395
|
const slug = this.slug();
|
|
386
396
|
// `description` is ALWAYS emitted, empty when unset, so it is never omit-masked; the only Secret PATCH
|
|
387
397
|
// rides a value overwrite that sends name+description together, so a clear travels with that write.
|
|
388
|
-
// `
|
|
389
|
-
// converges on the declaration.
|
|
390
|
-
//
|
|
391
|
-
// is DEEP-COPIED because the stored recipe is shared across synths: emitting it by reference would let a
|
|
398
|
+
// `injection` is emitted only when declared, its `hosts` always, `[]` when undeclared, so a live recipe with
|
|
399
|
+
// other hosts converges on the declaration and a dropped recipe is cleared, leaving the secret stored only.
|
|
400
|
+
// It is DEEP-COPIED because the stored recipe is shared across synths: emitting it by reference would let a
|
|
392
401
|
// caller mutating one returned plan corrupt the validated recipe for every later synth.
|
|
393
402
|
const payload = {
|
|
394
403
|
name: this.displayName,
|
|
395
404
|
slug,
|
|
396
405
|
description: this.description,
|
|
397
|
-
hosts: [...this.#hosts],
|
|
398
406
|
...(this.#injection !== undefined ? { injection: cloneInjectionRecipe(this.#injection) } : {}),
|
|
399
407
|
...(this.team !== undefined ? { teamId: this.team.teamId } : {}),
|
|
400
408
|
};
|
package/dist/core/labels.d.ts
CHANGED
|
@@ -2,18 +2,23 @@
|
|
|
2
2
|
* Reserving the whole prefix rather than one literal key is what makes the ownership marker unforgeable and
|
|
3
3
|
* leaves room for the other markers `apply` writes without a second rule.
|
|
4
4
|
*/
|
|
5
|
-
export declare const LABEL_KEY_PREFIX = "
|
|
5
|
+
export declare const LABEL_KEY_PREFIX = "orchestra.";
|
|
6
|
+
/**
|
|
7
|
+
* The prefix `apply` wrote before the rename. A factory applied then carries its markers under it until its next
|
|
8
|
+
* apply moves them, so every marker is read under both prefixes, the new key winning, and both stay reserved.
|
|
9
|
+
*/
|
|
10
|
+
export declare const OLD_LABEL_KEY_PREFIX = "sfdk.";
|
|
6
11
|
/**
|
|
7
12
|
* Stamped on every resource `apply` creates or adopts among the kinds whose API request exposes `labels`.
|
|
8
13
|
* Its value is the root factory name, which is always present — even for a factory that declares no team — so a CLI
|
|
9
14
|
* write against a managed resource can name the owning factory verbatim.
|
|
10
15
|
*/
|
|
11
|
-
export declare const MANAGED_BY_LABEL_KEY = "
|
|
16
|
+
export declare const MANAGED_BY_LABEL_KEY = "orchestra.managedBy";
|
|
12
17
|
export declare function rejectTemplateBraces(map: Record<string, string> | undefined, where: string): void;
|
|
13
18
|
/**
|
|
14
19
|
* A synth-time rejection rather than an apply-time overwrite on purpose: a declared key that could shadow
|
|
15
|
-
*
|
|
16
|
-
* key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
20
|
+
* the managed-by marker, under either prefix, would make the ownership marker forgeable and make a re-apply
|
|
21
|
+
* fight itself over the same key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
17
22
|
*/
|
|
18
23
|
export declare function rejectReservedLabelKeys(map: Record<string, string> | undefined, where: string): void;
|
|
19
24
|
/** Constructs call this rather than the two halves, so a new rule lands everywhere at once. */
|
|
@@ -28,12 +33,33 @@ export declare function stampManagedBy(labels: Record<string, string> | undefine
|
|
|
28
33
|
* an explicit `null`. This record of the keys `apply` last wrote from a declaration is how a later apply
|
|
29
34
|
* tells a key the declaration dropped, which it removes, from a key someone else set, which it leaves alone.
|
|
30
35
|
*/
|
|
31
|
-
export declare const DECLARED_LABELS_KEY = "
|
|
36
|
+
export declare const DECLARED_LABELS_KEY = "orchestra.declaredLabels";
|
|
37
|
+
/**
|
|
38
|
+
* Set on a gateway key's paired Secret while a mint is in flight, so a mint whose token write never landed is
|
|
39
|
+
* seen and refused by name rather than read as a usable Secret.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = "orchestra.pendingGatewayKeyToken";
|
|
42
|
+
/** The key a marker had under the old prefix. */
|
|
43
|
+
export declare function oldMarkerKey(key: string): string;
|
|
44
|
+
/** A marker's value: under its key, else under the old prefix's, else `undefined`. */
|
|
45
|
+
export declare function readMarker(labels: Record<string, unknown> | null | undefined, key: string): string | undefined;
|
|
46
|
+
/** A merge patch removing each marker these labels carry under the old prefix. */
|
|
47
|
+
export declare function oldMarkersCleared(labels: Record<string, unknown> | null | undefined): Record<string, null>;
|
|
48
|
+
/**
|
|
49
|
+
* The merge patch that moves each marker found under the old prefix, for a resource whose labels apply does not
|
|
50
|
+
* otherwise reconcile: the old key sent as `null`, and the new key given the old value unless it is already
|
|
51
|
+
* live. `undefined` when no marker is under the old prefix.
|
|
52
|
+
*/
|
|
53
|
+
export declare function oldMarkersMoved(labels: Record<string, unknown> | null | undefined): Record<string, string | null> | undefined;
|
|
54
|
+
/** The factory whose apply stamped these labels, read under either prefix. */
|
|
55
|
+
export declare function managedByOf(labels: Record<string, unknown> | null | undefined): string | undefined;
|
|
32
56
|
/** A create's labels: the declaration, the record of its keys when it declares any, and the stamps. */
|
|
33
57
|
export declare function createLabels(declared: Record<string, string> | undefined, stamps?: Record<string, string>): Record<string, string>;
|
|
34
58
|
/**
|
|
35
59
|
* The merge patch that brings the live labels to the declaration, or `undefined` when they already agree: a
|
|
36
60
|
* declared value that differs, a key an earlier apply declared and this one does not (as `null`), the record
|
|
37
|
-
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift.
|
|
61
|
+
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift. A marker found under
|
|
62
|
+
* the old prefix is moved: its old key is sent as `null`, and its new key carries the stamp, or the old value
|
|
63
|
+
* when nothing stamps it.
|
|
38
64
|
*/
|
|
39
65
|
export declare function labelsPatch(declared: Record<string, string> | undefined, live: Record<string, string> | null | undefined, stamps?: Record<string, string>): Record<string, string | null> | undefined;
|
package/dist/core/labels.js
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* Reserving the whole prefix rather than one literal key is what makes the ownership marker unforgeable and
|
|
5
5
|
* leaves room for the other markers `apply` writes without a second rule.
|
|
6
6
|
*/
|
|
7
|
-
export const LABEL_KEY_PREFIX = "
|
|
7
|
+
export const LABEL_KEY_PREFIX = "orchestra.";
|
|
8
|
+
/**
|
|
9
|
+
* The prefix `apply` wrote before the rename. A factory applied then carries its markers under it until its next
|
|
10
|
+
* apply moves them, so every marker is read under both prefixes, the new key winning, and both stay reserved.
|
|
11
|
+
*/
|
|
12
|
+
export const OLD_LABEL_KEY_PREFIX = "sfdk.";
|
|
13
|
+
const RESERVED_LABEL_KEY_PREFIXES = [LABEL_KEY_PREFIX, OLD_LABEL_KEY_PREFIX];
|
|
8
14
|
/**
|
|
9
15
|
* Stamped on every resource `apply` creates or adopts among the kinds whose API request exposes `labels`.
|
|
10
16
|
* Its value is the root factory name, which is always present — even for a factory that declares no team — so a CLI
|
|
@@ -24,16 +30,17 @@ export function rejectTemplateBraces(map, where) {
|
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
26
32
|
* A synth-time rejection rather than an apply-time overwrite on purpose: a declared key that could shadow
|
|
27
|
-
*
|
|
28
|
-
* key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
33
|
+
* the managed-by marker, under either prefix, would make the ownership marker forgeable and make a re-apply
|
|
34
|
+
* fight itself over the same key. Refusing at the authoring boundary leaves the marker exactly one writer.
|
|
29
35
|
*/
|
|
30
36
|
export function rejectReservedLabelKeys(map, where) {
|
|
31
37
|
if (map === undefined)
|
|
32
38
|
return;
|
|
33
39
|
for (const key of Object.keys(map)) {
|
|
34
|
-
|
|
40
|
+
const prefix = reservedPrefixOf(key);
|
|
41
|
+
if (prefix !== undefined) {
|
|
35
42
|
throw new Error(`Label key ${JSON.stringify(key)} in ${where} uses the reserved ` +
|
|
36
|
-
`"${
|
|
43
|
+
`"${prefix}" prefix, which belongs to orchestra itself (it is how apply marks the ` +
|
|
37
44
|
`resources it manages, e.g. ${JSON.stringify(MANAGED_BY_LABEL_KEY)}). Rename the key to ` +
|
|
38
45
|
`anything outside that prefix.`);
|
|
39
46
|
}
|
|
@@ -57,6 +64,51 @@ export function stampManagedBy(labels, factory) {
|
|
|
57
64
|
* tells a key the declaration dropped, which it removes, from a key someone else set, which it leaves alone.
|
|
58
65
|
*/
|
|
59
66
|
export const DECLARED_LABELS_KEY = `${LABEL_KEY_PREFIX}declaredLabels`;
|
|
67
|
+
/**
|
|
68
|
+
* Set on a gateway key's paired Secret while a mint is in flight, so a mint whose token write never landed is
|
|
69
|
+
* seen and refused by name rather than read as a usable Secret.
|
|
70
|
+
*/
|
|
71
|
+
export const PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY = `${LABEL_KEY_PREFIX}pendingGatewayKeyToken`;
|
|
72
|
+
const MARKER_KEYS = [MANAGED_BY_LABEL_KEY, DECLARED_LABELS_KEY, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY];
|
|
73
|
+
/** The key a marker had under the old prefix. */
|
|
74
|
+
export function oldMarkerKey(key) {
|
|
75
|
+
return OLD_LABEL_KEY_PREFIX + key.slice(LABEL_KEY_PREFIX.length);
|
|
76
|
+
}
|
|
77
|
+
/** A marker's value: under its key, else under the old prefix's, else `undefined`. */
|
|
78
|
+
export function readMarker(labels, key) {
|
|
79
|
+
const value = labels?.[key] ?? labels?.[oldMarkerKey(key)];
|
|
80
|
+
return typeof value === "string" ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
/** A merge patch removing each marker these labels carry under the old prefix. */
|
|
83
|
+
export function oldMarkersCleared(labels) {
|
|
84
|
+
return Object.fromEntries(MARKER_KEYS.map(oldMarkerKey)
|
|
85
|
+
.filter((key) => labels?.[key] !== undefined)
|
|
86
|
+
.map((key) => [key, null]));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The merge patch that moves each marker found under the old prefix, for a resource whose labels apply does not
|
|
90
|
+
* otherwise reconcile: the old key sent as `null`, and the new key given the old value unless it is already
|
|
91
|
+
* live. `undefined` when no marker is under the old prefix.
|
|
92
|
+
*/
|
|
93
|
+
export function oldMarkersMoved(labels) {
|
|
94
|
+
const patch = {};
|
|
95
|
+
for (const key of MARKER_KEYS) {
|
|
96
|
+
const old = labels?.[oldMarkerKey(key)];
|
|
97
|
+
if (old === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
patch[oldMarkerKey(key)] = null;
|
|
100
|
+
if (labels?.[key] === undefined && typeof old === "string")
|
|
101
|
+
patch[key] = old;
|
|
102
|
+
}
|
|
103
|
+
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
104
|
+
}
|
|
105
|
+
/** The factory whose apply stamped these labels, read under either prefix. */
|
|
106
|
+
export function managedByOf(labels) {
|
|
107
|
+
return readMarker(labels, MANAGED_BY_LABEL_KEY);
|
|
108
|
+
}
|
|
109
|
+
function reservedPrefixOf(key) {
|
|
110
|
+
return RESERVED_LABEL_KEY_PREFIXES.find((prefix) => key.startsWith(prefix));
|
|
111
|
+
}
|
|
60
112
|
/** A create's labels: the declaration, the record of its keys when it declares any, and the stamps. */
|
|
61
113
|
export function createLabels(declared, stamps = {}) {
|
|
62
114
|
return { ...withDeclaredKeys(declared ?? {}), ...stamps };
|
|
@@ -64,7 +116,9 @@ export function createLabels(declared, stamps = {}) {
|
|
|
64
116
|
/**
|
|
65
117
|
* The merge patch that brings the live labels to the declaration, or `undefined` when they already agree: a
|
|
66
118
|
* declared value that differs, a key an earlier apply declared and this one does not (as `null`), the record
|
|
67
|
-
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift.
|
|
119
|
+
* of declared keys, and the stamps. A live key no apply declared is neither sent nor drift. A marker found under
|
|
120
|
+
* the old prefix is moved: its old key is sent as `null`, and its new key carries the stamp, or the old value
|
|
121
|
+
* when nothing stamps it.
|
|
68
122
|
*/
|
|
69
123
|
export function labelsPatch(declared, live, stamps = {}) {
|
|
70
124
|
const current = live ?? {};
|
|
@@ -80,6 +134,14 @@ export function labelsPatch(declared, live, stamps = {}) {
|
|
|
80
134
|
}
|
|
81
135
|
if (!(DECLARED_LABELS_KEY in desired) && DECLARED_LABELS_KEY in current)
|
|
82
136
|
patch[DECLARED_LABELS_KEY] = null;
|
|
137
|
+
for (const key of MARKER_KEYS) {
|
|
138
|
+
const old = oldMarkerKey(key);
|
|
139
|
+
if (!(old in current))
|
|
140
|
+
continue;
|
|
141
|
+
patch[old] = null;
|
|
142
|
+
if (key !== DECLARED_LABELS_KEY && !(key in desired) && !(key in current))
|
|
143
|
+
patch[key] = current[old];
|
|
144
|
+
}
|
|
83
145
|
return Object.keys(patch).length > 0 ? patch : undefined;
|
|
84
146
|
}
|
|
85
147
|
function withDeclaredKeys(declared) {
|
|
@@ -88,13 +150,13 @@ function withDeclaredKeys(declared) {
|
|
|
88
150
|
}
|
|
89
151
|
/** A malformed record removes nothing, and a reserved key is never removed through it. */
|
|
90
152
|
function previouslyDeclaredKeys(live) {
|
|
91
|
-
const record = live
|
|
153
|
+
const record = readMarker(live, DECLARED_LABELS_KEY);
|
|
92
154
|
if (record === undefined)
|
|
93
155
|
return [];
|
|
94
156
|
try {
|
|
95
157
|
const keys = JSON.parse(record);
|
|
96
158
|
return Array.isArray(keys)
|
|
97
|
-
? keys.filter((key) => typeof key === "string" &&
|
|
159
|
+
? keys.filter((key) => typeof key === "string" && reservedPrefixOf(key) === undefined)
|
|
98
160
|
: [];
|
|
99
161
|
}
|
|
100
162
|
catch {
|
package/dist/engine/diff.js
CHANGED
|
@@ -10,13 +10,12 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { CONNECTOR_SECRET_WIRE_FIELDS } from "../constructs/connector.js";
|
|
11
11
|
import { isEnvImportMarker, parseEnvImportMarker } from "../constructs/references.js";
|
|
12
12
|
import { gatewayKeyRefOf } from "../core/gateway-key-recipe.js";
|
|
13
|
-
import { labelsPatch, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
|
|
13
|
+
import { labelsPatch, MANAGED_BY_LABEL_KEY, oldMarkersMoved, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, readMarker, } from "../core/labels.js";
|
|
14
14
|
import { isRefToken, refToken } from "../core/tokens.js";
|
|
15
15
|
import { createTeamResolver, mapError, modelSettingsUnchanged, standbyCapacityUnchanged, platformErrorMessage, } from "./apply-core.js";
|
|
16
16
|
import { httpStatusOf } from "./client.js";
|
|
17
17
|
import { describeRefusal, describeStageEffects, leftoverRefusals, pipelineChange, pipelineUnchanged, previewPipelineChange, resolveGateReferences, workflowStagesAfterApply, } from "./pipeline-reconcile.js";
|
|
18
18
|
import { describeEntryChange, describeOverlaps, entriesUnchanged, pipelineEntryChange, proposedWorkflows, } from "./pipeline-workflow-reconcile.js";
|
|
19
|
-
import { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "./gateway-key-reconcile.js";
|
|
20
19
|
import { scanModelProviderKeys } from "./model-provider-key-reconcile.js";
|
|
21
20
|
import { computeProviderPatch, } from "./compute-provider-reconcile.js";
|
|
22
21
|
import { paginate } from "./pages.js";
|
|
@@ -25,7 +24,7 @@ import { reservedProviderSecretIds } from "./preflight.js";
|
|
|
25
24
|
import { gatewayKeyPairRefusal, refusalOf, } from "./refusals.js";
|
|
26
25
|
import { connectionUnchanged, environmentUnchanged, getBySlugOrUndefined, subsetDeepEqual, } from "./reconcile.js";
|
|
27
26
|
import { removedConfigurationNotice } from "./connector-configuration.js";
|
|
28
|
-
import { containsCommandLineRecipe,
|
|
27
|
+
import { containsCommandLineRecipe, declaredSecretInjection, metadataInjectionChange, pendingCredentialWrite, replacesLiveCredentialValue, resolveSecretValueSource, rootEnvName, } from "./secrets-apply.js";
|
|
29
28
|
/** Overridable for tests. */
|
|
30
29
|
export const ORPHAN_SCAN_CAP = 5;
|
|
31
30
|
/**
|
|
@@ -428,20 +427,30 @@ async function diffSecret(resource, pass) {
|
|
|
428
427
|
}
|
|
429
428
|
return entry(resource, "no-change", unsetNote, marks);
|
|
430
429
|
}
|
|
431
|
-
/** Value-free: a recipe and
|
|
430
|
+
/** Value-free: a recipe and its host list are shape, never credential material. */
|
|
432
431
|
const INJECTION_RECIPE_CHANGE = "injection recipe changes";
|
|
433
432
|
const HOSTS_CHANGE = "hosts change";
|
|
433
|
+
const OWNERSHIP_LABELS_MOVE = "ownership labels move to the orchestra. prefix";
|
|
434
434
|
function secretMetadataDetail(resource, live) {
|
|
435
435
|
const details = [
|
|
436
|
-
...(
|
|
437
|
-
|
|
438
|
-
: []),
|
|
439
|
-
...(metadataHostsChange(declaredSecretHosts(resource.payload), live.hosts) !== undefined ? [HOSTS_CHANGE] : []),
|
|
436
|
+
...secretRecipeDetails(resource, live),
|
|
437
|
+
...(oldMarkersMoved(live.labels) !== undefined ? [OWNERSHIP_LABELS_MOVE] : []),
|
|
440
438
|
];
|
|
441
439
|
return details.length > 0 ? details.join("; ") : undefined;
|
|
442
440
|
}
|
|
443
|
-
function
|
|
444
|
-
|
|
441
|
+
function secretRecipeDetails(resource, live) {
|
|
442
|
+
const declared = declaredSecretInjection(resource.payload);
|
|
443
|
+
if (metadataInjectionChange(declared, live.injection) === undefined)
|
|
444
|
+
return [];
|
|
445
|
+
if (declared === undefined || live.injection === undefined)
|
|
446
|
+
return [INJECTION_RECIPE_CHANGE];
|
|
447
|
+
const { hosts: desiredHosts = [], ...desiredRecipe } = declared;
|
|
448
|
+
const { hosts: liveHosts = [], ...liveRecipe } = live.injection;
|
|
449
|
+
const sameHosts = desiredHosts.length === liveHosts.length && desiredHosts.every((host, index) => host === liveHosts[index]);
|
|
450
|
+
return [
|
|
451
|
+
...(metadataInjectionChange(desiredRecipe, liveRecipe) !== undefined ? [INJECTION_RECIPE_CHANGE] : []),
|
|
452
|
+
...(sameHosts ? [] : [HOSTS_CHANGE]),
|
|
453
|
+
];
|
|
445
454
|
}
|
|
446
455
|
function withDetail(note, detail) {
|
|
447
456
|
return note === undefined ? detail : `${note}; ${detail}`;
|
|
@@ -480,7 +489,7 @@ async function diffGatewayKey(resource, plan, pass) {
|
|
|
480
489
|
return entry(resource, keyUnchanged(body, live) ? "no-change" : "update", undefined, marks);
|
|
481
490
|
}
|
|
482
491
|
function hasPendingMarker(secret) {
|
|
483
|
-
return secret?.labels
|
|
492
|
+
return readMarker(secret?.labels, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY) !== undefined;
|
|
484
493
|
}
|
|
485
494
|
/**
|
|
486
495
|
* Mirrors apply's `pairedSecretResource` lookup but returns `undefined` instead of throwing when the plan
|
|
@@ -630,6 +639,16 @@ async function diffEnvironment(resource, pass) {
|
|
|
630
639
|
if (typeof providerRef === "string" && providerRef.length > 0) {
|
|
631
640
|
launchSpec.computeProviderId = await resolveIdForDiff(providerRef, "compute-provider", pass);
|
|
632
641
|
}
|
|
642
|
+
// Each credential reference an env value or a default names, resolved to the id apply writes.
|
|
643
|
+
const env = (launchSpec.env ?? {});
|
|
644
|
+
for (const [name, value] of Object.entries(env)) {
|
|
645
|
+
if (!value.startsWith("orchestra:"))
|
|
646
|
+
continue;
|
|
647
|
+
env[name] = `orchestra:${await credentialReferenceForDiff(value.slice("orchestra:".length), pass)}`;
|
|
648
|
+
}
|
|
649
|
+
for (const entry of (body.credentials ?? [])) {
|
|
650
|
+
entry.credential = await credentialReferenceForDiff(String(entry.credential), pass);
|
|
651
|
+
}
|
|
633
652
|
const bound = pullSecrets.length === 0
|
|
634
653
|
? null
|
|
635
654
|
: verdictFor(resource, live, {
|
|
@@ -929,6 +948,14 @@ async function orphanReport(plan, apis, cap) {
|
|
|
929
948
|
function refKeyOf(resource) {
|
|
930
949
|
return refToken(resource.kind, resource.path.split("/"));
|
|
931
950
|
}
|
|
951
|
+
/** `secret/<ref>` or `connection/<ref>` with its ref resolved as {@link resolveIdForDiff} resolves one. */
|
|
952
|
+
async function credentialReferenceForDiff(text, pass) {
|
|
953
|
+
const match = /^(secret|connection)\/(.+)$/.exec(text);
|
|
954
|
+
if (match === null)
|
|
955
|
+
return text;
|
|
956
|
+
const kind = match[1];
|
|
957
|
+
return `${kind}/${await resolveIdForDiff(match[2], kind, pass)}`;
|
|
958
|
+
}
|
|
932
959
|
/** Lenient: any miss leaves the ref unchanged, so the compare simply reports `update`. */
|
|
933
960
|
async function resolveIdForDiff(ref, literalKind, pass) {
|
|
934
961
|
if (isRefToken(ref)) {
|
|
@@ -8,7 +8,7 @@ import { PreflightError } from "./preflight.js";
|
|
|
8
8
|
* second column — and is removed by the final token PATCH. It lives in a leaf module because the aggregate
|
|
9
9
|
* refusal classifier's rotation message names it too.
|
|
10
10
|
*/
|
|
11
|
-
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "
|
|
11
|
+
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "../core/labels.js";
|
|
12
12
|
/**
|
|
13
13
|
* The platform requires a non-empty `value` on create and the token does not exist yet, so a reservation
|
|
14
14
|
* writes this inert literal and the final PATCH replaces it. The Secret is detectably pending for exactly as
|
|
@@ -38,7 +38,6 @@ export interface LiveSecretRow {
|
|
|
38
38
|
id: string;
|
|
39
39
|
teamId?: string;
|
|
40
40
|
injection?: InjectionRecipe;
|
|
41
|
-
hosts?: string[];
|
|
42
41
|
labels?: Record<string, string>;
|
|
43
42
|
}
|
|
44
43
|
/** Value-free. */
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
import { gatewayKeyRefOf } from "../core/gateway-key-recipe.js";
|
|
14
14
|
import { refToken } from "../core/tokens.js";
|
|
15
15
|
import { credentialValueWritten, driftedFields, failing, getBySlugOrUndefined, pushOutcome, stampTeam, } from "./apply-core.js";
|
|
16
|
-
import {
|
|
16
|
+
import { refuseIf } from "./refusals.js";
|
|
17
17
|
import { httpStatusOf } from "./client.js";
|
|
18
18
|
import { PreflightError } from "./preflight.js";
|
|
19
|
-
import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY } from "../core/labels.js";
|
|
20
|
-
import {
|
|
19
|
+
import { createLabels, labelsPatch, MANAGED_BY_LABEL_KEY, oldMarkersCleared, oldMarkersMoved, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY, readMarker, } from "../core/labels.js";
|
|
20
|
+
import { declaredSecretInjection, metadataInjectionChange, pendingSecretWrite, reconcileInjection, } from "./secrets-apply.js";
|
|
21
21
|
/**
|
|
22
22
|
* The reserved marker rides the ordinary labels wire field — never smuggled into the write-only value or a
|
|
23
23
|
* second column — and is removed by the final token PATCH. It lives in a leaf module because the aggregate
|
|
24
24
|
* refusal classifier's rotation message names it too.
|
|
25
25
|
*/
|
|
26
|
-
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "
|
|
26
|
+
export { PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY } from "../core/labels.js";
|
|
27
27
|
/**
|
|
28
28
|
* The platform requires a non-empty `value` on create and the token does not exist yet, so a reservation
|
|
29
29
|
* writes this inert literal and the final PATCH replaces it. The Secret is detectably pending for exactly as
|
|
@@ -149,14 +149,17 @@ async function applySteadyGatewayKey(decision, apis, outcomes, applied) {
|
|
|
149
149
|
action = "updated";
|
|
150
150
|
}
|
|
151
151
|
pushOutcome(outcomes, applied, decision.resource, live.id, action);
|
|
152
|
-
// The paired Secret needs no value write in steady state, but a recipe
|
|
152
|
+
// The paired Secret needs no value write in steady state, but a recipe edit, its hosts included, still reconciles
|
|
153
153
|
// through a value-less PATCH — exactly what `applySecrets` would have done had this lane not owned it.
|
|
154
154
|
const liveSecret = decision.liveSecret;
|
|
155
155
|
const injectionChange = metadataInjectionChange(secretInjection(decision.secretResource), liveSecret.injection);
|
|
156
|
-
const
|
|
156
|
+
const labelsMove = oldMarkersMoved(liveSecret.labels);
|
|
157
157
|
let secretAction = "unchanged";
|
|
158
|
-
if (injectionChange !== undefined ||
|
|
159
|
-
const body = {
|
|
158
|
+
if (injectionChange !== undefined || labelsMove !== undefined) {
|
|
159
|
+
const body = {
|
|
160
|
+
...injectionChange,
|
|
161
|
+
...(labelsMove !== undefined ? { labels: labelsMove } : {}),
|
|
162
|
+
};
|
|
160
163
|
await failing(() => apis.secrets.updateSecret({ id: liveSecret.id, updateSecretRequest: body }), { kind: "secret", slug: decision.secretSlug, op: "update" }, outcomes);
|
|
161
164
|
secretAction = "updated";
|
|
162
165
|
}
|
|
@@ -174,9 +177,12 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
174
177
|
const refKey = refToken("gateway-key", decision.resource.path.split("/"));
|
|
175
178
|
// The FINAL patch removes the pending marker and stamps ownership; the merge keeps every caller-authored entry
|
|
176
179
|
// an adopted Secret carries.
|
|
177
|
-
const finalLabels = {
|
|
180
|
+
const finalLabels = {
|
|
181
|
+
[MANAGED_BY_LABEL_KEY]: factory,
|
|
182
|
+
[PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: null,
|
|
183
|
+
...oldMarkersCleared(decision.liveSecret?.labels),
|
|
184
|
+
};
|
|
178
185
|
const declaredInjection = secretInjection(decision.secretResource);
|
|
179
|
-
const declaredHosts = declaredSecretHosts(decision.secretResource.payload);
|
|
180
186
|
// (1) Make the paired Secret detectably pending BEFORE the mint.
|
|
181
187
|
let secretId;
|
|
182
188
|
if (decision.action === "mint-new") {
|
|
@@ -188,7 +194,6 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
188
194
|
labels: { [PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: decision.slug },
|
|
189
195
|
...placement,
|
|
190
196
|
...(declaredInjection !== undefined ? { injection: declaredInjection } : {}),
|
|
191
|
-
...(declaredHosts !== undefined ? { hosts: declaredHosts } : {}),
|
|
192
197
|
};
|
|
193
198
|
const created = await failing(() => apis.secrets.createSecret({ createSecretRequest: createBody }), { kind: "secret", slug: decision.secretSlug, op: "create" }, outcomes);
|
|
194
199
|
secretId = created.id;
|
|
@@ -202,6 +207,7 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
202
207
|
await patchSecretPendingMarker(apis, outcomes, decision.secretSlug, secretId, {
|
|
203
208
|
[MANAGED_BY_LABEL_KEY]: factory,
|
|
204
209
|
[PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY]: decision.slug,
|
|
210
|
+
...oldMarkersCleared(liveSecret.labels),
|
|
205
211
|
});
|
|
206
212
|
}
|
|
207
213
|
}
|
|
@@ -237,8 +243,6 @@ async function mintGatewayKey(decision, apis, outcomes, applied, tokens, factory
|
|
|
237
243
|
// No team: a `mint-new` reservation already placed the create, and an adopted or resumed Secret is
|
|
238
244
|
// only here because the decide phase found it already in the declared team.
|
|
239
245
|
...(injection !== undefined ? { injection } : {}),
|
|
240
|
-
// A `mint-new` reservation already carried the hosts on its create.
|
|
241
|
-
...(decision.action !== "mint-new" && declaredHosts !== undefined ? { hosts: declaredHosts } : {}),
|
|
242
246
|
},
|
|
243
247
|
}), { kind: "secret", slug: decision.secretSlug, op: "update" }, outcomes);
|
|
244
248
|
// The pinned asymmetry between the pair's two rows: the `gateway-key` row is the MINT, whose sensitivity
|
|
@@ -277,7 +281,7 @@ function pairedSecretResource(plan, key) {
|
|
|
277
281
|
return paired[0];
|
|
278
282
|
}
|
|
279
283
|
function hasPendingMarker(secret) {
|
|
280
|
-
return secret.labels
|
|
284
|
+
return readMarker(secret.labels, PENDING_GATEWAY_KEY_TOKEN_LABEL_KEY) !== undefined;
|
|
281
285
|
}
|
|
282
286
|
/**
|
|
283
287
|
* Exactly the fields the update request accepts and the construct emits. `monthlyCreditCap` is present only
|
|
@@ -293,5 +297,5 @@ function declaredKeyState(payload) {
|
|
|
293
297
|
};
|
|
294
298
|
}
|
|
295
299
|
function secretInjection(secretResource) {
|
|
296
|
-
return secretResource.payload
|
|
300
|
+
return declaredSecretInjection(secretResource.payload);
|
|
297
301
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { OPEN_CATEGORIES } from "../constructs/pipeline.js";
|
|
12
12
|
import { resolveConditionReferences, resolveStageInField } from "./condition-references.js";
|
|
13
13
|
import { wireNode } from "../constructs/workflow-filter.js";
|
|
14
|
-
import { createLabels, labelsPatch,
|
|
14
|
+
import { createLabels, labelsPatch, managedByOf } from "../core/labels.js";
|
|
15
15
|
import { refToken } from "../core/tokens.js";
|
|
16
16
|
import { exactDeepEqual, failing, getBySlugOrUndefined, stampTeam, } from "./apply-core.js";
|
|
17
17
|
import { PreflightError } from "./preflight-errors.js";
|
|
@@ -242,7 +242,7 @@ export async function workflowStagesAfterApply(pipeline, live, resources, factor
|
|
|
242
242
|
const versions = [
|
|
243
243
|
...declaredEntries.filter((entry) => refs.has(entry.workflow)).map((entry) => entry.version),
|
|
244
244
|
...liveEntries
|
|
245
|
-
.filter((entry) => entry.labels
|
|
245
|
+
.filter((entry) => managedByOf(entry.labels) !== factory && refs.has(entry.workflow.id))
|
|
246
246
|
.map((entry) => entry.version ?? null),
|
|
247
247
|
];
|
|
248
248
|
if (versions.some((version) => typeof version === "number"))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// A pipeline's workflow entries. Apply owns only the entries it stamped with this factory's
|
|
1
|
+
// A pipeline's workflow entries. Apply owns only the entries it stamped with this factory's managed-by marker:
|
|
2
2
|
// each declared entry takes a stamped live entry of its workflow, an identical one first and then the next in
|
|
3
3
|
// order, and a changed one is patched field by field; a declared entry left over is added with the stamp, and
|
|
4
4
|
// a stamped entry nothing declares is removed. Every other entry stays where it is and is named, since an
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
// new rules before its entry says so; the entries phase then sets each to its declared version.
|
|
10
10
|
import { APPLIED_VERSION } from "../constructs/pipeline.js";
|
|
11
11
|
import { wireNode } from "../constructs/workflow-filter.js";
|
|
12
|
-
import { MANAGED_BY_LABEL_KEY } from "../core/labels.js";
|
|
12
|
+
import { MANAGED_BY_LABEL_KEY, managedByOf, oldMarkersCleared } from "../core/labels.js";
|
|
13
13
|
import { exactDeepEqual, failing, getBySlugOrUndefined } from "./apply-core.js";
|
|
14
14
|
import { rawJsonBody } from "./raw-json-request.js";
|
|
15
15
|
export function entryChange(declared, live, factory) {
|
|
16
|
-
const stamped = live.filter((entry) => entry.labels
|
|
16
|
+
const stamped = live.filter((entry) => managedByOf(entry.labels) === factory);
|
|
17
17
|
const taken = new Set();
|
|
18
18
|
const take = (entry, fits) => {
|
|
19
19
|
const found = stamped.find((candidate) => !taken.has(candidate.id) && names(entry, candidate) && fits(candidate));
|
|
@@ -21,18 +21,18 @@ export function entryChange(declared, live, factory) {
|
|
|
21
21
|
taken.add(found.id);
|
|
22
22
|
return found;
|
|
23
23
|
};
|
|
24
|
-
const exact = declared.map((entry) => take(entry, (candidate) => patchOf(entry, candidate) === undefined));
|
|
24
|
+
const exact = declared.map((entry) => take(entry, (candidate) => patchOf(entry, candidate, factory) === undefined));
|
|
25
25
|
const matched = declared.map((entry, index) => exact[index] ?? take(entry, () => true));
|
|
26
26
|
return {
|
|
27
27
|
declared: declared.map((entry, index) => ({ entry, ...(matched[index] ? { liveId: matched[index].id } : {}) })),
|
|
28
28
|
create: declared.filter((_, index) => matched[index] === undefined),
|
|
29
29
|
update: declared.flatMap((entry, index) => {
|
|
30
30
|
const live = matched[index];
|
|
31
|
-
const patch = live !== undefined ? patchOf(entry, live) : undefined;
|
|
31
|
+
const patch = live !== undefined ? patchOf(entry, live, factory) : undefined;
|
|
32
32
|
return live !== undefined && patch !== undefined ? [{ id: live.id, patch }] : [];
|
|
33
33
|
}),
|
|
34
34
|
remove: stamped.filter((entry) => !taken.has(entry.id)),
|
|
35
|
-
unmanaged: live.filter((entry) => entry.labels
|
|
35
|
+
unmanaged: live.filter((entry) => managedByOf(entry.labels) !== factory),
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
export function entriesUnchanged(change) {
|
|
@@ -87,7 +87,7 @@ export async function applyEntriesPhase(apis, pipelines, versions, factory, outc
|
|
|
87
87
|
*/
|
|
88
88
|
export async function pinFollowingEntries(apis, pipeline, workflow, version, factory) {
|
|
89
89
|
const live = await getBySlugOrUndefined(() => apis.pipelines.getPipeline({ id: pipeline }));
|
|
90
|
-
const following = (live?.workflows ?? []).filter((entry) => entry.labels
|
|
90
|
+
const following = (live?.workflows ?? []).filter((entry) => managedByOf(entry.labels) === factory && entry.workflow.id === workflow.id && entry.version === undefined);
|
|
91
91
|
for (const entry of following)
|
|
92
92
|
await patchEntry(apis, live.id, entry.id, { version });
|
|
93
93
|
return following.length;
|
|
@@ -165,9 +165,15 @@ export function describeOverlaps(proposedList, overlaps) {
|
|
|
165
165
|
function names(entry, live) {
|
|
166
166
|
return entry.workflow === live.workflow.slug || entry.workflow === live.workflow.id;
|
|
167
167
|
}
|
|
168
|
-
/**
|
|
169
|
-
|
|
168
|
+
/**
|
|
169
|
+
* The fields a patch must send to make [live] the declared entry, null resetting; `undefined` when none. An entry
|
|
170
|
+
* stamped under the old prefix has its stamp moved.
|
|
171
|
+
*/
|
|
172
|
+
function patchOf(entry, live, factory) {
|
|
170
173
|
const patch = {};
|
|
174
|
+
const moved = oldMarkersCleared(live.labels);
|
|
175
|
+
if (Object.keys(moved).length > 0)
|
|
176
|
+
patch.labels = { ...moved, [MANAGED_BY_LABEL_KEY]: factory };
|
|
171
177
|
if (entry.version !== (live.version ?? null))
|
|
172
178
|
patch.version = entry.version;
|
|
173
179
|
const where = entry.where === null ? null : wireNode(entry.where);
|