@intentius/chant 0.45.0 → 0.46.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/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/lexicon.d.ts +182 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +1 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +130 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +2 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/op/builders.d.ts +36 -7
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +1 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/package.json +6 -1
- package/src/cli/commands/build.test.ts +131 -0
- package/src/cli/commands/build.ts +20 -0
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +19 -6
- package/src/cli/handlers/graph.ts +2 -2
- package/src/cli/handlers/lifecycle.test.ts +231 -1
- package/src/cli/handlers/lifecycle.ts +219 -3
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/registry.ts +7 -0
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/config.test.ts +40 -0
- package/src/config.ts +58 -1
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/lexicon.ts +182 -2
- package/src/lifecycle/index.ts +1 -0
- package/src/lifecycle/teardown.test.ts +537 -0
- package/src/lifecycle/teardown.ts +357 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/index.ts +4 -1
- package/src/op/builders.ts +40 -7
- package/src/op/index.ts +1 -1
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
package/src/testing.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@intentius/chant/testing` (#1224) — the live-stack test harness.
|
|
3
|
+
*
|
|
4
|
+
* A vitest suite deploys a real instance of its project once, asserts against
|
|
5
|
+
* it, and tears it down — emulator-locally on a dev machine, real-cloud in CI,
|
|
6
|
+
* with the same suite text. One call composes machinery that already exists:
|
|
7
|
+
*
|
|
8
|
+
* - **build** — the same programmatic path `chant build` takes: config
|
|
9
|
+
* resolution (`buildParams` + the ownership marker) mirrored from the CLI,
|
|
10
|
+
* then `build()` over the project's own serializers.
|
|
11
|
+
* - **apply** — an additive `nativeApply` per built lexicon output, run
|
|
12
|
+
* in-process through the local Op executor (`runOpLocally`). `deleteMode`
|
|
13
|
+
* is `"never"`: a test deploy creates and updates, nothing else.
|
|
14
|
+
* - **destroy** — #1222's marker-scoped teardown (`executeTeardown`), called
|
|
15
|
+
* in-process for exactly this suite's environment. Stateless by design:
|
|
16
|
+
* a crashed suite's environment is recovered by calling destroy again (or
|
|
17
|
+
* `chant lifecycle teardown <env> --yes`).
|
|
18
|
+
*
|
|
19
|
+
* ## Isolation
|
|
20
|
+
*
|
|
21
|
+
* Every deploy targets its own environment: `test-<suite>-<nonce>` by default
|
|
22
|
+
* ({@link testEnvName}), so parallel CI jobs never collide. The environment
|
|
23
|
+
* is the teardown key — everything the deploy stamps carries the ownership
|
|
24
|
+
* marker `{ stack, env }`, and `destroy()` selects on that identity and
|
|
25
|
+
* nothing else. A project that declares `environments` must legalize the
|
|
26
|
+
* dynamic names with a pattern entry (#1221):
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* environments: ["dev", "prod", { name: "test-*", endpoint: "http://localhost:4566" }]
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ## Emulators
|
|
33
|
+
*
|
|
34
|
+
* The harness is emulator-aware the same way `--live` reads are (#1166): the
|
|
35
|
+
* target environment's declared `endpoint` is injected into each lexicon's
|
|
36
|
+
* ambient endpoint variable (`AWS_ENDPOINT_URL`, …) for the apply and the
|
|
37
|
+
* teardown — unless the variable is already set. Ambient always wins, so the
|
|
38
|
+
* env vars `chant emulator up --json` reports (#920) take precedence when
|
|
39
|
+
* exported, and a CI job pointing the identical suite at a real account just
|
|
40
|
+
* leaves both unset.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { basename, join, resolve } from "node:path";
|
|
44
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
45
|
+
import { tmpdir } from "node:os";
|
|
46
|
+
import { build } from "./build";
|
|
47
|
+
import type { Declarable } from "./declarable";
|
|
48
|
+
import type { SerializerResult } from "./serializer";
|
|
49
|
+
import {
|
|
50
|
+
loadChantConfigUpward,
|
|
51
|
+
resolveOwnershipStack,
|
|
52
|
+
type ChantConfig,
|
|
53
|
+
} from "./config";
|
|
54
|
+
import { resolveBuildParams, type BuildParamValue } from "./build-params";
|
|
55
|
+
import { ENV_VAR, unknownEnvError } from "./env";
|
|
56
|
+
import { applyLiveEndpoint } from "./live-endpoint";
|
|
57
|
+
import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "./cli/plugins";
|
|
58
|
+
import type { LexiconPlugin } from "./lexicon";
|
|
59
|
+
import type { OwnershipMarker } from "./ownership";
|
|
60
|
+
import { runOpLocally } from "./op/local-executor";
|
|
61
|
+
import { loadActivities, loadProfiles, type ActivityFn, type ActivityProfile } from "./op/activity-registry";
|
|
62
|
+
import type { OpConfig, ActivityStep } from "./op/types";
|
|
63
|
+
import { executeTeardown, type TeardownReport } from "./lifecycle/teardown";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Which `nativeApply` target deploys each lexicon's built output. Only these
|
|
67
|
+
* lexicons have a native apply mechanism; an output from any other lexicon
|
|
68
|
+
* (helm values, a CI pipeline file, …) is returned to the caller but not
|
|
69
|
+
* applied — deploying it is not what a live-stack test means by "deploy".
|
|
70
|
+
*/
|
|
71
|
+
const APPLY_TARGETS: Record<string, string> = {
|
|
72
|
+
aws: "cloudformation",
|
|
73
|
+
k8s: "kubectl",
|
|
74
|
+
azure: "arm",
|
|
75
|
+
gcp: "gcp",
|
|
76
|
+
fly: "fly",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export interface DeployStackOptions {
|
|
80
|
+
/** The chant project directory (where the infra source lives). */
|
|
81
|
+
dir: string;
|
|
82
|
+
/**
|
|
83
|
+
* Explicit environment name. Overrides the default `test-<suite>-<nonce>`
|
|
84
|
+
* derivation — for a shared long-lived test environment, or a test that
|
|
85
|
+
* needs a deterministic name. Must be legal for the project's declared
|
|
86
|
+
* `environments`, exactly as `--env` would be.
|
|
87
|
+
*/
|
|
88
|
+
env?: string;
|
|
89
|
+
/**
|
|
90
|
+
* Suite name folded into the default environment name. Defaults to the
|
|
91
|
+
* project directory's basename.
|
|
92
|
+
*/
|
|
93
|
+
suite?: string;
|
|
94
|
+
/** Build parameters, exactly as `--param name=value` flags would supply them. */
|
|
95
|
+
params?: Record<string, BuildParamValue>;
|
|
96
|
+
/**
|
|
97
|
+
* Loaded lexicon plugins. Defaults to loading the project's own declared/
|
|
98
|
+
* detected lexicons — pass this only to substitute test doubles.
|
|
99
|
+
*/
|
|
100
|
+
plugins?: LexiconPlugin[];
|
|
101
|
+
/** Activity implementations for the apply. Defaults to the real registry. */
|
|
102
|
+
activities?: Map<string, ActivityFn>;
|
|
103
|
+
/** Activity profiles (timeouts/retries). Defaults to the lexicon-declared table. */
|
|
104
|
+
profiles?: Record<string, ActivityProfile>;
|
|
105
|
+
/**
|
|
106
|
+
* Extra lexicon-name → `nativeApply` target entries, merged over the
|
|
107
|
+
* built-in map (aws → cloudformation, k8s → kubectl, azure → arm,
|
|
108
|
+
* gcp → gcp, fly → fly). A seam for tests and out-of-tree lexicons.
|
|
109
|
+
*/
|
|
110
|
+
applyTargets?: Record<string, string>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The handle a suite holds between `beforeAll` and `afterAll`. */
|
|
114
|
+
export interface DeployedStack {
|
|
115
|
+
/** The built outputs, keyed by lexicon — what was deployed. */
|
|
116
|
+
outputs: Map<string, string | SerializerResult>;
|
|
117
|
+
/** The discovered entities, keyed by name. */
|
|
118
|
+
entities: Map<string, Declarable>;
|
|
119
|
+
/** The environment this deploy targeted — the teardown key. */
|
|
120
|
+
env: string;
|
|
121
|
+
/**
|
|
122
|
+
* Tear down everything carrying this suite's marker `{ stack, env }`.
|
|
123
|
+
* Throws {@link TeardownIncompleteError} when any candidate failed to
|
|
124
|
+
* delete or the plan had holes — an environment that cannot be called
|
|
125
|
+
* clean is a test failure, never a silent leak. Safe to call again:
|
|
126
|
+
* teardown is stateless, and a second call over a clean environment
|
|
127
|
+
* plans nothing.
|
|
128
|
+
*/
|
|
129
|
+
destroy(): Promise<TeardownReport>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Thrown by {@link DeployedStack.destroy} when the environment cannot be
|
|
134
|
+
* called clean: candidates still `failed` after the retry pass, or the plan
|
|
135
|
+
* had holes (#1089 — parts of the estate could not be read, so "nothing
|
|
136
|
+
* left" would be a claim about what was readable, not about the environment).
|
|
137
|
+
* Carries the full report for diagnosis.
|
|
138
|
+
*/
|
|
139
|
+
export class TeardownIncompleteError extends Error {
|
|
140
|
+
constructor(public readonly report: TeardownReport) {
|
|
141
|
+
const failed = report.outcomes.filter((o) => o.outcome === "failed");
|
|
142
|
+
const parts: string[] = [];
|
|
143
|
+
if (failed.length > 0) {
|
|
144
|
+
parts.push(
|
|
145
|
+
`${failed.length} candidate(s) failed to delete: ` +
|
|
146
|
+
failed.map((o) => `${o.lexicon}/${o.name}${o.detail ? ` (${o.detail})` : ""}`).join(", "),
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (report.plan.holes.length > 0) {
|
|
150
|
+
parts.push(
|
|
151
|
+
`${report.plan.holes.length} hole(s) — resources chant may own but could not read: ` +
|
|
152
|
+
report.plan.holes.map((h) => `${h.lexicon}/${h.name} (${h.reason})`).join(", "),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
super(`teardown of "${report.environment}" is incomplete — ${parts.join("; ")}`);
|
|
156
|
+
this.name = "TeardownIncompleteError";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The default environment name for one suite run: `test-<suite>-<nonce>`.
|
|
162
|
+
* The `test-` prefix is what a project's `"test-*"` pattern entry (#1221)
|
|
163
|
+
* legalizes; the nonce keeps parallel CI jobs of the same suite apart.
|
|
164
|
+
*/
|
|
165
|
+
export function testEnvName(suite: string): string {
|
|
166
|
+
const slug =
|
|
167
|
+
suite
|
|
168
|
+
.toLowerCase()
|
|
169
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
170
|
+
.replace(/^-+|-+$/g, "")
|
|
171
|
+
.slice(0, 24)
|
|
172
|
+
.replace(/-+$/, "") || "suite";
|
|
173
|
+
const nonce = Math.random().toString(36).slice(2, 8).padEnd(6, "0");
|
|
174
|
+
return `test-${slug}-${nonce}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** JSON output starts with a brace/bracket; everything else is written as YAML. */
|
|
178
|
+
function outputExtension(content: string): string {
|
|
179
|
+
const head = content.trimStart();
|
|
180
|
+
return head.startsWith("{") || head.startsWith("[") ? ".json" : ".yaml";
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Write each lexicon's built output to a scratch directory and return the
|
|
185
|
+
* primary file path per lexicon. Secondary files (a multi-file serializer
|
|
186
|
+
* result) land beside the primary under their own names.
|
|
187
|
+
*/
|
|
188
|
+
function writeOutputs(outputs: Map<string, string | SerializerResult>, env: string): Map<string, string> {
|
|
189
|
+
const dir = mkdtempSync(join(tmpdir(), `chant-testing-${env}-`));
|
|
190
|
+
const paths = new Map<string, string>();
|
|
191
|
+
for (const [lexicon, output] of outputs) {
|
|
192
|
+
const primary = typeof output === "string" ? output : output.primary;
|
|
193
|
+
const path = join(dir, `${lexicon}${outputExtension(primary)}`);
|
|
194
|
+
writeFileSync(path, primary);
|
|
195
|
+
if (typeof output !== "string" && output.files) {
|
|
196
|
+
for (const [name, content] of Object.entries(output.files)) {
|
|
197
|
+
writeFileSync(join(dir, basename(name)), content);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
paths.set(lexicon, path);
|
|
201
|
+
}
|
|
202
|
+
return paths;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Deploy a live instance of the project in `dir` for one test suite.
|
|
207
|
+
*
|
|
208
|
+
* Build (the CLI's own config resolution: `buildParams`, ownership marker,
|
|
209
|
+
* build roots) + additive apply (one `nativeApply` per built output with a
|
|
210
|
+
* native mechanism, via the local Op executor). Returns the handle the suite
|
|
211
|
+
* holds: outputs, entities, the environment name, and `destroy`.
|
|
212
|
+
*
|
|
213
|
+
* The ownership marker stamped on every resource is `{ stack: ownership.stack,
|
|
214
|
+
* env: <this deploy's environment> }` — the marker env always follows the
|
|
215
|
+
* suite environment, whatever `ownership.env` in config says, because the
|
|
216
|
+
* marker is the only thing `destroy()` selects on. A project with no
|
|
217
|
+
* `ownership.stack` is refused up front: a deploy that stamps nothing is a
|
|
218
|
+
* deploy nothing can sweep.
|
|
219
|
+
*
|
|
220
|
+
* Failures are thrown, never returned: build errors, unresolved parameters,
|
|
221
|
+
* an illegal environment name, and a failed apply (the local executor's
|
|
222
|
+
* `OpRunFailure`) all reject the returned promise.
|
|
223
|
+
*/
|
|
224
|
+
export async function deployStack(options: DeployStackOptions): Promise<DeployedStack> {
|
|
225
|
+
const dir = resolve(options.dir);
|
|
226
|
+
const { config, configPath } = await loadChantConfigUpward(dir);
|
|
227
|
+
|
|
228
|
+
const env = options.env ?? testEnvName(options.suite ?? basename(dir));
|
|
229
|
+
const envError = unknownEnvError(env, config.environments);
|
|
230
|
+
if (envError) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`${envError} The harness derives per-run environment names — declare a "test-*" pattern entry ` +
|
|
233
|
+
`in chant.config.ts's environments to legalize them (#1221).`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const stack = resolveOwnershipStack(config);
|
|
238
|
+
if (stack === undefined) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`${dir}: this project declares no ownership.stack — the harness's destroy() is marker-scoped ` +
|
|
241
|
+
`and would have nothing to select on. Set ownership: { stack: "<name>" } in chant.config.ts.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const marker: OwnershipMarker = { stack, env };
|
|
245
|
+
|
|
246
|
+
const plugins = options.plugins ?? (await loadPlugins(await resolveProjectLexicons(dir)));
|
|
247
|
+
|
|
248
|
+
// Build parameters, resolved as `chant build --param ...` resolves them.
|
|
249
|
+
// A project whose config maps `env` to a build parameter gets this deploy's
|
|
250
|
+
// environment automatically, unless the caller passed one explicitly.
|
|
251
|
+
const cli: Record<string, string> = {};
|
|
252
|
+
for (const [name, value] of Object.entries(options.params ?? {})) cli[name] = String(value);
|
|
253
|
+
if (config.buildParams && "env" in config.buildParams && !("env" in cli)) cli.env = env;
|
|
254
|
+
const resolution = resolveBuildParams(config.buildParams, { cli, env: process.env });
|
|
255
|
+
if (resolution.errors.length > 0) {
|
|
256
|
+
throw new Error(`${dir}: build parameters did not resolve —\n ${resolution.errors.join("\n ")}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const projectRoot = configPath ? resolve(configPath, "..") : dir;
|
|
260
|
+
const configRecord = config as unknown as Record<string, unknown>;
|
|
261
|
+
|
|
262
|
+
// Env-aware source branches on env() (#505) during discovery — scope the
|
|
263
|
+
// variable to the build and restore whatever the shell had.
|
|
264
|
+
const priorEnv = process.env[ENV_VAR];
|
|
265
|
+
let result;
|
|
266
|
+
try {
|
|
267
|
+
process.env[ENV_VAR] = env;
|
|
268
|
+
result = await build(dir, plugins.map((p) => p.serializer), undefined, {
|
|
269
|
+
ownership: marker,
|
|
270
|
+
config: configRecord,
|
|
271
|
+
lexicons: plugins.map((p) => p.name),
|
|
272
|
+
intrinsics: plugins.flatMap((p) => p.intrinsics?.() ?? []),
|
|
273
|
+
buildParams: resolution.provenance,
|
|
274
|
+
buildRoots: collectBuildRootContributors(plugins, configRecord, projectRoot),
|
|
275
|
+
});
|
|
276
|
+
} finally {
|
|
277
|
+
if (priorEnv === undefined) delete process.env[ENV_VAR];
|
|
278
|
+
else process.env[ENV_VAR] = priorEnv;
|
|
279
|
+
}
|
|
280
|
+
if (result.errors.length > 0) {
|
|
281
|
+
const messages = result.errors.map((e) => e.message);
|
|
282
|
+
throw new Error(`${dir}: build failed —\n ${messages.join("\n ")}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// One additive apply step per built output with a native mechanism.
|
|
286
|
+
const targets = { ...APPLY_TARGETS, ...options.applyTargets };
|
|
287
|
+
const paths = writeOutputs(result.outputs, env);
|
|
288
|
+
const steps: ActivityStep[] = [];
|
|
289
|
+
for (const [lexicon, path] of paths) {
|
|
290
|
+
const target = targets[lexicon];
|
|
291
|
+
if (!target) continue;
|
|
292
|
+
steps.push({
|
|
293
|
+
kind: "activity",
|
|
294
|
+
fn: "nativeApply",
|
|
295
|
+
args: { target, env, output: path, deleteMode: "never" },
|
|
296
|
+
profile: "longInfra",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (steps.length === 0) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`${dir}: nothing to deploy — no built output has a native apply target ` +
|
|
302
|
+
`(built: ${[...result.outputs.keys()].join(", ") || "none"}; targets exist for: ${Object.keys(targets).join(", ")}).`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const op: OpConfig = {
|
|
306
|
+
name: `deploy-${env}`,
|
|
307
|
+
overview: `test harness deploy of ${dir} into ${env}`,
|
|
308
|
+
phases: [{ name: "Deploy", steps }],
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const activities = options.activities ?? (await loadActivities(plugins.map((p) => p.name)));
|
|
312
|
+
const profiles = options.profiles ?? (await loadProfiles());
|
|
313
|
+
|
|
314
|
+
// The environment's declared endpoint (#1166) applies to the deploy the way
|
|
315
|
+
// it applies to a --live read — and ambient always wins, so exported
|
|
316
|
+
// `chant emulator up --json` vars take precedence.
|
|
317
|
+
const applied = applyLiveEndpoint(config.environments, env, plugins);
|
|
318
|
+
try {
|
|
319
|
+
await runOpLocally(op, activities, profiles);
|
|
320
|
+
} finally {
|
|
321
|
+
applied.restore();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const destroy = async (): Promise<TeardownReport> => {
|
|
325
|
+
const endpointForTeardown = applyLiveEndpoint(config.environments, env, plugins);
|
|
326
|
+
let report: TeardownReport;
|
|
327
|
+
try {
|
|
328
|
+
report = await executeTeardown({ environment: env, stack, plugins });
|
|
329
|
+
} finally {
|
|
330
|
+
endpointForTeardown.restore();
|
|
331
|
+
}
|
|
332
|
+
const failed = report.outcomes.some((o) => o.outcome === "failed");
|
|
333
|
+
if (failed || report.plan.holes.length > 0) throw new TeardownIncompleteError(report);
|
|
334
|
+
return report;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
return { outputs: result.outputs, entities: result.entities, env, destroy };
|
|
338
|
+
}
|