@tahminator/pipeline 1.0.90-beta.gd62102f7 → 1.0.91

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 CHANGED
@@ -312,24 +312,22 @@ const flux = new FluxClient(gh, {
312
312
  isLocalSource: (ref) => ref.kind === "GitRepository" && ref.name === "flux-system",
313
313
  });
314
314
 
315
- // renders every Flux Kustomization at baseRef and in the working tree
316
- const result = await flux.diff({ baseRef: process.env.BASE_SHA! });
317
- // result.changed: [{ name, namespace, path, status, diff, additions, deletions, notes }]
318
- // result.skipped: Kustomizations whose source is not this repository
319
-
320
- // posts a new PR comment. If it would exceed GitHub's comment size limit, the full
315
+ // renders every Flux Kustomization at baseRef and in the working tree, then posts the
316
+ // changed ones as a new PR comment. If comment would exceed GitHub's comment size limit, the full
321
317
  // comment is uploaded as a workflow run artifact and the PR comment links to it instead.
322
- await flux.commentDiff({
318
+ await flux.diff({
319
+ baseRef: process.env.BASE_SHA!,
323
320
  owner: "tahminator",
324
321
  repository: "infra",
325
322
  prId: 123,
326
- result,
327
323
  title: "Rendered manifest diff", // optional
328
324
  description: "Secrets are shown SOPS-encrypted.", // optional
329
325
  });
330
326
  ```
331
327
 
332
- Each Kustomization is rendered as `kustomize build` of `spec.path` with `targetNamespace`, `namePrefix`, `nameSuffix`, `components`, `images`, `patches` and `commonMetadata` applied on top, followed by `postBuild.substitute`. Paths without a `kustomization.yaml` get one generated, as Flux does. Values from `postBuild.substituteFrom` live in the cluster, so those variables are left as-is and listed in the diff's notes. A build failure is shown in the diff instead of being skipped. `HelmRelease` objects are diffed as manifests; charts are not templated.
328
+ Each Kustomization is rendered as `kustomize build` of `spec.path` with `targetNamespace`, `namePrefix`, `nameSuffix`, `components`, `images`, `patches` and `commonMetadata` applied on top, followed by `postBuild.substitute`. Paths without a `kustomization.yaml` get one generated, as Flux does. Values from `postBuild.substituteFrom` live in the cluster, so those variables are left as-is and listed in the diff's notes. A build failure is shown in the comment instead of being skipped. `HelmRelease` objects are diffed as manifests; charts are not templated.
329
+
330
+ The diff is split per Kubernetes object (matched by API group, kind, namespace and name), and each object is labelled with the file that defines it, taken from kustomize's `originAnnotations`. Generated ConfigMaps and Secrets point at the `kustomization.yaml` whose generator creates them. Files that only patch an object are not recorded, so a change made by a patch is attributed to the object's original file. A renamed object (e.g. a new generator hash suffix) shows as one removal and one addition.
333
331
 
334
332
  ### `DockerClient`
335
333
 
@@ -1,6 +1,6 @@
1
1
  import type { GitHubClient } from "../gh";
2
2
  import { type FormatFluxDiffOpts } from "./format";
3
- import { type FluxClientOpts, type FluxDiffResult, type FluxKustomization } from "./types";
3
+ import { type FluxClientOpts, type FluxKustomization } from "./types";
4
4
  /**
5
5
  * Works with a repository of Flux manifests: finds its Flux `Kustomization`s, renders them the
6
6
  * way kustomize-controller would, and reports how a change affects the rendered output.
@@ -16,27 +16,18 @@ export declare class FluxClient {
16
16
  findKustomizations(root?: string): Promise<FluxKustomization[]>;
17
17
  /**
18
18
  * Renders every Flux `Kustomization` at `baseRef` and in the working tree at `root`,
19
- * returning the ones whose output differs.
19
+ * and posts the ones whose output differs as a new PR comment.
20
20
  *
21
21
  * `baseRef` is fetched from `origin` if it is not available locally (e.g. shallow CI clones).
22
- */
23
- diff({ baseRef, root, }: {
24
- baseRef: string;
25
- root?: string;
26
- }): Promise<FluxDiffResult>;
27
- /**
28
- * Posts a {@link diff} result as a new PR comment.
29
- *
30
22
  * If the comment would exceed GitHub's size limit, the full comment is uploaded as an
31
23
  * artifact of the current workflow run and the PR comment links to it instead.
32
24
  */
33
- commentDiff({ owner, repository, prId, result, ...formatOpts }: {
25
+ diff({ baseRef, root, owner, repository, prId, ...formatOpts }: {
26
+ baseRef: string;
27
+ root?: string;
34
28
  owner: string;
35
29
  repository: string;
36
30
  prId: number;
37
- result: FluxDiffResult;
38
- } & Omit<FormatFluxDiffOpts, "artifactUrl">): Promise<{
39
- message: string;
40
- artifactUrl?: string;
41
- }>;
31
+ } & Omit<FormatFluxDiffOpts, "artifactUrl">): Promise<void>;
32
+ private collectDiff;
42
33
  }
@@ -31,11 +31,45 @@ export class FluxClient {
31
31
  }
32
32
  /**
33
33
  * Renders every Flux `Kustomization` at `baseRef` and in the working tree at `root`,
34
- * returning the ones whose output differs.
34
+ * and posts the ones whose output differs as a new PR comment.
35
35
  *
36
36
  * `baseRef` is fetched from `origin` if it is not available locally (e.g. shallow CI clones).
37
+ * If the comment would exceed GitHub's size limit, the full comment is uploaded as an
38
+ * artifact of the current workflow run and the PR comment links to it instead.
37
39
  */
38
- async diff({ baseRef, root = ".", }) {
40
+ async diff({ baseRef, root = ".", owner, repository, prId, ...formatOpts }) {
41
+ const result = await this.collectDiff(baseRef, root);
42
+ const full = formatFluxDiff(result, formatOpts);
43
+ if (full.length <= MAX_COMMENT_LENGTH) {
44
+ await this.ghClient.sendPrMessage({
45
+ owner,
46
+ repository,
47
+ prId,
48
+ message: full,
49
+ });
50
+ return;
51
+ }
52
+ const dir = await mkdtemp(path.join(tmpdir(), "flux-diff-artifact-"));
53
+ try {
54
+ const file = path.join(dir, "flux-diff.md");
55
+ await Bun.write(file, full);
56
+ const attempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
57
+ const { url } = await this.ghClient.uploadArtifact({
58
+ name: `flux-diff-pr-${prId}-attempt-${attempt}`,
59
+ files: [file],
60
+ rootDirectory: dir,
61
+ });
62
+ const message = formatFluxDiff(result, {
63
+ ...formatOpts,
64
+ artifactUrl: url,
65
+ });
66
+ await this.ghClient.sendPrMessage({ owner, repository, prId, message });
67
+ }
68
+ finally {
69
+ await rm(dir, { recursive: true, force: true });
70
+ }
71
+ }
72
+ async collectDiff(baseRef, root) {
39
73
  const workdir = await mkdtemp(path.join(tmpdir(), "flux-diff-"));
40
74
  const baseRoot = path.join(workdir, "base");
41
75
  await $ `git -C ${root} fetch --depth=1 origin ${baseRef}`.quiet().nothrow();
@@ -55,6 +89,8 @@ export class FluxClient {
55
89
  }
56
90
  const changed = [];
57
91
  const skipped = [];
92
+ let nextDiffId = 0;
93
+ const nextDiffDir = () => path.join(workdir, "diffs", String(nextDiffId++));
58
94
  await Promise.all([...pairs.values()].map(async ({ base, head }) => {
59
95
  const current = (head ?? base);
60
96
  const { name, namespace } = current;
@@ -73,13 +109,23 @@ export class FluxClient {
73
109
  base ? renderKustomization(baseRoot, base) : undefined,
74
110
  head ? renderKustomization(root, head) : undefined,
75
111
  ]);
76
- const baseOutput = baseRender?.output ?? "";
77
- const headOutput = headRender?.output ?? "";
78
- if (baseOutput === headOutput) {
112
+ const buildError = headRender?.error;
113
+ const objects = buildError ?
114
+ []
115
+ : await diffObjects(baseRender?.objects ?? [], headRender?.objects ?? [], nextDiffDir);
116
+ const unchanged = buildError ?
117
+ buildError === baseRender?.error
118
+ : objects.length === 0;
119
+ if (unchanged) {
79
120
  return;
80
121
  }
81
- const diff = await unifiedDiff(path.join(workdir, "diffs", namespace, name), current.path, baseOutput, headOutput);
82
- const diffLines = diff.split("\n");
122
+ const notes = new Set([
123
+ ...(baseRender?.notes ?? []),
124
+ ...(headRender?.notes ?? []),
125
+ ]);
126
+ if (baseRender?.error && !buildError) {
127
+ notes.add("`kustomize build` fails at the base commit, so every object is shown as added.");
128
+ }
83
129
  changed.push({
84
130
  name,
85
131
  namespace,
@@ -87,15 +133,11 @@ export class FluxClient {
87
133
  status: !base ? "added"
88
134
  : !head ? "removed"
89
135
  : "changed",
90
- diff,
91
- additions: diffLines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length,
92
- deletions: diffLines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length,
93
- notes: [
94
- ...new Set([
95
- ...(baseRender?.notes ?? []),
96
- ...(headRender?.notes ?? []),
97
- ]),
98
- ],
136
+ objects,
137
+ ...(buildError ? { buildError } : {}),
138
+ additions: objects.reduce((sum, o) => sum + o.additions, 0),
139
+ deletions: objects.reduce((sum, o) => sum + o.deletions, 0),
140
+ notes: [...notes],
99
141
  });
100
142
  }));
101
143
  const byLabel = (a, b) => label(a).localeCompare(label(b));
@@ -112,44 +154,54 @@ export class FluxClient {
112
154
  await rm(workdir, { recursive: true, force: true });
113
155
  }
114
156
  }
115
- /**
116
- * Posts a {@link diff} result as a new PR comment.
117
- *
118
- * If the comment would exceed GitHub's size limit, the full comment is uploaded as an
119
- * artifact of the current workflow run and the PR comment links to it instead.
120
- */
121
- async commentDiff({ owner, repository, prId, result, ...formatOpts }) {
122
- const full = formatFluxDiff(result, formatOpts);
123
- if (full.length <= MAX_COMMENT_LENGTH) {
124
- await this.ghClient.sendPrMessage({
125
- owner,
126
- repository,
127
- prId,
128
- message: full,
129
- });
130
- return { message: full };
131
- }
132
- const dir = await mkdtemp(path.join(tmpdir(), "flux-diff-artifact-"));
133
- try {
134
- const file = path.join(dir, "flux-diff.md");
135
- await Bun.write(file, full);
136
- const attempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
137
- const { url } = await this.ghClient.uploadArtifact({
138
- name: `flux-diff-pr-${prId}-attempt-${attempt}`,
139
- files: [file],
140
- rootDirectory: dir,
141
- });
142
- const message = formatFluxDiff(result, {
143
- ...formatOpts,
144
- artifactUrl: url,
145
- });
146
- await this.ghClient.sendPrMessage({ owner, repository, prId, message });
147
- return { message, artifactUrl: url };
148
- }
149
- finally {
150
- await rm(dir, { recursive: true, force: true });
151
- }
157
+ }
158
+ /**
159
+ * Pairs base and head objects by API group, kind, namespace and name, and diffs the ones whose
160
+ * rendered YAML differs. An apiVersion bump within a group is a change, not a remove + add.
161
+ */
162
+ async function diffObjects(baseObjects, headObjects, nextDir) {
163
+ const key = (object) => {
164
+ const group = object.apiVersion.includes("/") ? object.apiVersion.split("/")[0] : "";
165
+ return [group, object.kind, object.namespace, object.name].join("\0");
166
+ };
167
+ const pairs = new Map();
168
+ for (const object of baseObjects) {
169
+ pairs.set(key(object), { base: object });
152
170
  }
171
+ for (const object of headObjects) {
172
+ const k = key(object);
173
+ pairs.set(k, { ...pairs.get(k), head: object });
174
+ }
175
+ const diffs = await Promise.all([...pairs.values()].map(async ({ base, head }) => {
176
+ if (base?.yaml === head?.yaml) {
177
+ return null;
178
+ }
179
+ const current = (head ?? base);
180
+ const { kind, namespace, name, file } = current;
181
+ const diff = await unifiedDiff(nextDir(), [kind, namespace, name].filter(Boolean).join("/"), base?.yaml ?? "", head?.yaml ?? "");
182
+ const lines = diff.split("\n");
183
+ const objectDiff = {
184
+ kind,
185
+ namespace,
186
+ name,
187
+ ...(file ? { file } : {}),
188
+ status: !base ? "added"
189
+ : !head ? "removed"
190
+ : "changed",
191
+ diff,
192
+ additions: lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).length,
193
+ deletions: lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).length,
194
+ };
195
+ return objectDiff;
196
+ }));
197
+ return diffs
198
+ .filter((diff) => diff !== null)
199
+ .sort((a, b) =>
200
+ // objects without a recorded file sort last
201
+ (a.file ?? "\uffff").localeCompare(b.file ?? "\uffff") ||
202
+ a.kind.localeCompare(b.kind) ||
203
+ a.namespace.localeCompare(b.namespace) ||
204
+ a.name.localeCompare(b.name));
153
205
  }
154
206
  async function unifiedDiff(dir, targetPath, baseOutput, headOutput) {
155
207
  await mkdir(dir, { recursive: true });
@@ -9,11 +9,26 @@ export function formatFluxDiff(result, { title = "Flux diff", description, artif
9
9
  `No Flux Kustomization renders differently against ${base}.`
10
10
  : `${result.changed.length} Flux Kustomization(s) render differently against ${base}.`, "");
11
11
  if (artifactUrl) {
12
- lines.push(`> [!WARNING]`, `> The rendered diff is too long for a GitHub comment. [Download the full diff from the workflow run](${artifactUrl}).`, "", ...result.changed.map((change) => `- ${summary(change)}`), "");
12
+ lines.push(`> [!WARNING]`, `> The rendered diff is too long for a GitHub comment. [Download the full diff from the workflow run](${artifactUrl}).`, "");
13
+ for (const change of result.changed) {
14
+ lines.push(`- ${summary(change)}`);
15
+ if (change.buildError) {
16
+ lines.push(" - `kustomize build` failed");
17
+ }
18
+ lines.push(...change.objects.map((object) => ` - ${objectSummary(object)}`));
19
+ }
20
+ lines.push("");
13
21
  }
14
22
  else {
15
23
  for (const change of result.changed) {
16
- lines.push(`<details><summary>${summary(change)}</summary>`, "", ...change.notes.map((note) => `> ${note}\n`), "````diff", change.diff.trimEnd(), "````", "", "</details>", "");
24
+ lines.push(`<details><summary>${summary(change)}</summary>`, "", ...change.notes.map((note) => `> ${note}\n`));
25
+ if (change.buildError) {
26
+ lines.push("`kustomize build` failed:", "", "````text", change.buildError, "````", "");
27
+ }
28
+ for (const object of change.objects) {
29
+ lines.push(objectSummary(object), "", "````diff", object.diff.trimEnd(), "````", "");
30
+ }
31
+ lines.push("</details>", "");
17
32
  }
18
33
  }
19
34
  if (result.skipped.length > 0) {
@@ -25,5 +40,11 @@ export function label({ name, namespace, }) {
25
40
  return namespace ? `${namespace}/${name}` : name;
26
41
  }
27
42
  function summary(change) {
28
- return `<code>${label(change)}</code> — <code>${change.path}</code> (${change.status}, +${change.additions} −${change.deletions})`;
43
+ const counts = change.buildError ? "build failed" : (`${change.objects.length} object(s), +${change.additions} −${change.deletions}`);
44
+ return `<code>${label(change)}</code> — <code>${change.path}</code> (${change.status}, ${counts})`;
45
+ }
46
+ function objectSummary(object) {
47
+ const name = [object.namespace, object.name].filter(Boolean).join("/");
48
+ const file = object.file ? ` — \`${object.file}\`` : "";
49
+ return `**\`${object.kind}\` \`${name}\`**${file} (${object.status}, +${object.additions} −${object.deletions})`;
29
50
  }
@@ -1,4 +1,2 @@
1
1
  export * from "./client";
2
- export { formatFluxDiff, type FormatFluxDiffOpts } from "./format";
3
- export { substituteVariables } from "./render";
4
- export * from "./types";
2
+ export type { FluxClientOpts, FluxKustomization, FluxSourceRef } from "./types";
@@ -1,4 +1 @@
1
1
  export * from "./client";
2
- export { formatFluxDiff } from "./format";
3
- export { substituteVariables } from "./render";
4
- export * from "./types";
@@ -1,6 +1,22 @@
1
1
  import type { FluxKustomization } from "./types";
2
- export type RenderedKustomization = {
3
- output: string;
2
+ export type RenderedObject = {
3
+ apiVersion: string;
4
+ kind: string;
5
+ namespace: string;
6
+ name: string;
7
+ /**
8
+ * repository-relative file that defines the object (for generated objects, the
9
+ * kustomization.yaml whose generator creates it); `undefined` if kustomize did not record one
10
+ */
11
+ file?: string;
12
+ /** the object's rendered YAML, without the origin annotation */
13
+ yaml: string;
14
+ };
15
+ type RenderedKustomization = {
16
+ /** rendered objects, in kustomize's output order; empty when `error` is set */
17
+ objects: RenderedObject[];
18
+ /** `kustomize build` error output, when the build failed */
19
+ error?: string;
4
20
  notes: string[];
5
21
  };
6
22
  /**
@@ -9,7 +25,7 @@ export type RenderedKustomization = {
9
25
  * `nameSuffix`, `components`, `images`, `patches`, `commonMetadata`) applied on top,
10
26
  * followed by `postBuild` variable substitution.
11
27
  *
12
- * A failed build is returned as commented-out output so it shows up in a diff.
28
+ * Each rendered object records the file that defines it.
13
29
  */
14
30
  export declare function renderKustomization(root: string, kustomization: FluxKustomization): Promise<RenderedKustomization>;
15
31
  /**
@@ -25,3 +41,4 @@ export declare function substituteVariables(input: string, vars: Record<string,
25
41
  output: string;
26
42
  unresolved: string[];
27
43
  };
44
+ export {};
@@ -10,13 +10,15 @@ const KUSTOMIZATION_FILES = [
10
10
  const SUBSTITUTE_DISABLED = /kustomize\.toolkit\.fluxcd\.io\/substitute:\s*["']?disabled["']?/;
11
11
  /** `${VAR}`, `${VAR:-default}`, `${VAR-default}`, `${VAR:=default}`, `${VAR=default}`; `$${VAR}` escapes. */
12
12
  const VARIABLE = /\$(\$)?\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?[-=])([^}]*))?\}/g;
13
+ /** kustomize's `originAnnotations` build metadata; records the file that defines each object. */
14
+ const ORIGIN_ANNOTATION = "config.kubernetes.io/origin";
13
15
  /**
14
16
  * Renders a Flux `Kustomization` the way kustomize-controller does: `kustomize build` of
15
17
  * `spec.path` with the overlay fields from the spec (`targetNamespace`, `namePrefix`,
16
18
  * `nameSuffix`, `components`, `images`, `patches`, `commonMetadata`) applied on top,
17
19
  * followed by `postBuild` variable substitution.
18
20
  *
19
- * A failed build is returned as commented-out output so it shows up in a diff.
21
+ * Each rendered object records the file that defines it.
20
22
  */
21
23
  export async function renderKustomization(root, kustomization) {
22
24
  const notes = [];
@@ -45,19 +47,14 @@ export async function renderKustomization(root, kustomization) {
45
47
  [{ pairs: spec.commonMetadata.labels, includeSelectors: false }]
46
48
  : undefined,
47
49
  commonAnnotations: spec.commonMetadata?.annotations,
50
+ buildMetadata: ["originAnnotations"],
48
51
  };
49
52
  await Bun.write(path.join(wrapper, "kustomization.yaml"), yaml.stringify(wrapperKustomization));
50
53
  const result = await $ `kustomize build --load-restrictor LoadRestrictionsNone ${wrapper}`
51
54
  .quiet()
52
55
  .nothrow();
53
56
  if (result.exitCode !== 0) {
54
- const stderr = result.stderr
55
- .toString()
56
- .trim()
57
- .split("\n")
58
- .map((line) => `# ${line}`)
59
- .join("\n");
60
- return { output: `# kustomize build failed\n${stderr}\n`, notes };
57
+ return { objects: [], error: result.stderr.toString().trim(), notes };
61
58
  }
62
59
  let output = result.stdout.toString();
63
60
  if (spec.postBuild) {
@@ -70,12 +67,56 @@ export async function renderKustomization(root, kustomization) {
70
67
  .join(", ")}.`);
71
68
  }
72
69
  }
73
- return { output, notes };
70
+ const docs = yaml.parseAllDocuments(output);
71
+ const invalid = docs.find((doc) => doc.errors.length > 0);
72
+ if (invalid) {
73
+ return {
74
+ objects: [],
75
+ error: `rendered output is not valid YAML: ${invalid.errors[0]?.message}`,
76
+ notes,
77
+ };
78
+ }
79
+ const objects = docs
80
+ .filter((doc) => doc.contents !== null)
81
+ .map((doc) => toRenderedObject(doc, root, wrapper));
82
+ return { objects, notes };
74
83
  }
75
84
  finally {
76
85
  await rm(wrapper, { recursive: true, force: true });
77
86
  }
78
87
  }
88
+ function toRenderedObject(doc, root, wrapper) {
89
+ const origin = doc.getIn(["metadata", "annotations", ORIGIN_ANNOTATION]);
90
+ doc.deleteIn(["metadata", "annotations", ORIGIN_ANNOTATION]);
91
+ const annotations = doc.getIn(["metadata", "annotations"]);
92
+ if (yaml.isMap(annotations) && annotations.items.length === 0) {
93
+ doc.deleteIn(["metadata", "annotations"]);
94
+ }
95
+ const value = (doc.toJS() ?? {});
96
+ return {
97
+ apiVersion: String(value.apiVersion ?? ""),
98
+ kind: String(value.kind ?? ""),
99
+ namespace: String(value.metadata?.namespace ?? ""),
100
+ name: String(value.metadata?.name ?? ""),
101
+ file: typeof origin === "string" ?
102
+ originFile(origin, root, wrapper)
103
+ : undefined,
104
+ yaml: doc.toString({ lineWidth: 0 }).replace(/^---\n/, ""),
105
+ };
106
+ }
107
+ /**
108
+ * Converts an origin annotation (`path`, or `configuredIn` for generated objects, relative to
109
+ * the wrapper kustomization) to a repository-relative path. Remote origins are returned as
110
+ * `<repo>//<path>?ref=<ref>`.
111
+ */
112
+ function originFile(origin, root, wrapper) {
113
+ const parsed = yaml.parse(origin);
114
+ const file = parsed?.path ?? parsed?.configuredIn;
115
+ if (parsed?.repo) {
116
+ return `${parsed.repo}//${file ?? ""}${parsed.ref ? `?ref=${parsed.ref}` : ""}`;
117
+ }
118
+ return file ? path.relative(root, path.resolve(wrapper, file)) : undefined;
119
+ }
79
120
  /**
80
121
  * Applies Flux `postBuild` substitution to rendered multi-document YAML. Documents annotated
81
122
  * with `kustomize.toolkit.fluxcd.io/substitute: disabled` are left untouched.
@@ -40,7 +40,7 @@ export declare const fluxKustomizationSchema: z.ZodObject<{
40
40
  }, z.core.$strip>>;
41
41
  }, z.core.$strip>;
42
42
  }, z.core.$strip>;
43
- export type FluxKustomizationSpec = z.infer<typeof fluxKustomizationSchema>["spec"];
43
+ type FluxKustomizationSpec = z.infer<typeof fluxKustomizationSchema>["spec"];
44
44
  /** A Flux `Kustomization` found in the repository. */
45
45
  export type FluxKustomization = {
46
46
  /** repository-relative file the Kustomization is declared in */
@@ -53,13 +53,35 @@ export type FluxKustomization = {
53
53
  spec: FluxKustomizationSpec;
54
54
  };
55
55
  export type FluxSourceRef = FluxKustomizationSpec["sourceRef"];
56
+ type FluxDiffStatus = "added" | "changed" | "removed";
57
+ /** A rendered Kubernetes object whose output differs between base and head. */
58
+ export type FluxObjectDiff = {
59
+ kind: string;
60
+ /** `""` for cluster-scoped objects */
61
+ namespace: string;
62
+ name: string;
63
+ /**
64
+ * repository-relative file that defines the object, from kustomize's origin annotations.
65
+ * Generated objects (ConfigMap/Secret generators) point at the kustomization.yaml that
66
+ * generates them. Files that only patch the object are not recorded.
67
+ */
68
+ file?: string;
69
+ status: FluxDiffStatus;
70
+ /** unified diff of the object's rendered YAML */
71
+ diff: string;
72
+ additions: number;
73
+ deletions: number;
74
+ };
56
75
  export type FluxKustomizationDiff = {
57
76
  name: string;
58
77
  namespace: string;
59
78
  path: string;
60
- status: "added" | "changed" | "removed";
61
- /** unified diff of the rendered output */
62
- diff: string;
79
+ status: FluxDiffStatus;
80
+ /** changed objects, sorted by file then kind, namespace and name */
81
+ objects: FluxObjectDiff[];
82
+ /** `kustomize build` error at head; `objects` is empty when set */
83
+ buildError?: string;
84
+ /** totals across `objects` */
63
85
  additions: number;
64
86
  deletions: number;
65
87
  /** caveats about how faithful the render is, e.g. unresolvable `substituteFrom` variables */
@@ -92,3 +114,4 @@ export type FluxClientOpts = {
92
114
  */
93
115
  isLocalSource?: (sourceRef: FluxSourceRef) => boolean;
94
116
  };
117
+ export {};
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "type": "module",
4
4
  "author": "Tahmid Ahmed",
5
5
  "description": "A collection of Bun shell scripts that can be re-used in various CICD pipelines.",
6
- "version": "1.0.90-beta.gd62102f7",
6
+ "version": "1.0.91",
7
7
  "repository": {
8
8
  "url": "git+https://github.com/tahminator/pipeline.git"
9
9
  },