@intentius/chant 0.53.0 → 0.54.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/audit/catalog.d.ts +4 -0
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/nginx.d.ts +53 -0
- package/dist/audit/nginx.d.ts.map +1 -0
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/carve-apply.d.ts.map +1 -1
- package/dist/cli/commands/carve-status.d.ts +56 -0
- package/dist/cli/commands/carve-status.d.ts.map +1 -0
- package/dist/cli/handlers/carve-status.d.ts +9 -0
- package/dist/cli/handlers/carve-status.d.ts.map +1 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/op-progress.d.ts +37 -4
- package/dist/cli/handlers/op-progress.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +61 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +9 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +3 -1
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +2 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +7 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon.d.ts +8 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +81 -1
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +43 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +55 -1
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +15 -1
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/terraform/manifest.d.ts +31 -6
- package/dist/terraform/manifest.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.ts +35 -0
- package/src/audit/edge-init-safety.test.ts +36 -3
- package/src/audit/nginx.test.ts +180 -0
- package/src/audit/nginx.ts +379 -0
- package/src/audit/rules-doc.ts +1 -0
- package/src/cli/commands/audit.ts +8 -3
- package/src/cli/commands/carve-apply.test.ts +4 -1
- package/src/cli/commands/carve-apply.ts +4 -2
- package/src/cli/commands/carve-bridge.test.ts +2 -1
- package/src/cli/commands/carve-emit-state.test.ts +3 -1
- package/src/cli/commands/carve-status.test.ts +116 -0
- package/src/cli/commands/carve-status.ts +163 -0
- package/src/cli/handlers/carve-status.ts +30 -0
- package/src/cli/handlers/components.ts +5 -1
- package/src/cli/handlers/graph.ts +3 -0
- package/src/cli/handlers/op-progress.test.ts +67 -1
- package/src/cli/handlers/op-progress.ts +70 -14
- package/src/cli/handlers/operator.test.ts +302 -1
- package/src/cli/handlers/operator.ts +193 -5
- package/src/cli/handlers/run.ts +15 -14
- package/src/cli/main.test.ts +19 -0
- package/src/cli/main.ts +34 -5
- package/src/cli/registry.ts +9 -1
- package/src/components/auto-release.ts +4 -1
- package/src/components/cli-support.ts +4 -2
- package/src/graph-ir.ts +7 -0
- package/src/lexicon.ts +8 -0
- package/src/lifecycle/converge-ledger.test.ts +110 -0
- package/src/lifecycle/converge-ledger.ts +110 -2
- package/src/lifecycle/gate-ledger.test.ts +81 -1
- package/src/lifecycle/gate-ledger.ts +64 -1
- package/src/lifecycle/release-ledger.test.ts +64 -0
- package/src/lifecycle/release-ledger.ts +79 -1
- package/src/op/activity-contract.ts +14 -0
- package/src/terraform/manifest.test.ts +52 -0
- package/src/terraform/manifest.ts +72 -10
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
InvalidReleaseRecordError,
|
|
12
12
|
latestPerComponent,
|
|
13
13
|
recordsForDigest,
|
|
14
|
+
resolveRunId,
|
|
14
15
|
type ReleaseRecord,
|
|
15
16
|
type ReleaseRecordInput,
|
|
16
17
|
} from "./release-ledger";
|
|
@@ -234,4 +235,67 @@ describe("release-ledger", () => {
|
|
|
234
235
|
expect(found.every((r) => r.digest === "sha256:shared")).toBe(true);
|
|
235
236
|
});
|
|
236
237
|
});
|
|
238
|
+
|
|
239
|
+
describe("resolveRunId (#2045)", () => {
|
|
240
|
+
test("GitHub Actions env resolves id + origin together, with repo and run url", () => {
|
|
241
|
+
const env = {
|
|
242
|
+
GITHUB_RUN_ID: "18234771902",
|
|
243
|
+
GITHUB_REPOSITORY: "INTENTIUS/chant",
|
|
244
|
+
GITHUB_SERVER_URL: "https://github.com",
|
|
245
|
+
};
|
|
246
|
+
expect(resolveRunId(undefined, env)).toEqual({
|
|
247
|
+
runId: "18234771902",
|
|
248
|
+
runOrigin: {
|
|
249
|
+
forge: "github",
|
|
250
|
+
repo: "INTENTIUS/chant",
|
|
251
|
+
url: "https://github.com/INTENTIUS/chant/actions/runs/18234771902",
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("a Forgejo instance resolves through the same env contract to its own host", () => {
|
|
257
|
+
const env = {
|
|
258
|
+
GITHUB_RUN_ID: "42",
|
|
259
|
+
GITHUB_REPOSITORY: "intentius/loomster",
|
|
260
|
+
GITHUB_SERVER_URL: "https://forge.example.dev",
|
|
261
|
+
};
|
|
262
|
+
expect(resolveRunId(undefined, env).runOrigin!.url).toBe("https://forge.example.dev/intentius/loomster/actions/runs/42");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("GitLab CI env records the project path and takes CI_PIPELINE_URL verbatim", () => {
|
|
266
|
+
const env = {
|
|
267
|
+
CI_PIPELINE_ID: "991",
|
|
268
|
+
CI_PROJECT_PATH: "group/app",
|
|
269
|
+
CI_PIPELINE_URL: "https://gitlab.com/group/app/-/pipelines/991",
|
|
270
|
+
};
|
|
271
|
+
expect(resolveRunId(undefined, env)).toEqual({
|
|
272
|
+
runId: "991",
|
|
273
|
+
runOrigin: { forge: "gitlab", repo: "group/app", url: "https://gitlab.com/group/app/-/pipelines/991" },
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("outside CI the mint says local in a field, not only in the id's spelling", () => {
|
|
278
|
+
const { runId, runOrigin } = resolveRunId(undefined, {});
|
|
279
|
+
expect(runId).toMatch(/^local-\d+$/);
|
|
280
|
+
expect(runOrigin).toEqual({ forge: "local" });
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("an explicit id gets an origin only when it matches what the environment advertises", () => {
|
|
284
|
+
const env = { GITHUB_RUN_ID: "42", GITHUB_REPOSITORY: "o/r", GITHUB_SERVER_URL: "https://github.com" };
|
|
285
|
+
expect(resolveRunId("42", env).runOrigin?.forge).toBe("github");
|
|
286
|
+
expect(resolveRunId("something-else", env).runOrigin).toBeUndefined();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("a record carrying runOrigin round-trips through the ledger", async () => {
|
|
290
|
+
await withTestDir(async (dir) => {
|
|
291
|
+
await initRepo(dir);
|
|
292
|
+
const input = makeInput({
|
|
293
|
+
runOrigin: { forge: "github", repo: "o/r", url: "https://github.com/o/r/actions/runs/1" },
|
|
294
|
+
});
|
|
295
|
+
await appendReleaseRecord(input, { cwd: dir });
|
|
296
|
+
const { records } = await readReleaseLedger("prod", { cwd: dir });
|
|
297
|
+
expect(records[0].runOrigin).toEqual(input.runOrigin);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
237
301
|
});
|
|
@@ -31,6 +31,25 @@
|
|
|
31
31
|
import { sortedJsonReplacer } from "../utils";
|
|
32
32
|
import { appendReleaseRecordLine, readReleaseLedgerLines, listLedgerEnvironments as gitListLedgerEnvironments } from "./git";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The id space a `ReleaseRecord.runId` lives in, and how to reach the run
|
|
36
|
+
* (#2045).
|
|
37
|
+
*/
|
|
38
|
+
export interface RunOrigin {
|
|
39
|
+
/**
|
|
40
|
+
* Which id space minted `runId`: `github` (a GitHub Actions run id — also
|
|
41
|
+
* the spelling Forgejo Actions uses, since it implements the same env
|
|
42
|
+
* contract; `url` disambiguates the actual host), `gitlab` (a CI pipeline
|
|
43
|
+
* id), `op` (an orchestrator/Op run id), or `local` (minted on the machine
|
|
44
|
+
* that ran the deploy, resolvable nowhere).
|
|
45
|
+
*/
|
|
46
|
+
forge: "github" | "gitlab" | "op" | "local";
|
|
47
|
+
/** Repo/project slug on the forge (`GITHUB_REPOSITORY` / `CI_PROJECT_PATH`), when the environment named one. */
|
|
48
|
+
repo?: string;
|
|
49
|
+
/** Resolved link to the run, when the environment provided enough to build one. */
|
|
50
|
+
url?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
/**
|
|
35
54
|
* One immutable deploy record: `(component, env, artifact digest, git sha,
|
|
36
55
|
* run id, timestamp, actor)`, referencing the build archive by digest — the
|
|
@@ -54,8 +73,19 @@ export interface ReleaseRecord {
|
|
|
54
73
|
digest: string;
|
|
55
74
|
/** Git commit SHA the deploy was built from. */
|
|
56
75
|
gitSha: string;
|
|
57
|
-
/** Orchestrator run identifier (a `DriverRunResult`/Op run id, or a CI run id in generate mode) — the pointer back into the deploy event log. */
|
|
76
|
+
/** Orchestrator run identifier (a `DriverRunResult`/Op run id, or a CI run id in generate mode) — the pointer back into the deploy event log. Which id space it lives in, and how to follow it, is `runOrigin`'s job (#2045). */
|
|
58
77
|
runId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Where `runId` can be resolved (#2045). A bare `runId` names a run in one
|
|
80
|
+
* of several mutually incompatible id spaces — a GitHub Actions run id, a
|
|
81
|
+
* GitLab pipeline id, an Op run id, a locally minted id — and without this
|
|
82
|
+
* a reader can tell them apart only by inference from whoever wrote the
|
|
83
|
+
* record. Written at record time from the same environment the id came
|
|
84
|
+
* from, so the two cannot skew. Optional: records written before this
|
|
85
|
+
* field existed, and callers passing an id whose origin they did not
|
|
86
|
+
* declare, simply have none.
|
|
87
|
+
*/
|
|
88
|
+
runOrigin?: RunOrigin;
|
|
59
89
|
/**
|
|
60
90
|
* When this record was written. The caller supplies this — the session
|
|
61
91
|
* cannot call `Date.now()` reliably in every context, so the CLI resolves
|
|
@@ -132,6 +162,54 @@ export function validateReleaseRecord(record: Partial<ReleaseRecord>): string[]
|
|
|
132
162
|
/** Input a caller supplies to record one deploy — `version` is filled in, everything else is required (see `ReleaseRecord`). */
|
|
133
163
|
export type ReleaseRecordInput = Omit<ReleaseRecord, "version">;
|
|
134
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Resolve a run id and its origin from the same environment, in one step, so
|
|
167
|
+
* the two cannot skew (#2045). Resolution order mirrors what
|
|
168
|
+
* `chant components release` always did — explicit id, then
|
|
169
|
+
* `GITHUB_RUN_ID`, then `CI_PIPELINE_ID`, then a locally minted
|
|
170
|
+
* `local-<ms>` — but now each source also names its id space:
|
|
171
|
+
*
|
|
172
|
+
* - the GitHub path records `repo` (`GITHUB_REPOSITORY`) and builds the run
|
|
173
|
+
* `url` from `GITHUB_SERVER_URL` — which also makes a Forgejo instance's
|
|
174
|
+
* runs resolvable, since Forgejo Actions implements the same env contract;
|
|
175
|
+
* - the GitLab path records `repo` (`CI_PROJECT_PATH`) and takes
|
|
176
|
+
* `CI_PIPELINE_URL` verbatim;
|
|
177
|
+
* - the local mint says `local` in a field instead of only in the id's
|
|
178
|
+
* spelling.
|
|
179
|
+
*
|
|
180
|
+
* An explicit id still gets an origin when it matches the id the environment
|
|
181
|
+
* itself is advertising (a CI job passing `--run-id $GITHUB_RUN_ID`);
|
|
182
|
+
* otherwise the caller's id is recorded without one — guessing an origin the
|
|
183
|
+
* caller did not state would be exactly the inference this field exists to
|
|
184
|
+
* remove.
|
|
185
|
+
*/
|
|
186
|
+
export function resolveRunId(
|
|
187
|
+
explicit?: string,
|
|
188
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
189
|
+
): { runId: string; runOrigin?: RunOrigin } {
|
|
190
|
+
const githubOrigin = (id: string): RunOrigin => ({
|
|
191
|
+
forge: "github",
|
|
192
|
+
...(env.GITHUB_REPOSITORY ? { repo: env.GITHUB_REPOSITORY } : {}),
|
|
193
|
+
...(env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY
|
|
194
|
+
? { url: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${id}` }
|
|
195
|
+
: {}),
|
|
196
|
+
});
|
|
197
|
+
const gitlabOrigin = (): RunOrigin => ({
|
|
198
|
+
forge: "gitlab",
|
|
199
|
+
...(env.CI_PROJECT_PATH ? { repo: env.CI_PROJECT_PATH } : {}),
|
|
200
|
+
...(env.CI_PIPELINE_URL ? { url: env.CI_PIPELINE_URL } : {}),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (explicit) {
|
|
204
|
+
if (env.GITHUB_RUN_ID && explicit === env.GITHUB_RUN_ID) return { runId: explicit, runOrigin: githubOrigin(explicit) };
|
|
205
|
+
if (env.CI_PIPELINE_ID && explicit === env.CI_PIPELINE_ID) return { runId: explicit, runOrigin: gitlabOrigin() };
|
|
206
|
+
return { runId: explicit };
|
|
207
|
+
}
|
|
208
|
+
if (env.GITHUB_RUN_ID) return { runId: env.GITHUB_RUN_ID, runOrigin: githubOrigin(env.GITHUB_RUN_ID) };
|
|
209
|
+
if (env.CI_PIPELINE_ID) return { runId: env.CI_PIPELINE_ID, runOrigin: gitlabOrigin() };
|
|
210
|
+
return { runId: `local-${Date.now()}`, runOrigin: { forge: "local" } };
|
|
211
|
+
}
|
|
212
|
+
|
|
135
213
|
/**
|
|
136
214
|
* Append one immutable release record to the `<env>` ledger on the
|
|
137
215
|
* `chant/lifecycle` orphan branch. Throws `InvalidReleaseRecordError` before
|
|
@@ -75,6 +75,18 @@ export interface ActivityContract<Args = unknown, Return = unknown> {
|
|
|
75
75
|
* per #1290 — a later step's reference into this one's output.
|
|
76
76
|
*/
|
|
77
77
|
returns?: z.ZodType<Return>;
|
|
78
|
+
/**
|
|
79
|
+
* Names of top-level args whose values identify what the activity's effect
|
|
80
|
+
* touches in the estate (#2022) — an entity id, a resource address, a
|
|
81
|
+
* target endpoint — as opposed to scope (`env`, a stack name) or mechanics
|
|
82
|
+
* (a timeout, a mode). op.json's IR resolves these per step into
|
|
83
|
+
* `OpIRActivityStep.entities`, so a renderer can join a step to the estate
|
|
84
|
+
* nodes it touches instead of inventing joins off scope fields. Optional
|
|
85
|
+
* and partial by design, like the contract itself: an activity whose
|
|
86
|
+
* touched set is not spelled in its args (e.g. an applier that derives
|
|
87
|
+
* stack members internally) simply declares none.
|
|
88
|
+
*/
|
|
89
|
+
entities?: string[];
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/** Declare an activity contract. */
|
|
@@ -82,12 +94,14 @@ export function activityContract<ArgsSchema extends z.ZodTypeAny, ReturnSchema e
|
|
|
82
94
|
name: string,
|
|
83
95
|
args: ArgsSchema,
|
|
84
96
|
returns?: ReturnSchema,
|
|
97
|
+
opts?: { entities?: string[] },
|
|
85
98
|
): ActivityContract<z.infer<ArgsSchema>, ReturnSchema extends z.ZodTypeAny ? z.infer<ReturnSchema> : unknown> {
|
|
86
99
|
return {
|
|
87
100
|
[CONTRACT_BRAND]: true,
|
|
88
101
|
name,
|
|
89
102
|
args,
|
|
90
103
|
...(returns ? { returns } : {}),
|
|
104
|
+
...(opts?.entities && opts.entities.length > 0 ? { entities: opts.entities } : {}),
|
|
91
105
|
} as ActivityContract<z.infer<ArgsSchema>, ReturnSchema extends z.ZodTypeAny ? z.infer<ReturnSchema> : unknown>;
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -3,10 +3,12 @@ import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import {
|
|
6
|
+
CARVE_MANIFEST_VERSION,
|
|
6
7
|
carveManifestPath,
|
|
7
8
|
listCarveManifests,
|
|
8
9
|
readCarveManifest,
|
|
9
10
|
resolveCarveManifest,
|
|
11
|
+
resolveManifestFilePath,
|
|
10
12
|
updateCarveManifest,
|
|
11
13
|
writeCarveManifest,
|
|
12
14
|
type CarveManifest,
|
|
@@ -89,6 +91,56 @@ describe("carve manifest", () => {
|
|
|
89
91
|
});
|
|
90
92
|
});
|
|
91
93
|
|
|
94
|
+
test("records file paths relative to its own directory, so the tree can move (#2039)", () => {
|
|
95
|
+
withDir((dir) => {
|
|
96
|
+
const m = manifest("aws_s3_bucket.assets");
|
|
97
|
+
m.emit = { source: "tfstate", files: [join(dir, "src", "assets.ts")], at: "t" };
|
|
98
|
+
m.bridge = {
|
|
99
|
+
written: [join(dir, "aws_s3_bucket-assets-runbook.md"), join(dir, "..", "api.tf")],
|
|
100
|
+
appliedInPlace: false,
|
|
101
|
+
patch: join(dir, "aws_s3_bucket-assets-bridge.patch"),
|
|
102
|
+
at: "t",
|
|
103
|
+
};
|
|
104
|
+
const path = writeCarveManifest(dir, m);
|
|
105
|
+
const read = readCarveManifest(path)!;
|
|
106
|
+
expect(read.version).toBe(CARVE_MANIFEST_VERSION);
|
|
107
|
+
expect(read.emit!.files).toEqual([join("src", "assets.ts")]);
|
|
108
|
+
expect(read.bridge!.written).toEqual(["aws_s3_bucket-assets-runbook.md", join("..", "api.tf")]);
|
|
109
|
+
expect(read.bridge!.patch).toBe("aws_s3_bucket-assets-bridge.patch");
|
|
110
|
+
// `from`/`statePath` locate the estate, a tree the manifest does not
|
|
111
|
+
// travel with — they stay absolute.
|
|
112
|
+
expect(read.from).toBe("/estate");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("resolveManifestFilePath joins relative entries and passes absolute (v1) entries through", () => {
|
|
117
|
+
expect(resolveManifestFilePath("/moved/carveout", join("src", "assets.ts"))).toBe(join("/moved/carveout", "src", "assets.ts"));
|
|
118
|
+
expect(resolveManifestFilePath("/moved/carveout", "/original/carveout/src/assets.ts")).toBe("/original/carveout/src/assets.ts");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("reads a version-1 manifest (absolute paths) and migrates it on update", () => {
|
|
122
|
+
withDir((dir) => {
|
|
123
|
+
const path = carveManifestPath(dir, "aws_s3_bucket.assets");
|
|
124
|
+
const v1: CarveManifest = {
|
|
125
|
+
...manifest("aws_s3_bucket.assets"),
|
|
126
|
+
version: 1,
|
|
127
|
+
emit: { source: "tfstate", files: [join(dir, "src", "assets.ts")], at: "t" },
|
|
128
|
+
};
|
|
129
|
+
writeFileSync(path, JSON.stringify(v1, null, 2) + "\n");
|
|
130
|
+
// Readable as-is, absolute paths intact.
|
|
131
|
+
expect(readCarveManifest(path)!.emit!.files).toEqual([join(dir, "src", "assets.ts")]);
|
|
132
|
+
|
|
133
|
+
// Any update rewrites the whole manifest to the version-2 contract.
|
|
134
|
+
updateCarveManifest(dir, "aws_s3_bucket.assets", {
|
|
135
|
+
bridge: { written: [join(dir, "runbook.md")], appliedInPlace: false, at: "t" },
|
|
136
|
+
});
|
|
137
|
+
const migrated = readCarveManifest(path)!;
|
|
138
|
+
expect(migrated.version).toBe(CARVE_MANIFEST_VERSION);
|
|
139
|
+
expect(migrated.emit!.files).toEqual([join("src", "assets.ts")]);
|
|
140
|
+
expect(migrated.bridge!.written).toEqual(["runbook.md"]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
92
144
|
test("update patches an existing manifest and no-ops on a missing one", () => {
|
|
93
145
|
withDir((dir) => {
|
|
94
146
|
expect(updateCarveManifest(dir, "aws_s3_bucket.assets", { bridge: { written: [], appliedInPlace: false, at: "t" } })).toBeUndefined();
|
|
@@ -9,14 +9,23 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
12
|
-
import { join } from "path";
|
|
12
|
+
import { isAbsolute, join, relative } from "path";
|
|
13
13
|
import type { CarveReport } from "./carve";
|
|
14
14
|
import type { OwnershipMarker } from "../ownership";
|
|
15
15
|
|
|
16
16
|
export const CARVE_MANIFEST_SUFFIX = ".carve.json";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Version 2 (#2039): `emit.files`, `bridge.written`, `bridge.patch` and
|
|
20
|
+
* `apply.stampedFiles` are recorded relative to the manifest's own directory,
|
|
21
|
+
* so the manifest stays true when the tree is copied, moved, or checked out
|
|
22
|
+
* elsewhere. Version-1 manifests recorded them absolute; readers resolve
|
|
23
|
+
* either through `resolveManifestFilePath`.
|
|
24
|
+
*/
|
|
25
|
+
export const CARVE_MANIFEST_VERSION = 2;
|
|
26
|
+
|
|
18
27
|
export interface CarveManifest {
|
|
19
|
-
version: 1;
|
|
28
|
+
version: 1 | 2;
|
|
20
29
|
/** Terraform address of the carved resource, e.g. `aws_s3_bucket.assets`. */
|
|
21
30
|
target: string;
|
|
22
31
|
/** Terraform resource type, when known. */
|
|
@@ -30,7 +39,13 @@ export interface CarveManifest {
|
|
|
30
39
|
/** Recorded by `carve emit`. */
|
|
31
40
|
emit?: {
|
|
32
41
|
source: "tfstate" | "live";
|
|
33
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Emitted chant source file path(s), relative to the manifest's directory
|
|
44
|
+
* (absolute in version-1 manifests). The manifest sits at the scaffolded
|
|
45
|
+
* project's root, so these are also project-root-relative — the same base
|
|
46
|
+
* `chant graph --format ir` uses for `sourceLoc.file`, which is the join
|
|
47
|
+
* from a carved Terraform address to the chant entity it became (#2040).
|
|
48
|
+
*/
|
|
34
49
|
files: string[];
|
|
35
50
|
/**
|
|
36
51
|
* Deferred outbound inputs declared as build parameters in the emitted
|
|
@@ -53,9 +68,10 @@ export interface CarveManifest {
|
|
|
53
68
|
};
|
|
54
69
|
/** Recorded by `carve bridge`. */
|
|
55
70
|
bridge?: {
|
|
71
|
+
/** Files written, relative to the manifest's directory (absolute in version-1 manifests). */
|
|
56
72
|
written: string[];
|
|
57
73
|
appliedInPlace: boolean;
|
|
58
|
-
/** The git-applyable `.patch` carrying the whole survivor edit. */
|
|
74
|
+
/** The git-applyable `.patch` carrying the whole survivor edit, relative to the manifest's directory (absolute in version-1 manifests). */
|
|
59
75
|
patch?: string;
|
|
60
76
|
/** Carved addresses whose own `.tf` block the rewrites remove (#998). */
|
|
61
77
|
excised?: string[];
|
|
@@ -65,7 +81,7 @@ export interface CarveManifest {
|
|
|
65
81
|
apply?: {
|
|
66
82
|
marker: OwnershipMarker;
|
|
67
83
|
ownershipTags: Record<string, string>;
|
|
68
|
-
/** Emitted files the marker was stamped into (`--write-source`). */
|
|
84
|
+
/** Emitted files the marker was stamped into (`--write-source`), relative to the manifest's directory (absolute in version-1 manifests). */
|
|
69
85
|
stampedFiles?: string[];
|
|
70
86
|
at: string;
|
|
71
87
|
};
|
|
@@ -81,20 +97,62 @@ export function carveManifestPath(outDir: string, target: string): string {
|
|
|
81
97
|
return join(outDir, `${carveSlug(target)}${CARVE_MANIFEST_SUFFIX}`);
|
|
82
98
|
}
|
|
83
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Relative-to-`outDir` spelling of a recorded file path. Absolute entries
|
|
102
|
+
* (the run-time spelling every step naturally produces, and every version-1
|
|
103
|
+
* manifest on disk) are rewritten; already-relative entries pass through.
|
|
104
|
+
*/
|
|
105
|
+
function relativizePath(outDir: string, p: string): string {
|
|
106
|
+
return isAbsolute(p) ? relative(outDir, p) : p;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Normalize a manifest to the version-2 on-disk contract: recorded file
|
|
111
|
+
* paths relative to the manifest's own directory (#2039). `from` and
|
|
112
|
+
* `statePath` stay absolute — they locate the Terraform estate, a different
|
|
113
|
+
* tree the manifest does not travel with.
|
|
114
|
+
*/
|
|
115
|
+
function normalizeManifest(outDir: string, manifest: CarveManifest): CarveManifest {
|
|
116
|
+
const out: CarveManifest = { ...manifest, version: CARVE_MANIFEST_VERSION };
|
|
117
|
+
if (manifest.emit) {
|
|
118
|
+
out.emit = { ...manifest.emit, files: manifest.emit.files.map((f) => relativizePath(outDir, f)) };
|
|
119
|
+
}
|
|
120
|
+
if (manifest.bridge) {
|
|
121
|
+
out.bridge = {
|
|
122
|
+
...manifest.bridge,
|
|
123
|
+
written: manifest.bridge.written.map((f) => relativizePath(outDir, f)),
|
|
124
|
+
...(manifest.bridge.patch !== undefined ? { patch: relativizePath(outDir, manifest.bridge.patch) } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (manifest.apply?.stampedFiles) {
|
|
128
|
+
out.apply = { ...manifest.apply, stampedFiles: manifest.apply.stampedFiles.map((f) => relativizePath(outDir, f)) };
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a manifest-recorded file path against the manifest's directory.
|
|
135
|
+
* Absolute paths (version-1 manifests) pass through untouched, so both
|
|
136
|
+
* on-disk spellings read the same.
|
|
137
|
+
*/
|
|
138
|
+
export function resolveManifestFilePath(manifestDir: string, p: string): string {
|
|
139
|
+
return isAbsolute(p) ? p : join(manifestDir, p);
|
|
140
|
+
}
|
|
141
|
+
|
|
84
142
|
/** Write (or overwrite) a manifest into the output dir. Returns the path. */
|
|
85
143
|
export function writeCarveManifest(outDir: string, manifest: CarveManifest): string {
|
|
86
144
|
mkdirSync(outDir, { recursive: true });
|
|
87
145
|
const path = carveManifestPath(outDir, manifest.target);
|
|
88
|
-
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
|
|
146
|
+
writeFileSync(path, JSON.stringify(normalizeManifest(outDir, manifest), null, 2) + "\n");
|
|
89
147
|
return path;
|
|
90
148
|
}
|
|
91
149
|
|
|
92
|
-
/** Read a manifest, or null when absent/unreadable/not a
|
|
150
|
+
/** Read a manifest, or null when absent/unreadable/not a known-version manifest. */
|
|
93
151
|
export function readCarveManifest(path: string): CarveManifest | null {
|
|
94
152
|
if (!existsSync(path)) return null;
|
|
95
153
|
try {
|
|
96
154
|
const parsed = JSON.parse(readFileSync(path, "utf-8")) as CarveManifest;
|
|
97
|
-
if (parsed?.version !== 1) return null;
|
|
155
|
+
if (parsed?.version !== 1 && parsed?.version !== 2) return null;
|
|
98
156
|
if (typeof parsed.target !== "string" || typeof parsed.boundary !== "object") return null;
|
|
99
157
|
return parsed;
|
|
100
158
|
} catch {
|
|
@@ -147,7 +205,11 @@ export function resolveCarveManifest(outDir: string, select?: string): ResolvedM
|
|
|
147
205
|
return { manifest: manifests[0].manifest, path: manifests[0].path };
|
|
148
206
|
}
|
|
149
207
|
|
|
150
|
-
/**
|
|
208
|
+
/**
|
|
209
|
+
* Read-modify-write a section of an existing manifest. No-op when absent.
|
|
210
|
+
* The whole manifest is re-normalized on write, so updating a version-1
|
|
211
|
+
* manifest also migrates its recorded paths to the relative contract.
|
|
212
|
+
*/
|
|
151
213
|
export function updateCarveManifest(
|
|
152
214
|
outDir: string,
|
|
153
215
|
target: string,
|
|
@@ -157,6 +219,6 @@ export function updateCarveManifest(
|
|
|
157
219
|
const manifest = readCarveManifest(path);
|
|
158
220
|
if (!manifest) return undefined;
|
|
159
221
|
Object.assign(manifest, patch);
|
|
160
|
-
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
|
|
222
|
+
writeFileSync(path, JSON.stringify(normalizeManifest(outDir, manifest), null, 2) + "\n");
|
|
161
223
|
return path;
|
|
162
224
|
}
|