[tenant][kubernetes] Introduce better cleanup logic - #1661
Conversation
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughPre-delete hook timing added and reorganized: kubernetes delete job now runs as pre-delete with expanded RBAC and an ordered multi-step cleanup; helmreleases teardown template removed; a new tenant-scoped pre-delete cleanup job was introduced; two dashboard deployments receive checksum annotation changes. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Helm as Helm Uninstall
participant PreDeleteJob as Pre-delete Cleanup Job
participant K8s as Kubernetes API
note right of PreDeleteJob `#DDEBF7`: Kubernetes delete orchestration\n(suspend -> delete -> control planes -> datavolumes -> services)
Helm->>PreDeleteJob: trigger pre-delete hooks
PreDeleteJob->>K8s: patch (suspend) HelmReleases
PreDeleteJob->>K8s: delete HelmReleases
PreDeleteJob->>K8s: delete KamajiControlPlane / TenantControlPlane
PreDeleteJob->>K8s: delete DataVolumes (cdi.kubevirt.io)
PreDeleteJob->>K8s: delete LoadBalancer Services (core/v1)
K8s-->>PreDeleteJob: responses / status
PreDeleteJob-->>Helm: cleanup complete
sequenceDiagram
autonumber
participant TenantHelm as Tenant Uninstall
participant TenantJob as Tenant Cleanup Job
participant K8s as Kubernetes API
note right of TenantJob `#F6F3E8`: Two cohorts: Applications\n(cozystack.io/ui=true, tenantmodule!=true)\nTenant Modules (tenantmodule=true)
TenantHelm->>TenantJob: trigger pre-delete hook
TenantJob->>K8s: delete HelmReleases (applications selector)
K8s-->>TenantJob: deleted/ignored
TenantJob->>K8s: delete HelmReleases (tenant modules selector)
K8s-->>TenantJob: deleted/ignored
TenantJob-->>TenantHelm: finished (TTL enforced)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello Andrei Kvapil (@kvaps), I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly refactors and enhances the cleanup logic for Kubernetes resources managed by Helm. The core change involves shifting the main cleanup operations to a Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a more robust cleanup logic for both the kubernetes and tenant Helm charts by moving the cleanup hooks from post-delete to pre-delete. This ensures resources are cleaned up in a specific order before Helm removes the release. The kubernetes chart's cleanup is improved by using a label selector to dynamically find and manage HelmRelease resources, replacing a previous hardcoded list. A new cleanup job is also added for the tenant chart. These are positive changes, but I've found a few issues that could impact the reliability of the cleanup process. My review includes suggestions to improve error handling in the cleanup script, pin the version of a container image, and correct RBAC permissions to ensure the jobs can execute successfully.
| kubectl -n {{ .Release.Namespace }} patch "$hr" \ | ||
| -p '{"spec": {"suspend": true}}' \ | ||
| --type=merge --field-manager=flux-client-side-apply |
There was a problem hiding this comment.
The kubectl patch command is not resilient to failures. Because set -e is active, if patching a single HelmRelease fails, the entire cleanup script will exit. This would prevent other HelmReleases from being suspended and block the subsequent cleanup steps. The previous implementation in the removed helmreleases/delete.yaml file handled this by appending || true to the patch command. To make the cleanup job more robust, consider adding similar resilience here.
kubectl -n {{ .Release.Namespace }} patch "$hr" \
-p '{"spec": {"suspend": true}}' \
--type=merge --field-manager=flux-client-side-apply || trueThere was a problem hiding this comment.
Not relevant we need these errors
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/apps/kubernetes/templates/delete.yaml (1)
1-9: ---Quote the annotation value on line 8 to fix the YAML syntax error.
The
helm.sh/hook-delete-policyannotation value contains commas and must be quoted according to YAML spec. yamllint correctly reports a syntax error.Change line 8 from:
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failedTo:
"helm.sh/hook-delete-policy": "hook-succeeded,before-hook-creation,hook-failed"Note: This same pattern appears in other files (
packages/apps/tenant/templates/cleanup-job.yamland others), which also trigger the same yamllint error and should be corrected similarly.
🧹 Nitpick comments (4)
packages/apps/kubernetes/templates/delete.yaml (2)
32-40: Verify safety of the HelmRelease suspension loop.The loop at lines 33-40 iterates over
kubectl get ... -o nameoutput. The inline checkif [ -n "$hr" ]; thenis redundant becauseget -o nameonly outputs names (or nothing if no matches). However, if the kubectl command itself fails, the script will exit (due toset -e) before any errors are logged. Add explicit error handling for the kubectl call itself.- for hr in $(kubectl -n {{ .Release.Namespace }} get helmreleases.helm.toolkit.fluxcd.io -l "cozystack.io/target-cluster-name={{ .Release.Name }}" -o name 2>/dev/null || true); do - if [ -n "$hr" ]; then - echo " Suspending $hr" - kubectl -n {{ .Release.Namespace }} patch "$hr" \ - -p '{"spec": {"suspend": true}}' \ - --type=merge --field-manager=flux-client-side-apply - fi - done + for hr in $(kubectl -n {{ .Release.Namespace }} get helmreleases.helm.toolkit.fluxcd.io -l "cozystack.io/target-cluster-name={{ .Release.Name }}" -o name 2>/dev/null || echo ""); do + echo " Suspending $hr" + kubectl -n {{ .Release.Namespace }} patch "$hr" \ + -p '{"spec": {"suspend": true}}' \ + --type=merge --field-manager=flux-client-side-apply + doneRemoves the redundant
[ -n "$hr" ]check and simplifies the logic.
88-116: Verify that the 'watch' verb is necessary for HelmRelease patch operations.The Role grants
watchverb for datavolumes and services (newly added), and the HelmReleases rule includespatchverb (line 96). Typically,watchis needed for controller-managed operations, but for direct imperative patches, it may not be required. Confirm whether thewatchverb is necessary, or if it should be removed to follow least-privilege principles.Also, verify that
patchverb with--type=merge --field-manager=flux-client-side-applyis the correct approach for Flux objects. This uses server-side apply semantics; ensure the field-manager alignment is intentional.If
watchis not needed for patch operations, consider removing it:- apiGroups: - "helm.toolkit.fluxcd.io" resources: - helmreleases verbs: - get - list - - watch - patch - deletepackages/apps/tenant/templates/cleanup-job.yaml (2)
70-85: Improve error handling and clarify the two-phase HelmRelease deletion strategy.The cleanup script lacks explicit error handling and logging similar to the concern in file 1. Additionally, the script deletes HelmReleases in two cohorts (Applications and Tenant Modules). Clarify:
- Why are they split into two cohorts? (e.g., ordering dependency, different deletion semantics)
- Is there any risk of the second cohort failing if the first succeeds?
- Should both cohorts run even if the first fails, or should the script halt on first failure?
Consider adding error handling:
- /bin/bash - -c - | set -e NAMESPACE="{{ include "tenant.name" . }}" echo "Cleaning up HelmReleases in namespace: $NAMESPACE" echo "Deleting Applications" - kubectl delete helmreleases.helm.toolkit.fluxcd.io -n "$NAMESPACE" \ + kubectl delete helmreleases.helm.toolkit.fluxcd.io -n "$NAMESPACE" \ -l 'cozystack.io/ui=true,internal.cozystack.io/tenantmodule!=true' \ - --ignore-not-found=true --wait=true + --ignore-not-found=true --wait=true || echo "Warning: Failed to delete Applications" echo "Deleting Tenant Modules" - kubectl delete helmreleases.helm.toolkit.fluxcd.io -n "$NAMESPACE" \ + kubectl delete helmreleases.helm.toolkit.fluxcd.io -n "$NAMESPACE" \ -l 'cozystack.io/ui=true,internal.cozystack.io/tenantmodule=true' \ - --ignore-not-found=true --wait=true + --ignore-not-found=true --wait=true || echo "Warning: Failed to delete Tenant Modules" echo "Cleanup completed successfully"This logs warnings if either cohort deletion fails while still completing cleanup.
43-52: Verify pre-delete hook execution ordering across kubernetes and tenant cleanup jobs.File 1 (kubernetes/delete.yaml) defines a pre-delete Job with weights: ServiceAccount (0), Role (5), RoleBinding (5), Job (10).
File 2 (tenant/cleanup-job.yaml) defines a pre-delete Job with weights: ServiceAccount/Role/RoleBinding (-5), Job (0).
Lower weights execute first in Helm hooks, so tenant cleanup (weight -5 Job) runs before kubernetes cleanup (weight 10 Job). Verify this ordering is intentional:
- If tenant cleanup must complete before cluster cleanup, this is correct.
- If cluster cleanup should occur first, adjust tenant Job weight to be higher than 10.
- If there's no strict dependency, document this to prevent future confusion.
Also confirm that the tenant namespace is still accessible when kubernetes-level cleanup (which deletes control planes) runs.
Document the hook weight rationale in comments or a separate ADR explaining why tenant cleanup (weight 0) runs before kubernetes cleanup (weight 10).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
packages/apps/kubernetes/templates/delete.yaml(4 hunks)packages/apps/kubernetes/templates/helmreleases/delete.yaml(0 hunks)packages/apps/tenant/templates/cleanup-job.yaml(1 hunks)
💤 Files with no reviewable changes (1)
- packages/apps/kubernetes/templates/helmreleases/delete.yaml
🧰 Additional context used
🪛 YAMLlint (1.37.1)
packages/apps/tenant/templates/cleanup-job.yaml
[error] 5-5: syntax error: expected , but found ''
(syntax)
packages/apps/kubernetes/templates/delete.yaml
[error] 9-9: syntax error: expected , but found ''
(syntax)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Build
🔇 Additional comments (1)
packages/apps/kubernetes/templates/delete.yaml (1)
12-14: The policy label correctly enables Cilium network policy for required API server access.The
policy.cozystack.io/allow-to-apiserver: "true"label controls a Cilium NetworkPolicy (defined inpackages/apps/tenant/templates/networkpolicy.yamllines 136-145) that permits egress to the kube-apiserver. This cleanup job legitimately requires API access to execute multiple kubectl operations: suspending/deleting HelmReleases, deleting KamajiControlPlane and TenantControlPlane resources, cleaning up DataVolumes, and removing LoadBalancer Services. The label aligns with the security posture by combining explicit RBAC permissions (Role with scoped verbs) with network-level access control (Cilium policy selector).
| - | | ||
| set -e | ||
| echo "Step 1: Suspending all HelmReleases with label cozystack.io/target-cluster-name={{ .Release.Name }}" | ||
| for hr in $(kubectl -n {{ .Release.Namespace }} get helmreleases.helm.toolkit.fluxcd.io -l "cozystack.io/target-cluster-name={{ .Release.Name }}" -o name 2>/dev/null || true); do | ||
| if [ -n "$hr" ]; then | ||
| echo " Suspending $hr" | ||
| kubectl -n {{ .Release.Namespace }} patch "$hr" \ | ||
| -p '{"spec": {"suspend": true}}' \ | ||
| --type=merge --field-manager=flux-client-side-apply | ||
| fi | ||
| done | ||
|
|
||
| kubectl -n {{ .Release.Namespace }} delete services | ||
| -l "cluster.x-k8s.io/cluster-name={{ .Release.Name }}" | ||
| --field-selector spec.type=LoadBalancer | ||
| --ignore-not-found=true | ||
| echo "Step 2: Deleting HelmReleases with label cozystack.io/target-cluster-name={{ .Release.Name }}" | ||
| kubectl -n {{ .Release.Namespace }} delete helmreleases.helm.toolkit.fluxcd.io \ | ||
| -l "cozystack.io/target-cluster-name={{ .Release.Name }}" \ | ||
| --ignore-not-found=true --wait=true | ||
|
|
||
| echo "Step 3: Deleting KamajiControlPlane {{ .Release.Name }}" | ||
| kubectl -n {{ .Release.Namespace }} delete kamajicontrolplanes.controlplane.cluster.x-k8s.io {{ .Release.Name }} \ | ||
| --ignore-not-found=true | ||
|
|
||
| echo "Step 4: Deleting TenantControlPlane {{ .Release.Name }}" | ||
| kubectl -n {{ .Release.Namespace }} delete tenantcontrolplanes.kamaji.clastix.io {{ .Release.Name }} \ | ||
| --ignore-not-found=true | ||
|
|
||
| echo "Step 5: Cleaning up DataVolumes" | ||
| kubectl -n {{ .Release.Namespace }} delete datavolumes \ | ||
| -l "cluster.x-k8s.io/cluster-name={{ .Release.Name }}" \ | ||
| --ignore-not-found=true | ||
|
|
||
| echo "Step 6: Cleaning up LoadBalancer Services" | ||
| kubectl -n {{ .Release.Namespace }} delete services \ | ||
| -l "cluster.x-k8s.io/cluster-name={{ .Release.Name }}" \ | ||
| --field-selector spec.type=LoadBalancer \ | ||
| --ignore-not-found=true | ||
|
|
||
| echo "Cleanup completed successfully" |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Improve error handling and logging in the cleanup script.
The shell script uses set -e for fail-fast behavior, but lacks explicit error context. If any step fails mid-execution (e.g., during the HelmRelease suspension loop), subsequent steps are skipped without detailed indication of what failed. Add error handling to:
- Capture and report the specific step/resource that failed
- Ensure idempotence even if individual commands partially fail (some succeed, some don't)
- Log which resources were successfully cleaned vs. which were skipped/failed
Consider using || echo "...failed (continuing)" || true for non-critical steps, or wrap with explicit error recovery logic.
Consider this refactored approach:
- /bin/sh
- -c
- |
set -e
NAMESPACE="{{ .Release.Namespace }}"
CLUSTER_NAME="{{ .Release.Name }}"
FAILED=false
echo "Step 1: Suspending all HelmReleases with label cozystack.io/target-cluster-name=$CLUSTER_NAME"
if kubectl -n "$NAMESPACE" get helmreleases.helm.toolkit.fluxcd.io \
-l "cozystack.io/target-cluster-name=$CLUSTER_NAME" -o name 2>/dev/null | grep . &>/dev/null; then
kubectl -n "$NAMESPACE" get helmreleases.helm.toolkit.fluxcd.io \
-l "cozystack.io/target-cluster-name=$CLUSTER_NAME" -o name | while read hr; do
echo " Suspending $hr"
kubectl -n "$NAMESPACE" patch "$hr" \
-p '{"spec": {"suspend": true}}' \
--type=merge --field-manager=flux-client-side-apply || { echo " Failed to suspend $hr"; FAILED=true; }
done
else
echo " No HelmReleases found"
fi
echo "Step 2: Deleting HelmReleases with label cozystack.io/target-cluster-name=$CLUSTER_NAME"
kubectl -n "$NAMESPACE" delete helmreleases.helm.toolkit.fluxcd.io \
-l "cozystack.io/target-cluster-name=$CLUSTER_NAME" \
--ignore-not-found=true --wait=true || { echo " Failed to delete HelmReleases"; FAILED=true; }
# ... continue for remaining steps ...
if [ "$FAILED" = true ]; then
echo "Cleanup completed with errors"
exit 1
else
echo "Cleanup completed successfully"
fiThis approach:
- Uses variables to avoid template rendering on every line
- Checks if resources exist before attempting operations
- Tracks failures but allows subsequent steps to execute
- Provides clear per-step error reporting
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In packages/apps/kubernetes/templates/delete.yaml around lines 30-66, the
Helm/cleanup shell block uses set -e and has no per-step error context or
recovery which aborts the whole cleanup on first failure and hides which
resources succeeded or failed; update the script to assign .Release.Name and
.Release.Namespace to local variables once, remove brittle global set -e, wrap
each logical step (suspend HelmReleases, delete HelmReleases, delete
KamajiControlPlane, delete TenantControlPlane, delete DataVolumes, delete
LoadBalancer Services) in a small handler that: checks for existence before
acting, runs the kubectl command but captures its exit code, logs a clear
success or failure message including resource type/name and step number, records
failures in a cumulative flag or list, and continues to the next step; at the
end, print a summary of succeeded vs failed steps and exit with non-zero only if
any critical failures recorded (or zero if all non-critical), ensuring
idempotence by using --ignore-not-found where appropriate and using || true only
for explicitly non-fatal commands.
There was a problem hiding this comment.
Not relevant we need these errors
There was a problem hiding this comment.
Andrei Kvapil (@kvaps), understood! I'll keep the current fail-fast behavior with set -e. The cleanup will halt immediately on any error, ensuring failures are visible and require attention.
| restartPolicy: OnFailure | ||
| containers: | ||
| - name: cleanup | ||
| image: bitnami/kubectl:latest |
There was a problem hiding this comment.
Pin the container image to a specific version.
Line 65 uses bitnami/kubectl:latest, which is not reproducible and will pull different image versions across deployments. Replace with a specific version tag to ensure consistency.
- image: bitnami/kubectl:latest
+ image: bitnami/kubectl:1.32.0Verify the available versions and compatibility with your Kubernetes clusters. For pinning best practices, consider using image digests (SHA256) for maximum immutability, though version tags are also acceptable.
🤖 Prompt for AI Agents
In packages/apps/tenant/templates/cleanup-job.yaml around line 65, the container
image is pinned to the non-reproducible tag "bitnami/kubectl:latest"; change
this to a specific version tag or, ideally, an image digest (sha256) to ensure
consistent, immutable deployments; update the image field to a chosen stable tag
(e.g., bitnami/kubectl:<version>) or to the digest form
(bitnami/kubectl@sha256:<digest>), verify the tag/digest exists and is
compatible with your cluster, and update any documentation or deployment CI that
relied on "latest".
There was a problem hiding this comment.
we use it in many cases like that, let's fix it in other PR
There was a problem hiding this comment.
Andrei Kvapil (@kvaps), understood! Since this pattern exists in multiple places across the codebase, would you like me to create a GitHub issue to track the work of pinning container image versions? That way it won't be forgotten and can be addressed systematically in a follow-up PR.
…nges Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
…nges (#1662) ## What this PR does This PR adds config hash annotations to the dashboard deployment templates to ensure pods are automatically restarted when their configuration changes. Changes: - Added `checksum/config` annotation to `nginx.yaml` deployment that computes a hash of `nginx-config.yaml` - Added `checksum/config` annotation to `web.yaml` deployment that computes a hash of `configmap.yaml` This replaces the hardcoded checksum in nginx.yaml with a dynamically computed one, and adds a new checksum for the web deployment. ### Release note ```release-note [dashboard] Add config hash annotations to automatically restart pods when configuration changes ```
|
Backport failed because this pull request contains merge commits. You can either backport this pull request manually, or configure the action to skip merge commits. |
Signed-off-by: Andrei Kvapil <kvapss@gmail.com> <!-- Thank you for making a contribution! Here are some tips for you: - Start the PR title with the [label] of Cozystack component: - For system components: [platform], [system], [linstor], [cilium], [kube-ovn], [dashboard], [cluster-api], etc. - For managed apps: [apps], [tenant], [kubernetes], [postgres], [virtual-machine] etc. - For development and maintenance: [tests], [ci], [docs], [maintenance]. - If it's a work in progress, consider creating this PR as a draft. - Don't hesistate to ask for opinion and review in the community chats, even if it's still a draft. - Add the label `backport` if it's a bugfix that needs to be backported to a previous version. --> ## What this PR does ### Release note <!-- Write a release note: - Explain what has changed internally and for users. - Start with the same [label] as in the PR title - Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md. --> ```release-note [tenant][kubernetes] Introduce better cleanup logic ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an automated pre-delete cleanup job for tenant namespaces to remove tenant-related releases during uninstall. * **Improvements** * Cleanup now runs early in the uninstall sequence with a clearer, stepwise orchestration for resource removal. * Expanded permissions and execution allowances to enable the cleanup workflow. * Deployment annotations now use a content checksum to better detect config changes. * **Removals** * Previous teardown sequence for certain release types was removed and replaced by the new workflow. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Signed-off-by: Andrei Kvapil <kvapss@gmail.com> <!-- Thank you for making a contribution! Here are some tips for you: - Start the PR title with the [label] of Cozystack component: - For system components: [platform], [system], [linstor], [cilium], [kube-ovn], [dashboard], [cluster-api], etc. - For managed apps: [apps], [tenant], [kubernetes], [postgres], [virtual-machine] etc. - For development and maintenance: [tests], [ci], [docs], [maintenance]. - If it's a work in progress, consider creating this PR as a draft. - Don't hesistate to ask for opinion and review in the community chats, even if it's still a draft. - Add the label `backport` if it's a bugfix that needs to be backported to a previous version. --> ## What this PR does ### Release note <!-- Write a release note: - Explain what has changed internally and for users. - Start with the same [label] as in the PR title - Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md. --> ```release-note [tenant][kubernetes] Introduce better cleanup logic ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an automated pre-delete cleanup job for tenant namespaces to remove tenant-related releases during uninstall. * **Improvements** * Cleanup now runs early in the uninstall sequence with a clearer, stepwise orchestration for resource removal. * Expanded permissions and execution allowances to enable the cleanup workflow. * Deployment annotations now use a content checksum to better detect config changes. * **Removals** * Previous teardown sequence for certain release types was removed and replaced by the new workflow. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Signed-off-by: Andrei Kvapil kvapss@gmail.com
What this PR does
Release note
Summary by CodeRabbit
New Features
Improvements
Removals
✏️ Tip: You can customize this high-level summary in your review settings.