Skip to content

feat(gitops): show ArgoCD ApplicationSets in the fleet view - #1609

Merged
hisco merged 1 commit into
skyhook-io:mainfrom
jfillman:feature/gitops-applicationset-picklist
Sep 4, 2026
Merged

hisco merged 1 commit into
skyhook-io:mainfrom
jfillman:feature/gitops-applicationset-picklist

Conversation

@jfillman

@jfillman jfillman commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

The GitOps fleet view only ever fetched Applications, Kustomizations, and HelmReleases as table rows — ApplicationSet was already a documented "Supported CRD" with working detail-page routing, but never appeared as a row of its own, only implicitly through the Applications it generates.

This unions ApplicationSet rows into the same Applications-mode table (the same pattern already used for Kustomization/HelmRelease coexisting there), behind a new Kind filter (Applications / ApplicationSets) that only appears once a cluster actually has ApplicationSets.

ApplicationSet has no sync/health of its own in the sense the table shows for Applications — its status.conditions use a different vocabulary (ErrorOccurred, ParametersGenerated, ResourcesUpToDate, never Ready) — so it correctly falls through the existing generic status mapper to Unknown/Unknown rather than inventing a fleet-health rollup that doesn't exist for this kind.

Test plan

  • Existing GitOps table test coverage extended for the new filter
  • Manually verified the Kind filter stays hidden on clusters with zero ApplicationSets

Note

Low Risk
Read-only list fetch and UI filtering/normalization; no auth or mutation paths changed beyond including another CRD in the existing GitOps query.

Overview
Argo CD ApplicationSets now appear as first-class rows in the GitOps fleet table alongside Applications, Kustomizations, and HelmReleases. The web layer fetches applicationsets when the CRD is present, maps them through a new normalizeArgoApplicationSet helper (template destination, best-effort git-generator source hints, empty project/lastSync/autoSync; sync/health stay Unknown via existing status mapping), and wires detail normalization the same way.

The shared GitOpsTableView adds a URL-backed Kind facet (Applications / ApplicationSets) that only renders when at least one ApplicationSet exists. The filter narrows Argo Application/ApplicationSet rows only and leaves Flux kinds untouched. normalizeArgoApplicationSet is re-exported from @skyhook-io/k8s-ui.

Reviewed by Cursor Bugbot for commit d5e23fd. Bugbot is set up for automated code reviews on this repo. Configure here.

The GitOps fleet view only ever fetched Applications, Kustomizations,
and HelmReleases as table rows — ApplicationSet was a documented
"Supported CRD" with working detail-page routing, but never appeared
as a row of its own, only implicitly via the Applications it generates.

Union ApplicationSet rows into the same Applications-mode table (same
pattern already used for Kustomization/HelmRelease coexisting there),
behind a new Kind filter (Applications / ApplicationSets) that only
appears once a cluster actually has ApplicationSets. ApplicationSet has
no sync/health of its own in the sense the table shows for Applications
— its own status.conditions use a different vocabulary (ErrorOccurred,
ParametersGenerated, ResourcesUpToDate, never Ready) — so it correctly
falls through the existing generic status mapper to Unknown/Unknown
rather than us inventing a fleet-health rollup that doesn't exist.
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Show Argo CD ApplicationSets in the GitOps fleet view

✨ Enhancement 🕐 20-40 Minutes

Grey Divider

AI Description

• Fetch and display Argo CD ApplicationSets alongside existing GitOps application rows.
• Add a conditional Argo kind filter without affecting Flux resources.
• Preserve Unknown statuses while exposing generator source and destination hints.
Diagram

sequenceDiagram
  participant D as API Discovery
  participant V as GitOps View
  participant A as Resource API
  participant N as Row Normalizer
  participant T as Fleet Table
  V->>D: Check ApplicationSet CRD
  alt CRD available
    D-->>V: Supported
    V->>A: Fetch ApplicationSets
    A-->>V: Raw resources
    V->>N: Normalize resources
    N-->>T: ApplicationSet rows
    T->>T: Apply kind filter
  else CRD absent
    D-->>V: Unsupported
    V-->>T: Keep facet hidden
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Dedicated ApplicationSets view
  • ➕ Allows columns and status semantics tailored specifically to generators.
  • ➕ Separates deployable Applications from ApplicationSet definitions.
  • ➖ Adds another navigation mode and fragments the existing GitOps fleet.
  • ➖ Duplicates table, filtering, and resource-loading behavior.
2. Display generated Applications only
  • ➕ Retains meaningful Application sync and health statuses.
  • ➕ Requires no additional row model or filtering behavior.
  • ➖ ApplicationSet definitions remain undiscoverable in the fleet.
  • ➖ Users cannot navigate directly to generator resources.
3. Map ApplicationSet conditions to fleet health
  • ➕ Could surface generator failures more prominently.
  • ➕ Provides richer status information than Unknown values.
  • ➖ ApplicationSet conditions do not directly correspond to sync and health semantics.
  • ➖ A synthetic mapping could mislead users without a separately designed status model.

Recommendation: The PR's shared-table approach is preferable because it makes ApplicationSets discoverable while reusing established GitOps fleet behavior. Conditional API discovery avoids unsupported requests, the scoped kind facet preserves Flux rows, and Unknown status values avoid inventing misleading health semantics.

Files changed (3) +99 / -3

Enhancement (3) +99 / -3
GitOpsTableView.tsxNormalize and filter ApplicationSet fleet rows +91/-2

Normalize and filter ApplicationSet fleet rows

• Adds ApplicationSet-to-GitOpsRow normalization with generator source and template destination hints. Introduces a URL-backed Argo kind facet that appears only when ApplicationSets exist and leaves Flux rows unaffected.

packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx

index.tsExport the ApplicationSet normalizer +1/-0

Export the ApplicationSet normalizer

• Re-exports 'normalizeArgoApplicationSet' through the shared GitOps package entry point for host applications.

packages/k8s-ui/src/components/gitops/index.ts

GitOpsView.tsxFetch ApplicationSets for fleet and detail views +7/-1

Fetch ApplicationSets for fleet and detail views

• Detects ApplicationSet API availability, conditionally fetches resources alongside other GitOps kinds, and adds normalized rows to the fleet. Detail-resource normalization now also recognizes ApplicationSets.

web/src/components/gitops/GitOpsView.tsx

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Application-only actions exposed 🐞 Bug ≡ Correctness
Description
ApplicationSet rows are marked only as tool: 'argo', so the shared row menu exposes Sync, Refresh,
Hard refresh, Suspend/Resume, and Terminate actions intended for Applications. The OSS handler sends
those operations to Application mutation endpoints using the ApplicationSet's namespace and name,
causing failed requests or potentially targeting a same-named Application.
Code

packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[R1988-1990]

+    tool: 'argo',
+    kindName: 'applicationsets',
+    kind: 'ApplicationSet',
Relevance

●●● Strong

ApplicationSet rows sharing Application action menus creates a concrete mutation correctness risk;
team accepts analogous GitOps action fixes.

PR-#815
PR-#719

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new normalizer identifies ApplicationSets as Argo rows, while the table builds the complete
Application action menu for every Argo row. The OSS action dispatcher then invokes
Application-specific mutations without passing the row kind; the detail view demonstrates the
intended kind check by enabling Argo handlers only for applications.

packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1408-1479]
packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1986-1990]
web/src/components/gitops/GitOpsView.tsx[262-296]
web/src/components/gitops/GitOpsView.tsx[499-504]
web/src/components/gitops/GitOpsView.tsx[572-590]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ApplicationSet rows inherit the complete Argo Application action menu because action selection checks only `row.tool`. These operations use Application-specific mutations and are invalid for ApplicationSets.

## Issue Context
The detail page already distinguishes Applications using `kind === 'applications'`. Apply the same distinction to table row actions, returning no Application mutation actions for ApplicationSets unless dedicated ApplicationSet operations are implemented.

## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1408-1479]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1986-1990]
- web/src/components/gitops/GitOpsView.tsx[262-296]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Active kind filter disappears 🐞 Bug ≡ Correctness
Description
The Kind section is hidden whenever the current rows contain no ApplicationSets, even though a
persisted kind URL filter remains active and continues filtering Applications. After switching
cluster or namespace with kind=ApplicationSet, users can therefore see Applications disappear
without any visible facet indicating or directly disabling the responsible filter.
Code

packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[R971-975]

+        {kindCounts.ApplicationSet > 0 && (
+          <GitOpsFilterSection icon={Layers} title="Kind">
+            <GitOpsFacetButton label="Applications" count={kindCounts.Application} active={kindFilters.has('Application')} onClick={() => onToggleKind('Application')} />
+            <GitOpsFacetButton label="ApplicationSets" count={kindCounts.ApplicationSet} active={kindFilters.has('ApplicationSet')} onClick={() => onToggleKind('ApplicationSet')} />
+          </GitOpsFilterSection>
Relevance

●●● Strong

Accepted GitOps filter correctness precedents support preserving controls for active filters; hidden
persisted facets are directly misleading.

PR-#719
PR-#802

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Kind state is decoded from persistent filter state and applied whenever non-empty, but the only
controls representing that state render solely when the unfiltered ApplicationSet count is positive.
The active-filter calculation confirms the hidden state remains active rather than being discarded.

packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[337-347]
packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[367-376]
packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[484-487]
packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[971-976]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A persisted kind filter remains effective when the Kind facet is hidden because the current data has no ApplicationSets. This silently removes Argo Application rows after cluster, namespace, or URL changes.

## Issue Context
Render the Kind section whenever ApplicationSets exist or `kindFilters` is non-empty. Alternatively, clear or validate the kind filter when ApplicationSets are unavailable, while avoiding URL-state changes during transient loading.

## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[345-347]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[484-487]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[971-976]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. ApplicationSets counted as manual 🐞 Bug ≡ Correctness
Description
normalizeArgoApplicationSet assigns autoSync: false, causing every ApplicationSet to inflate the
Manual automation count and pass the Manual sync-policy filter. ApplicationSets do not themselves
have the Application sync policy represented by this facet, so the count and filtered results are
incorrect.
Code

packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[R2012-2014]

+    lastSync: '',
+    autoSync: false,
+    terminating: isTerminating(resource),
Relevance

●●● Strong

Automation counts and filtering incorrectly classify ApplicationSets; this conflicts with the PR’s
stated absence of Application sync semantics.

PR-#719
PR-#1422

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Automation counts classify every false autoSync value as Manual, and the automation predicate
likewise includes all such rows when Manual is selected. The new ApplicationSet normalizer supplies
false despite the PR's own model treating ApplicationSets as generators without Application-level
sync semantics.

packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[427-431]
packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[477-481]
packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1968-1975]
packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[2004-2014]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ApplicationSets are represented as manually synced because the required boolean `autoSync` field is set to false. This makes an inapplicable policy appear as a real Manual policy in counts and filtering.

## Issue Context
Exclude ApplicationSet rows from automation counts and predicates, or make automation applicability explicit in the row model rather than encoding “not applicable” as false. Preserve existing behavior for Applications and Flux workloads.

## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[427-431]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[477-481]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[2012-2014]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 41 rules
Review mode: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 18/18, lines 102/200; both must reach the floor). Router rationale: This adds a new CRD across fetching, normalization, detail routing, and filtering, creating several independent integration points where subtle defects could be missed in a single pass.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +1988 to +1990
tool: 'argo',
kindName: 'applicationsets',
kind: 'ApplicationSet',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Application-only actions exposed 🐞 Bug ≡ Correctness

ApplicationSet rows are marked only as tool: 'argo', so the shared row menu exposes Sync, Refresh,
Hard refresh, Suspend/Resume, and Terminate actions intended for Applications. The OSS handler sends
those operations to Application mutation endpoints using the ApplicationSet's namespace and name,
causing failed requests or potentially targeting a same-named Application.
Agent Prompt
## Issue description
ApplicationSet rows inherit the complete Argo Application action menu because action selection checks only `row.tool`. These operations use Application-specific mutations and are invalid for ApplicationSets.

## Issue Context
The detail page already distinguishes Applications using `kind === 'applications'`. Apply the same distinction to table row actions, returning no Application mutation actions for ApplicationSets unless dedicated ApplicationSet operations are implemented.

## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1408-1479]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[1986-1990]
- web/src/components/gitops/GitOpsView.tsx[262-296]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +971 to +975
{kindCounts.ApplicationSet > 0 && (
<GitOpsFilterSection icon={Layers} title="Kind">
<GitOpsFacetButton label="Applications" count={kindCounts.Application} active={kindFilters.has('Application')} onClick={() => onToggleKind('Application')} />
<GitOpsFacetButton label="ApplicationSets" count={kindCounts.ApplicationSet} active={kindFilters.has('ApplicationSet')} onClick={() => onToggleKind('ApplicationSet')} />
</GitOpsFilterSection>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Active kind filter disappears 🐞 Bug ≡ Correctness

The Kind section is hidden whenever the current rows contain no ApplicationSets, even though a
persisted kind URL filter remains active and continues filtering Applications. After switching
cluster or namespace with kind=ApplicationSet, users can therefore see Applications disappear
without any visible facet indicating or directly disabling the responsible filter.
Agent Prompt
## Issue description
A persisted kind filter remains effective when the Kind facet is hidden because the current data has no ApplicationSets. This silently removes Argo Application rows after cluster, namespace, or URL changes.

## Issue Context
Render the Kind section whenever ApplicationSets exist or `kindFilters` is non-empty. Alternatively, clear or validate the kind filter when ApplicationSets are unavailable, while avoiding URL-state changes during transient loading.

## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[345-347]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[484-487]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[971-976]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +2012 to +2014
lastSync: '',
autoSync: false,
terminating: isTerminating(resource),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Applicationsets counted as manual 🐞 Bug ≡ Correctness

normalizeArgoApplicationSet assigns autoSync: false, causing every ApplicationSet to inflate the
Manual automation count and pass the Manual sync-policy filter. ApplicationSets do not themselves
have the Application sync policy represented by this facet, so the count and filtered results are
incorrect.
Agent Prompt
## Issue description
ApplicationSets are represented as manually synced because the required boolean `autoSync` field is set to false. This makes an inapplicable policy appear as a real Manual policy in counts and filtering.

## Issue Context
Exclude ApplicationSet rows from automation counts and predicates, or make automation applicability explicit in the row model rather than encoding “not applicable” as false. Preserve existing behavior for Applications and Flux workloads.

## Fix Focus Areas
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[427-431]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[477-481]
- packages/k8s-ui/src/components/gitops/GitOpsTableView.tsx[2012-2014]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d5e23fd. Configure here.

<GitOpsFacetButton label="ApplicationSets" count={kindCounts.ApplicationSet} active={kindFilters.has('ApplicationSet')} onClick={() => onToggleKind('ApplicationSet')} />
</GitOpsFilterSection>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ApplicationSet rows expose Application actions

Medium Severity

ApplicationSet rows set tool to argo, so the table action menu offers Sync, Refresh, Hard refresh, and Suspend. Those mutations call the Application APIs with the ApplicationSet name. The detail page already gates the same actions with kind === 'applications'. A same-named Application in that namespace can be mutated by mistake; otherwise the request fails.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d5e23fd. Configure here.

hisco added a commit that referenced this pull request Sep 4, 2026
…1628)

## Summary

Two places in the GitOps fleet code treat "this row is an Argo object"
as "this row is an Argo Application". That holds today only because
Applications are the only Argo kind that becomes a row. It stops holding
the moment a second one does, and #1609 adds ApplicationSets.

### Row actions were gated on the tool, not the kind

`buildRowActionItems` offered Sync / Refresh / Hard refresh / Suspend to
any row with `tool === 'argo'`. All four post to
`/argo/applications/{namespace}/{name}`, and the handler resolves that
name against the Application GVR.

So on a non-Application Argo row those buttons either 404, or — when an
Application happens to share the name, which Kubernetes permits since
they are different resource types — quietly mutate that unrelated
Application and report success.

Gated on `kindName === 'applications'` instead, which is how the detail
page already gates the same actions (`isArgoApp`). `RowActionMenu` now
renders nothing for an empty item list, so a kind with no actions gets
no dead three-dot trigger.

### ApplicationSet status went through the Flux condition reader

`getGitOpsResourceStatus` special-cased `applications` and sent
everything else to `fluxConditionsToGitOpsStatus`, which looks for
`Ready` / `Reconciling` / `Stalled`.

An ApplicationSet reports through `ErrorOccurred` /
`ParametersGenerated` / `ResourcesUpToDate` and never sets `Ready`, so
every ApplicationSet bottomed out at Unknown — a generator failing
outright read exactly like a healthy one. This is live today on the
ApplicationSet detail page, independent of #1609.

Added a reader for those conditions. Sync stays Unknown in all cases:
Sync answers "is the declared state applied", and an ApplicationSet
applies nothing — it generates Applications, each carrying its own sync
state.

## Test plan

- 6 unit tests on the new mapper covering failed / healthy / no-status,
that Sync stays Unknown either way, and that `ErrorOccurred=False` is
not treated as a fault
- `packages/k8s-ui`: 3387 passed
- `web`: 569 passed, `tsc` clean, lint 0 errors
- Verified against a live `kind` cluster (Argo CD 2.13.2, the
`scripts/gitops-demo.sh` fixtures plus an ApplicationSet with an
unreachable git generator), built with #1609 merged on top:
- before: both ApplicationSets Unknown/Unknown; Suspend on one of them
suspended a same-named Application and returned 200
- after: the failing generator reads Degraded and sorts to the top, the
healthy one reads Healthy, Sync stays Unknown, and neither row offers an
action menu

The action gate is a no-op on `main` as it stands, since Applications
are the only Argo rows today. The status reader is not — it fixes the
ApplicationSet detail page now.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes how GitOps row mutations are offered (prevents mistaken
Application API calls on non-Application Argo rows) and how
ApplicationSet status is shown in detail/list surfaces; UI-only but
affects operator triage and action safety.
> 
> **Overview**
> **GitOps fleet table** now limits Argo **Sync / Refresh / Suspend**
(and related) actions to rows with `kindName === 'applications'`,
matching the detail page. Other Argo kinds (e.g. ApplicationSets) no
longer get menus that POST to `/argo/applications/...` and could 404 or
hit a same-named Application. Flux reconcile actions are explicitly
limited to `tool === 'flux'`. **`RowActionMenu`** renders nothing when
there are no items, so rows without actions do not show a useless ⋮
control.
> 
> **ApplicationSet health/sync** is no longer derived via the Flux
`Ready` condition path. **`getGitOpsResourceStatus`** routes
`applicationsets` through a new
**`argoApplicationSetConditionsToGitOpsStatus`** mapper
(`ErrorOccurred`, `ResourcesUpToDate`, etc.): failed generators show
**Degraded** with the controller message, healthy generators
**Healthy**, and **sync stays Unknown** because ApplicationSets do not
apply cluster state themselves. Unit tests cover these cases.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
37d9fa5. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@hisco
hisco merged commit 25feaa3 into skyhook-io:main Sep 4, 2026
9 checks passed
hisco added a commit that referenced this pull request Sep 4, 2026
Follow-up to #1609. Several fields still described an ApplicationSet
using values that only mean something for an Application.

Source, project and destination came from a git generator or were left
blank. A git generator's repo is scanned to produce parameters and is
often not a deployment source; what the generated Applications deploy is
on spec.template.spec. Read the template first and fall back to the
generator. Template values can carry per-app placeholders ({{path}},
{{server}}), so a value is surfaced only when it is literal - rendering
the raw placeholder is worse than rendering nothing.

Last Sync fell through to the creation timestamp, dating the object
rather than reporting activity on it. It now carries the transition time
of the condition the status was read from.

The Automation (sync policy) facet counted every ApplicationSet as
Manual, answering 'what is not auto-syncing' with objects that never
sync. Skip the kind in the counts and the predicate. describeArgoAutoSync
had the same problem server-side: an ApplicationSet's spec.syncPolicy
governs how generated Applications are created and deleted and never
carries 'automated', so it reported Manual to the detail header, the
insights summary and the rollback gating alike. Fixed there rather than
at one call site.

The Kind section rendered only while ApplicationSets were present, but
the filter applied regardless, so scoping to a namespace without any hid
every Application with no facet on screen to explain it. Keep the section
while a kind filter is set. noOtherFiltersActive did not account for it
either, leaving the Total tile reading as unfiltered.

An empty destination means in-cluster for an Application, Argo's default
when unset. For an ApplicationSet it means the template was templated
away, so the detail header no longer makes that claim.

Source fields resolve as a unit rather than independently. A literal
template repo paired with a git generator's path named a tree that
existed in neither, which reads as a real location and is not one.

The destination namespace no longer falls back to the row's own
namespace for an ApplicationSet. That fallback suits a resource sitting
beside what it deploys; an ApplicationSet sits in the controller
namespace and generates Applications that land elsewhere.
hisco added a commit that referenced this pull request Sep 4, 2026
Follow-up to #1609. Several fields still described an ApplicationSet
using values that only mean something for an Application.

Source, project and destination came from a git generator or were left
blank. A git generator's repo is scanned to produce parameters and is
often not a deployment source; what the generated Applications deploy is
on spec.template.spec. Read the template first and fall back to the
generator. Template values can carry per-app placeholders ({{path}},
{{server}}), so a value is surfaced only when it is literal - rendering
the raw placeholder is worse than rendering nothing.

Last Sync fell through to the creation timestamp, dating the object
rather than reporting activity on it. It now carries the transition time
of the condition the status was read from.

The Automation (sync policy) facet counted every ApplicationSet as
Manual, answering 'what is not auto-syncing' with objects that never
sync. Skip the kind in the counts and the predicate. describeArgoAutoSync
had the same problem server-side: an ApplicationSet's spec.syncPolicy
governs how generated Applications are created and deleted and never
carries 'automated', so it reported Manual to the detail header, the
insights summary and the rollback gating alike. Fixed there rather than
at one call site.

The Kind section rendered only while ApplicationSets were present, but
the filter applied regardless, so scoping to a namespace without any hid
every Application with no facet on screen to explain it. Keep the section
while a kind filter is set. noOtherFiltersActive did not account for it
either, leaving the Total tile reading as unfiltered.

An empty destination means in-cluster for an Application, Argo's default
when unset. For an ApplicationSet it means the template was templated
away, so the detail header no longer makes that claim.

Source fields resolve as a unit rather than independently. A literal
template repo paired with a git generator's path named a tree that
existed in neither, which reads as a real location and is not one.

The destination namespace no longer falls back to the row's own
namespace for an ApplicationSet. That fallback suits a resource sitting
beside what it deploys; an ApplicationSet sits in the controller
namespace and generates Applications that land elsewhere.
hisco added a commit that referenced this pull request Sep 4, 2026
Follow-up to #1609. Several fields still described an ApplicationSet
using values that only mean something for an Application.

Source, project and destination came from a git generator or were left
blank. A git generator's repo is scanned to produce parameters and is
often not a deployment source; what the generated Applications deploy is
on spec.template.spec. Read the template first and fall back to the
generator. Template values can carry per-app placeholders ({{path}},
{{server}}), so a value is surfaced only when it is literal - rendering
the raw placeholder is worse than rendering nothing.

Last Sync fell through to the creation timestamp, dating the object
rather than reporting activity on it. It now carries the transition time
of the condition the status was read from.

The Automation (sync policy) facet counted every ApplicationSet as
Manual, answering 'what is not auto-syncing' with objects that never
sync. Skip the kind in the counts and the predicate. describeArgoAutoSync
had the same problem server-side: an ApplicationSet's spec.syncPolicy
governs how generated Applications are created and deleted and never
carries 'automated', so it reported Manual to the detail header, the
insights summary and the rollback gating alike. Fixed there rather than
at one call site.

The Kind section rendered only while ApplicationSets were present, but
the filter applied regardless, so scoping to a namespace without any hid
every Application with no facet on screen to explain it. Keep the section
while a kind filter is set. noOtherFiltersActive did not account for it
either, leaving the Total tile reading as unfiltered.

An empty destination means in-cluster for an Application, Argo's default
when unset. For an ApplicationSet it means the template was templated
away, so the detail header no longer makes that claim.

Source fields resolve as a unit rather than independently. A literal
template repo paired with a git generator's path named a tree that
existed in neither, which reads as a real location and is not one.

The destination namespace no longer falls back to the row's own
namespace for an ApplicationSet. That fallback suits a resource sitting
beside what it deploys; an ApplicationSet sits in the controller
namespace and generates Applications that land elsewhere.

Documents the Kind filter in docs/gitops.md, which lists the fleet
view's filters and had not been updated when the facet shipped, and
rewrites the filter's own comment to describe what it does rather than
why its scope was drawn where it was.
hisco added a commit that referenced this pull request Sep 4, 2026
…1634)

## Summary

Follow-up to #1609. Several fields still described an ApplicationSet
using values that only mean something for an Application. Verified
against a live `kind` cluster running Argo CD 2.13.2.

### Source, project and destination

These were read from a git generator, or left blank. A git generator's
repo is scanned to produce parameters and is often not a deployment
source at all; what the generated Applications deploy is on
`spec.template.spec`. The template is now read first, with the generator
kept as a fallback only for templates that declare no source of their
own.

Repo, revision and path resolve as a unit rather than independently.
Mixing a literal template repo with a generator's path names a tree that
exists in neither, which reads as a real location and is not one. The
branch turns on whether the template *declares* a source, not on whether
that source *resolved*, so a template whose `repoURL` is itself a
placeholder still claims the row and renders blank.

Template values can carry per-app placeholders (`{{path}}`,
`{{server}}`), so a value is surfaced only when it is literal. Rendering
the raw placeholder is worse than rendering nothing, and the table
already shows a dash for an empty destination.

The destination namespace no longer falls back to the row's own
namespace for an ApplicationSet. That fallback suits a resource sitting
beside what it deploys; an ApplicationSet sits in the controller
namespace and generates Applications that land elsewhere, so it named a
namespace the object never deploys to.

### Last Sync

Empty, so the column fell through to the creation timestamp and dated
the object rather than reporting activity on it. It now carries the
transition time of the condition the status was read from.

### Sync policy

The Automation facet counted every ApplicationSet as Manual, answering
"what is not auto-syncing" with objects that never sync. The kind is now
skipped in the counts and the predicate.

`describeArgoAutoSync` had the same problem server-side. An
ApplicationSet's `spec.syncPolicy` governs how generated Applications
are created and deleted and never carries `automated`, so it reported
`Manual` to the detail header, the insights summary and the rollback
gating alike. Fixed there rather than at one call site, since all three
read the same field.

### Kind facet

The section rendered only while ApplicationSets were present, but the
filter applied regardless. Scoping to a namespace without any hid every
Application and left an empty table with no facet on screen to explain
it or switch it off. The section now stays while a `kind` filter is set.
`noOtherFiltersActive` did not account for the filter either, so the
Total tile still read as unfiltered.

### Destination on the detail page

An empty destination means in-cluster for an Application, which is
Argo's default when the field is unset. For an ApplicationSet it means
the template's destination was a generator placeholder, so the header no
longer makes that claim.

## Test plan

- 21 unit tests on the normalizer: template vs generator precedence,
source resolved as a unit, declared-but-templated sources, placeholder
blanking, literal globs, condition timestamps, the sync-policy predicate
and the destination-namespace label
- 2 Go tests on `describeArgoAutoSync` covering ApplicationSet shapes
and pinning that Applications are unaffected
- `packages/k8s-ui` 3408 passed, `web` 569 passed, `tsc` clean, lint 0
errors, `pkg` and backend suites pass
- Live cluster with eight ApplicationSets covering list, cluster,
matrix, mixed-source, templated-fields, templated-repo,
templated-namespace and failing-git generators:
- source, project and path resolve from the template; no `{{...}}`
renders anywhere
- a template that declares a source never borrows the generator's repo
or path
- a cluster generator's `{{server}}` destination shows as a namespace
rather than a false "in-cluster"
- a templated destination namespace shows nothing rather than the
controller namespace
  - Last Sync reports condition times, not creation age
  - Automation counts cover only the Applications
- the Kind section stays visible and unsettable when scoped to a
namespace with no ApplicationSets
- Applications and Flux resources are unchanged: counts, filters, row
actions and detail header all as before

## Not included

Multi-source (`spec.sources`) is unrendered for Applications too, so a
sources-only ApplicationSet template reads blank rather than being
handled ahead of the rest of the view.

"Total Applications" counts every row, which has always included Flux
Kustomizations and HelmReleases. The label predates this and is a naming
decision rather than a bug here.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Display and normalization fixes for ApplicationSet rows with broad
test coverage; Argo Applications and Flux behavior are intentionally
unchanged.
> 
> **Overview**
> **ApplicationSet fleet rows** now derive source, project, destination,
and “last activity” from `spec.template.spec` (with git-generator
fallback only when the template declares no source), resolve
repo/revision/path as a single unit, and hide `{{…}}` placeholders
instead of showing misleading literals.
> 
> **Automation and detail metadata** stop treating ApplicationSets like
syncable Applications: `hasSyncPolicy` excludes them from Automation
counts/filters, `destinationNamespaceLabel` avoids wrongly using the
controller namespace, the **Kind** facet stays visible while a kind
filter is active (and **Total** respects that filter), and
`describeArgoAutoSync` returns empty for ApplicationSets so the detail
header/insights no longer show **Manual**.
> 
> **ApplicationSet detail** no longer implies in-cluster when the
template destination server is empty; namespace-only display is used
when appropriate. Docs note the Argo-only **Kind** filter. Covered by
new Vitest and Go tests.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
47e8fa1. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants