From b60f7d2c483d569014638dac5a0ae540ced5509c Mon Sep 17 00:00:00 2001 From: Drew Bailey Date: Wed, 8 Apr 2026 11:20:19 -0400 Subject: [PATCH 1/2] Fix | Truncate markdown summary before max diff length --- pkg/diff/markdown.go | 66 +++++++++++++++++++++++++++------ pkg/diff/markdown_test.go | 77 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 16 deletions(-) diff --git a/pkg/diff/markdown.go b/pkg/diff/markdown.go index f7b5cbe8..97c3bb5c 100644 --- a/pkg/diff/markdown.go +++ b/pkg/diff/markdown.go @@ -47,6 +47,13 @@ var ( diffTooLongWarning = "\n🚨 Diff is too long" ) +// the InfoBox has a dynamic size. This is a problem for the integration tests, because the output is not deterministic. +// By adding a buffer, we ensure availableSpaceForDetailedDiff has a fixed size +const ( + infoBoxBufferSize = 80 + summaryTooLongNotice = "\n... Summary truncated to fit `--max-diff-length`" +) + // build returns the section content and a boolean indicating if the section was truncated func (m *MarkdownSection) build(maxSize int) (string, bool) { header := markdownSectionHeader(m.appName, m.filePath, m.appURL) @@ -148,23 +155,60 @@ Summary: %info_box% ` +func truncateSummary(summary string, maxSize int) (string, bool) { + summary = strings.TrimSpace(summary) + if len(summary) <= maxSize { + return summary, false + } + if maxSize <= 0 { + return "", true + } + + if maxSize <= len(summaryTooLongNotice) { + return summaryTooLongNotice[:maxSize], true + } + + trimmedSummary := strings.TrimRight(summary[:maxSize-len(summaryTooLongNotice)], " \t\n\r") + if trimmedSummary == "" { + return summaryTooLongNotice[:maxSize], true + } + + return trimmedSummary + summaryTooLongNotice, true +} + func (m *MarkdownOutput) printDiff(maxDiffMessageCharCount uint) string { - output := strings.ReplaceAll(markdownTemplate, "%title%", m.title) - output = strings.ReplaceAll(output, "%summary%", strings.TrimSpace(m.summary)) selection_changes := "" if s := m.selectionInfo.String(); s != "" { selection_changes = fmt.Sprintf("\n%s\n", s) } - output = strings.ReplaceAll(output, "%selection_changes%", selection_changes) - - // the InfoBox has a dynamic size. This is a problem for the integration tests, because the output is not deterministic. - // By adding a buffer, we ensure availableSpaceForDetailedDiff has a fixed size - infoBoxBufferSize := 80 warningMessage := fmt.Sprintf("⚠️⚠️⚠️ Diff exceeds max length of %d characters. Truncating to fit. This can be adjusted with the `--max-diff-length` flag", maxDiffMessageCharCount) + output := strings.ReplaceAll(markdownTemplate, "%title%", m.title) + output = strings.ReplaceAll(output, "%selection_changes%", selection_changes) + + // temp value to check if summary was truncated, to decide whether to log a warning about it + var summary string + + // Truncate summary upfront if it would consume the entire budget + if 0 < maxDiffMessageCharCount { + diffLengthWithoutSummary := len(strings.ReplaceAll(output, "%summary%", "")) + summaryBudget := int(maxDiffMessageCharCount) - diffLengthWithoutSummary - len(warningMessage) - infoBoxBufferSize + truncatedSummary, truncated := truncateSummary(m.summary, summaryBudget) + if truncated { + log.Warn().Msgf("🚨 Markdown summary is too long, truncating to fit --max-diff-length (%d)", maxDiffMessageCharCount) + summary = truncatedSummary + } else { + summary = strings.TrimSpace(m.summary) + } + } else { + summary = strings.TrimSpace(m.summary) + } + + output = strings.ReplaceAll(output, "%summary%", summary) + availableSpaceForDetailedDiff := int(maxDiffMessageCharCount) - len(output) - len(warningMessage) - infoBoxBufferSize log.Debug().Msgf("availableSpaceForDetailedDiff: %d", availableSpaceForDetailedDiff) @@ -172,7 +216,7 @@ func (m *MarkdownOutput) printDiff(maxDiffMessageCharCount uint) string { var sectionsDiff strings.Builder spaceRemaining := availableSpaceForDetailedDiff - AddWarning := false + addWarning := false for _, section := range m.sections { if spaceRemaining <= 0 { @@ -181,12 +225,12 @@ func (m *MarkdownOutput) printDiff(maxDiffMessageCharCount uint) string { sectionContent, truncated := section.build(spaceRemaining) sectionsDiff.WriteString(sectionContent) if truncated { - AddWarning = true + addWarning = true } spaceRemaining -= len(sectionContent) } - if AddWarning { + if addWarning { sectionsDiff.WriteString(warningMessage) } @@ -204,7 +248,7 @@ func (m *MarkdownOutput) printDiff(maxDiffMessageCharCount uint) string { output = strings.TrimSpace(output) + "\n" - if AddWarning { + if addWarning { // log warning log.Warn().Msgf("🚨 Markdown diff is too long, which exceeds --max-diff-length (%d). Truncating to %d characters. This can be adjusted with the `--max-diff-length` flag", maxDiffMessageCharCount, len(output)) log.Warn().Msgf("🚨 HTML diff is not affected by this truncation") diff --git a/pkg/diff/markdown_test.go b/pkg/diff/markdown_test.go index b88a3fb6..e0eb9efc 100644 --- a/pkg/diff/markdown_test.go +++ b/pkg/diff/markdown_test.go @@ -139,7 +139,6 @@ func TestMarkdownOutput_PrintDiff(t *testing.T) { tests := []struct { name string output MarkdownOutput - maxSize int maxDiffMessageCharCount uint expectedContains []string expectedNotContains []string @@ -172,7 +171,6 @@ func TestMarkdownOutput_PrintDiff(t *testing.T) { FullDuration: time.Second * 5, }, }, - maxSize: 10000, maxDiffMessageCharCount: 5000, expectedContains: []string{ "## Test Diff", @@ -200,7 +198,6 @@ func TestMarkdownOutput_PrintDiff(t *testing.T) { ApplicationCount: 0, }, }, - maxSize: 10000, maxDiffMessageCharCount: 5000, expectedContains: []string{ "## Empty Diff", @@ -231,7 +228,6 @@ func TestMarkdownOutput_PrintDiff(t *testing.T) { ApplicationCount: 1, }, }, - maxSize: 3, // Extremely small - like the user in issue #392 maxDiffMessageCharCount: 3, expectedContains: []string{ "too small to display them", @@ -260,7 +256,6 @@ func TestMarkdownOutput_PrintDiff(t *testing.T) { ApplicationCount: 1, }, }, - maxSize: 500, // Small size to force truncation maxDiffMessageCharCount: 500, expectedContains: []string{ "## Large Diff", @@ -701,3 +696,75 @@ func TestMarkdownOutput_SelectionChanges(t *testing.T) { } }) } + +func TestTruncateSummary(t *testing.T) { + t.Run("No truncation when summary fits", func(t *testing.T) { + summary := "Modified (1):\n± app (+1)" + got, truncated := truncateSummary(summary, len(summary)) + if truncated { + t.Fatalf("expected summary to fit without truncation") + } + if got != summary { + t.Fatalf("expected summary to remain unchanged, got %q", got) + } + }) + + t.Run("Adds explicit notice when summary is truncated", func(t *testing.T) { + summary := strings.Repeat("± spark-a--clark (+1)\n", 50) + got, truncated := truncateSummary(summary, 120) + if !truncated { + t.Fatalf("expected summary to be truncated") + } + if len(got) > 120 { + t.Fatalf("expected truncated summary to fit budget, got len=%d", len(got)) + } + if !strings.Contains(got, "Summary truncated to fit") { + t.Fatalf("expected truncation notice, got %q", got) + } + }) + + t.Run("Drops summary entirely when no space remains", func(t *testing.T) { + got, truncated := truncateSummary("Modified (1):\n± app (+1)", 0) + if !truncated { + t.Fatalf("expected summary to be truncated") + } + if got != "" { + t.Fatalf("expected empty summary when budget is exhausted, got %q", got) + } + }) +} + +func TestMarkdownOutput_PrintDiff_TruncatesLargeSummary(t *testing.T) { + summary := "Modified (11160):\n" + strings.Repeat("± spark-a--clark (+1)\n", 400) + + output := MarkdownOutput{ + title: "Large Summary Diff", + summary: summary, + sections: []MarkdownSection{ + { + appName: "spark-a--clark", + filePath: "apps/spark-a--clark.yaml", + resources: []ResourceSection{ + {Header: "@@ Application modified: spark-a--clark @@", Content: "+ small change"}, + }, + }, + }, + statsInfo: StatsInfo{ + ApplicationCount: 1, + FullDuration: time.Second, + }, + } + + const maxDiffMessageCharCount = 700 + got := output.printDiff(maxDiffMessageCharCount) + + if len(got) > maxDiffMessageCharCount { + t.Fatalf("expected markdown output to fit max diff length, got %d > %d", len(got), maxDiffMessageCharCount) + } + if !strings.Contains(got, "Summary truncated to fit") { + t.Fatalf("expected output to contain summary truncation notice, got:\n%s", got) + } + if !strings.Contains(got, "--max-diff-length") { + t.Fatalf("expected output to mention --max-diff-length, got:\n%s", got) + } +} From 2e40298f492c29494782f89ce2aada40bf6c892d Mon Sep 17 00:00:00 2001 From: Dag Andersen Date: Fri, 10 Apr 2026 20:55:48 +0200 Subject: [PATCH 2/2] Add Integration test --- .../branch-9/target-3/output.html | 1114 +++++++++++++++++ integration-test/branch-9/target-3/output.md | 16 + integration-test/integration_test.go | 10 + 3 files changed, 1140 insertions(+) create mode 100644 integration-test/branch-9/target-3/output.html create mode 100644 integration-test/branch-9/target-3/output.md diff --git a/integration-test/branch-9/target-3/output.html b/integration-test/branch-9/target-3/output.html new file mode 100644 index 00000000..75981f44 --- /dev/null +++ b/integration-test/branch-9/target-3/output.html @@ -0,0 +1,1114 @@ + + + + + + +
+

Argo CD Diff Preview

+ +

Summary:

+
Deleted (9):
+- app1 (-19)
+- app1 (-19)
+- app2 (-19)
+- app2 (-19)
+- custom-target-revision-example (-14)
+- my-app-set-dev (-79)
+- my-app-set-prod (-79)
+- my-app-set-staging (-79)
+- nginx-ingress (-470)
+ +
+
+ +app1 (examples/duplicate-names/app/app-set-1.yaml) + + +

Deployment: deploy-from-folder-one

+
+ + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: deploy-from-folder-one
-spec:
-  replicas: 2
-  selector:
-    matchLabels:
-      app: myapp
-  template:
-    metadata:
-      labels:
-        app: myapp
-    spec:
-      containers:
-      - image: dag-andersen/myapp:latest
-        name: myapp
-        ports:
-        - containerPort: 80
+
+ +
+ +
+ +app1 (examples/duplicate-names/app/app-set-2.yaml) + + +

Deployment: deploy-from-folder-one

+
+ + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: deploy-from-folder-one
-spec:
-  replicas: 2
-  selector:
-    matchLabels:
-      app: myapp
-  template:
-    metadata:
-      labels:
-        app: myapp
-    spec:
-      containers:
-      - image: dag-andersen/myapp:latest
-        name: myapp
-        ports:
-        - containerPort: 80
+
+ +
+ +
+ +app2 (examples/duplicate-names/app/app-set-1.yaml) + + +

Deployment: deploy-from-folder-one

+
+ + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: deploy-from-folder-one
-spec:
-  replicas: 2
-  selector:
-    matchLabels:
-      app: myapp
-  template:
-    metadata:
-      labels:
-        app: myapp
-    spec:
-      containers:
-      - image: dag-andersen/myapp:latest
-        name: myapp
-        ports:
-        - containerPort: 80
+
+ +
+ +
+ +app2 (examples/duplicate-names/app/app-set-2.yaml) + + +

Deployment: deploy-from-folder-one

+
+ + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: deploy-from-folder-one
-spec:
-  replicas: 2
-  selector:
-    matchLabels:
-      app: myapp
-  template:
-    metadata:
-      labels:
-        app: myapp
-    spec:
-      containers:
-      - image: dag-andersen/myapp:latest
-        name: myapp
-        ports:
-        - containerPort: 80
+
+ +
+ +
+ +custom-target-revision-example (examples/custom-target-revision/app/app.yaml) + + +

Deployment: default/my-deployment

+
+ + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: my-deployment
-  namespace: default
-spec:
-  replicas: 2
-  template:
-    spec:
-      containers:
-      - image: dag-andersen/myapp:latest
-        name: my-deployment
-        ports:
-        - containerPort: 80
+
+ +
+ +
+ +my-app-set-dev (examples/basic-appset/my-app-set.yaml) + + +

Deployment: default/super-app-name

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-dev
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app.kubernetes.io/instance: my-app-set-dev
-      app.kubernetes.io/name: myApp
-  template:
-    metadata:
-      labels:
-        app.kubernetes.io/instance: my-app-set-dev
-        app.kubernetes.io/managed-by: Helm
-        app.kubernetes.io/name: myApp
-        app.kubernetes.io/version: 1.16.0
-        helm.sh/chart: myApp-0.1.0
-    spec:
-      containers:
-      - image: nginx:1.16.0
-        imagePullPolicy: IfNotPresent
-        livenessProbe:
-          httpGet:
-            path: /
-            port: http
-        name: myApp
-        ports:
-        - containerPort: 80
-          name: http
-          protocol: TCP
-        readinessProbe:
-          httpGet:
-            path: /
-            port: http
-        resources: {}
-        securityContext: {}
-      securityContext: {}
-      serviceAccountName: super-app-name
+
+ +

Service: default/super-app-name

+
+ + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-dev
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
-spec:
-  ports:
-  - name: http
-    port: 80
-    protocol: TCP
-    targetPort: http
-  selector:
-    app.kubernetes.io/instance: my-app-set-dev
-    app.kubernetes.io/name: myApp
-  type: ClusterIP
+
+ +

ServiceAccount: default/super-app-name

+
+ + + + + + + + + + + + + + +
-apiVersion: v1
-automountServiceAccountToken: true
-kind: ServiceAccount
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-dev
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
+
+ +
+ +
+ +my-app-set-prod (examples/basic-appset/my-app-set.yaml) + + +

Deployment: default/super-app-name

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-prod
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app.kubernetes.io/instance: my-app-set-prod
-      app.kubernetes.io/name: myApp
-  template:
-    metadata:
-      labels:
-        app.kubernetes.io/instance: my-app-set-prod
-        app.kubernetes.io/managed-by: Helm
-        app.kubernetes.io/name: myApp
-        app.kubernetes.io/version: 1.16.0
-        helm.sh/chart: myApp-0.1.0
-    spec:
-      containers:
-      - image: nginx:1.16.0
-        imagePullPolicy: IfNotPresent
-        livenessProbe:
-          httpGet:
-            path: /
-            port: http
-        name: myApp
-        ports:
-        - containerPort: 80
-          name: http
-          protocol: TCP
-        readinessProbe:
-          httpGet:
-            path: /
-            port: http
-        resources: {}
-        securityContext: {}
-      securityContext: {}
-      serviceAccountName: super-app-name
+
+ +

Service: default/super-app-name

+
+ + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-prod
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
-spec:
-  ports:
-  - name: http
-    port: 80
-    protocol: TCP
-    targetPort: http
-  selector:
-    app.kubernetes.io/instance: my-app-set-prod
-    app.kubernetes.io/name: myApp
-  type: ClusterIP
+
+ +

ServiceAccount: default/super-app-name

+
+ + + + + + + + + + + + + + +
-apiVersion: v1
-automountServiceAccountToken: true
-kind: ServiceAccount
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-prod
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
+
+ +
+ +
+ +my-app-set-staging (examples/basic-appset/my-app-set.yaml) + + +

Deployment: default/super-app-name

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-staging
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app.kubernetes.io/instance: my-app-set-staging
-      app.kubernetes.io/name: myApp
-  template:
-    metadata:
-      labels:
-        app.kubernetes.io/instance: my-app-set-staging
-        app.kubernetes.io/managed-by: Helm
-        app.kubernetes.io/name: myApp
-        app.kubernetes.io/version: 1.16.0
-        helm.sh/chart: myApp-0.1.0
-    spec:
-      containers:
-      - image: nginx:1.16.0
-        imagePullPolicy: IfNotPresent
-        livenessProbe:
-          httpGet:
-            path: /
-            port: http
-        name: myApp
-        ports:
-        - containerPort: 80
-          name: http
-          protocol: TCP
-        readinessProbe:
-          httpGet:
-            path: /
-            port: http
-        resources: {}
-        securityContext: {}
-      securityContext: {}
-      serviceAccountName: super-app-name
+
+ +

Service: default/super-app-name

+
+ + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-staging
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
-spec:
-  ports:
-  - name: http
-    port: 80
-    protocol: TCP
-    targetPort: http
-  selector:
-    app.kubernetes.io/instance: my-app-set-staging
-    app.kubernetes.io/name: myApp
-  type: ClusterIP
+
+ +

ServiceAccount: default/super-app-name

+
+ + + + + + + + + + + + + + +
-apiVersion: v1
-automountServiceAccountToken: true
-kind: ServiceAccount
-metadata:
-  labels:
-    app.kubernetes.io/instance: my-app-set-staging
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: myApp
-    app.kubernetes.io/version: 1.16.0
-    helm.sh/chart: myApp-0.1.0
-  name: super-app-name
-  namespace: default
+
+ +
+ +
+ +nginx-ingress (examples/external-chart/nginx.yaml) + + +

ValidatingWebhookConfiguration: nginx-ingress-ingress-nginx-admission

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: admissionregistration.k8s.io/v1
-kind: ValidatingWebhookConfiguration
-metadata:
-  labels:
-    app.kubernetes.io/component: admission-webhook
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx-admission
-webhooks:
-- admissionReviewVersions:
-  - v1
-  clientConfig:
-    service:
-      name: nginx-ingress-ingress-nginx-controller-admission
-      namespace: default
-      path: /networking/v1/ingresses
-  failurePolicy: Fail
-  matchPolicy: Equivalent
-  name: validate.nginx.ingress.kubernetes.io
-  rules:
-  - apiGroups:
-    - networking.k8s.io
-    apiVersions:
-    - v1
-    operations:
-    - CREATE
-    - UPDATE
-    resources:
-    - ingresses
-  sideEffects: None
+
+ +

Deployment: default/nginx-ingress-ingress-nginx-controller

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx-controller
-  namespace: default
-spec:
-  minReadySeconds: 0
-  replicas: 1
-  revisionHistoryLimit: 10
-  selector:
-    matchLabels:
-      app.kubernetes.io/component: controller
-      app.kubernetes.io/instance: nginx-ingress
-      app.kubernetes.io/name: ingress-nginx
-  template:
-    metadata:
-      labels:
-        app.kubernetes.io/component: controller
-        app.kubernetes.io/instance: nginx-ingress
-        app.kubernetes.io/managed-by: Helm
-        app.kubernetes.io/name: ingress-nginx
-        app.kubernetes.io/part-of: ingress-nginx
-        app.kubernetes.io/version: 1.10.0
-        helm.sh/chart: ingress-nginx-4.10.0
-    spec:
-      containers:
-      - args:
-        - /nginx-ingress-controller
-        - --publish-service=$(POD_NAMESPACE)/nginx-ingress-ingress-nginx-controller
-        - --election-id=nginx-ingress-ingress-nginx-leader
-        - --controller-class=k8s.io/ingress-nginx
-        - --ingress-class=test
-        - --configmap=$(POD_NAMESPACE)/nginx-ingress-ingress-nginx-controller
-        - --validating-webhook=:8443
-        - --validating-webhook-certificate=/usr/local/certificates/cert
-        - --validating-webhook-key=/usr/local/certificates/key
-        - --enable-metrics=false
-        env:
-        - name: POD_NAME
-          valueFrom:
-            fieldRef:
-              fieldPath: metadata.name
-        - name: POD_NAMESPACE
-          valueFrom:
-            fieldRef:
-              fieldPath: metadata.namespace
-        - name: LD_PRELOAD
-          value: /usr/local/lib/libmimalloc.so
-        image: registry.k8s.io/ingress-nginx/controller:v1.10.0@sha256:42b3f0e5d0846876b1791cd3afeb5f1cbbe4259d6f35651dcc1b5c980925379c
-        imagePullPolicy: IfNotPresent
-        lifecycle:
-          preStop:
-            exec:
-              command:
-              - /wait-shutdown
-        livenessProbe:
-          failureThreshold: 5
-          httpGet:
-            path: /healthz
-            port: 10254
-            scheme: HTTP
-          initialDelaySeconds: 10
-          periodSeconds: 10
-          successThreshold: 1
-          timeoutSeconds: 1
-        name: controller
-        ports:
-        - containerPort: 80
-          name: http
-          protocol: TCP
-        - containerPort: 443
-          name: https
-          protocol: TCP
-        - containerPort: 8443
-          name: webhook
-          protocol: TCP
-        readinessProbe:
-          failureThreshold: 3
-          httpGet:
-            path: /healthz
-            port: 10254
-            scheme: HTTP
-          initialDelaySeconds: 10
-          periodSeconds: 10
-          successThreshold: 1
-          timeoutSeconds: 1
-        resources:
-          requests:
-            cpu: 100m
-            memory: 90Mi
-        securityContext:
-          allowPrivilegeEscalation: false
-          capabilities:
-            add:
-            - NET_BIND_SERVICE
-            drop:
-            - ALL
-          readOnlyRootFilesystem: false
-          runAsNonRoot: true
-          runAsUser: 101
-          seccompProfile:
-            type: RuntimeDefault
-        volumeMounts:
-        - mountPath: /usr/local/certificates/
-          name: webhook-cert
-          readOnly: true
-      dnsPolicy: ClusterFirst
-      nodeSelector:
-        kubernetes.io/os: linux
-      serviceAccountName: nginx-ingress-ingress-nginx
-      terminationGracePeriodSeconds: 300
-      volumes:
-      - name: webhook-cert
-        secret:
-          secretName: nginx-ingress-ingress-nginx-admission
+
+ +

IngressClass: nginx

+
+ + + + + + + + + + + + + + + + +
-apiVersion: networking.k8s.io/v1
-kind: IngressClass
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx
-spec:
-  controller: k8s.io/ingress-nginx
+
+ +

ClusterRole: nginx-ingress-ingress-nginx

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx
-rules:
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  - endpoints
-  - nodes
-  - pods
-  - secrets
-  - namespaces
-  verbs:
-  - list
-  - watch
-- apiGroups:
-  - coordination.k8s.io
-  resources:
-  - leases
-  verbs:
-  - list
-  - watch
-- apiGroups:
-  - ""
-  resources:
-  - nodes
-  verbs:
-  - get
-- apiGroups:
-  - ""
-  resources:
-  - services
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingresses
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - ""
-  resources:
-  - events
-  verbs:
-  - create
-  - patch
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingresses/status
-  verbs:
-  - update
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingressclasses
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - discovery.k8s.io
-  resources:
-  - endpointslices
-  verbs:
-  - list
-  - watch
-  - get
+
+ +

ClusterRoleBinding: nginx-ingress-ingress-nginx

+
+ + + + + + + + + + + + + + + + + + + + + +
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRoleBinding
-metadata:
-  labels:
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: ClusterRole
-  name: nginx-ingress-ingress-nginx
-subjects:
-- kind: ServiceAccount
-  name: nginx-ingress-ingress-nginx
-  namespace: default
+
+ +

Role: default/nginx-ingress-ingress-nginx

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: rbac.authorization.k8s.io/v1
-kind: Role
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx
-  namespace: default
-rules:
-- apiGroups:
-  - ""
-  resources:
-  - namespaces
-  verbs:
-  - get
-- apiGroups:
-  - ""
-  resources:
-  - configmaps
-  - pods
-  - secrets
-  - endpoints
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - ""
-  resources:
-  - services
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingresses
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingresses/status
-  verbs:
-  - update
-- apiGroups:
-  - networking.k8s.io
-  resources:
-  - ingressclasses
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - coordination.k8s.io
-  resourceNames:
-  - nginx-ingress-ingress-nginx-leader
-  resources:
-  - leases
-  verbs:
-  - get
-  - update
-- apiGroups:
-  - coordination.k8s.io
-  resources:
-  - leases
-  verbs:
-  - create
-- apiGroups:
-  - ""
-  resources:
-  - events
-  verbs:
-  - create
-  - patch
-- apiGroups:
-  - discovery.k8s.io
-  resources:
-  - endpointslices
-  verbs:
-  - list
-  - watch
-  - get
+
+ +

RoleBinding: default/nginx-ingress-ingress-nginx

+
+ + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: rbac.authorization.k8s.io/v1
-kind: RoleBinding
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx
-  namespace: default
-roleRef:
-  apiGroup: rbac.authorization.k8s.io
-  kind: Role
-  name: nginx-ingress-ingress-nginx
-subjects:
-- kind: ServiceAccount
-  name: nginx-ingress-ingress-nginx
-  namespace: default
+
+ +

ConfigMap: default/nginx-ingress-ingress-nginx-controller

+
+ + + + + + + + + + + + + + + + + +
-apiVersion: v1
-data:
-  allow-snippet-annotations: "false"
-kind: ConfigMap
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx-controller
-  namespace: default
+
+ +

Service: default/nginx-ingress-ingress-nginx-controller

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx-controller
-  namespace: default
-spec:
-  ipFamilies:
-  - IPv4
-  ipFamilyPolicy: SingleStack
-  ports:
-  - appProtocol: http
-    name: http
-    port: 80
-    protocol: TCP
-    targetPort: http
-  - appProtocol: https
-    name: https
-    port: 443
-    protocol: TCP
-    targetPort: https
-  selector:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/name: ingress-nginx
-  type: LoadBalancer
+
+ +

Service: default/nginx-ingress-ingress-nginx-controller-admission

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
-apiVersion: v1
-kind: Service
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx-controller-admission
-  namespace: default
-spec:
-  ports:
-  - appProtocol: https
-    name: https-webhook
-    port: 443
-    targetPort: webhook
-  selector:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/name: ingress-nginx
-  type: ClusterIP
+
+ +

ServiceAccount: default/nginx-ingress-ingress-nginx

+
+ + + + + + + + + + + + + + + + +
-apiVersion: v1
-automountServiceAccountToken: true
-kind: ServiceAccount
-metadata:
-  labels:
-    app.kubernetes.io/component: controller
-    app.kubernetes.io/instance: nginx-ingress
-    app.kubernetes.io/managed-by: Helm
-    app.kubernetes.io/name: ingress-nginx
-    app.kubernetes.io/part-of: ingress-nginx
-    app.kubernetes.io/version: 1.10.0
-    helm.sh/chart: ingress-nginx-4.10.0
-  name: nginx-ingress-ingress-nginx
-  namespace: default
+
+ +
+
+ +
_Stats_:
+[Applications: 21], [Full Run: Xs], [Rendering: Xs], [Cluster: Xs], [Argo CD: Xs]
+
+ + diff --git a/integration-test/branch-9/target-3/output.md b/integration-test/branch-9/target-3/output.md new file mode 100644 index 00000000..db88811d --- /dev/null +++ b/integration-test/branch-9/target-3/output.md @@ -0,0 +1,16 @@ +## Argo CD Diff Preview + +Summary: +```yaml +Deleted (9): +- app1 (-19) +- app1 (-19) +- app2 (-19) +- app2 (-19 +... Summary truncated to fit `--max-diff-length` +``` + +⚠️ Changes were found but `--max-diff-length` (400) is too small to display them. Increase the value or check the HTML output instead. + +_Stats_: +[Applications: 21], [Full Run: Xs], [Rendering: Xs], [Cluster: Xs], [Argo CD: Xs] diff --git a/integration-test/integration_test.go b/integration-test/integration_test.go index e5a61992..5f01ea56 100644 --- a/integration-test/integration_test.go +++ b/integration-test/integration_test.go @@ -214,6 +214,16 @@ var testCases = []TestCase{ MaxDiffLength: "900", WatchIfNoWatchPatternFound: "false", }, + // Tests that a large summary gets truncated when --max-diff-length is very small. + // Branch-9 deletes 9 apps, producing a ~200 char summary that won't fit in 400 chars + // after template overhead, forcing summary truncation. + { + Name: "branch-9/target-3", + TargetBranch: "integration-test/branch-9/target", + BaseBranch: "integration-test/branch-9/base", + Suffix: "-3", + MaxDiffLength: "400", + }, { Name: "branch-10/target-1", TargetBranch: "integration-test/branch-10/target",