Skip to content

feat: scope cluster version selector to org-deployed versions#1539

Merged
Devin-Holland merged 4 commits into
stagefrom
feat/org-deployed-harper-versions
Jul 22, 2026
Merged

feat: scope cluster version selector to org-deployed versions#1539
Devin-Holland merged 4 commits into
stagefrom
feat/org-deployed-harper-versions

Conversation

@Devin-Holland

Copy link
Copy Markdown
Member

What & why

Companion to central-manager #466 (feat: org-scoped Harper versions on /HarperVersions).

The deploy version selector is fed by GET /HarperVersions, which returns the global default list. This threads the current organizationId into that query so the backend can additionally return the versions currently deployed on the org's clusters (tagged deployed), letting operators deploy a new cluster at a version they already run. The merge/authorization all happens server-side; this is just the wiring.

Changes

  • getHarperVersions / getHarperVersionsOptions take an optional organizationId, appended as a URL-encoded query param and folded into the React Query key (['HarperVersions', orgId ?? null]) so the cache is scoped per org.
  • UpsertCluster passes the route's organizationId into the query (the deploy screen already has it via useParams).
  • The existing edit-flow merge (the current tag + upgrade-only filtering) is untouched — org-deployed versions simply flow into the base list it builds on.

Notes

  • Backward compatible: no organizationId → same request as today (/HarperVersions/).
  • Depends on central-manager v4.8.20 #466 for the deployed-version merge; before that ships, passing an org id just returns the default list (or 403 for a non-member).

Tests

getHarperVersionsQuery.test.ts: adds coverage that the org id scopes the cache key and is passed url-encoded as a query param. Full suite green via the pre-commit hook (1734 passed), typecheck (tsc -b), oxlint, and dprint all clean.

🤖 Generated with Claude Code

Pass the current organizationId into the /HarperVersions query so the deploy
version selector can include versions currently deployed on the org's clusters
(the backend merges them in, tagged `deployed`, for enterprise orgs).

- getHarperVersions / getHarperVersionsOptions take an optional organizationId,
  appended as a query param and folded into the query key so the cache is
  scoped per org.
- UpsertCluster passes the route's organizationId through.

Requires the companion central-manager change adding organizationId to
GET /HarperVersions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request updates the Harper versions query to support scoping by an optional organizationId. The getHarperVersions API call now appends the organizationId as a URL-encoded query parameter when provided, and the React Query cache key is updated to include the organization ID (or null if absent). The cluster upsert component was updated to pass the organizationId to this query, and corresponding unit tests were added to verify the new behavior. There are no review comments, and I have no additional feedback to provide.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 51.86% 5557 / 10714
🔵 Statements 52.41% 5946 / 11344
🔵 Functions 44.35% 1375 / 3100
🔵 Branches 44.85% 3733 / 8322
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/clusters/queries/getHarperVersionsQuery.ts 100% 100% 100% 100%
src/features/clusters/upsert/index.tsx 0% 0% 0% 0% 41-301
Generated in workflow #1570 for commit 7b8675e by the Vitest Coverage Report Action

const path = organizationId
? `/HarperVersions/?organizationId=${encodeURIComponent(organizationId)}`
: `/HarperVersions/`;
const { data } = await apiClient.get(path as any);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you shift this to instead use the params? We don't need to manage the query string building ourselves.

	const { data } = await apiClient.get(`/HarperVersions/` as any, {
		params: {
			organizationId,
		},
	});

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done — switched to axios params: { organizationId } so we no longer hand-build the query string (axios handles encoding). Matches the getPlanTypesQuery pattern. (d6cb4a8)

— devain (Claude Opus 4.8)

}

async function getHarperVersions() {
async function getHarperVersions(organizationId?: string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We'll never make this request without an organizationId, too, so just make it a required parameter. That may splash outward more, preventing this from loading until we know the organizationId definitively from the route. That will simplify the testing, too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good call — made organizationId a required string on both getHarperVersions and getHarperVersionsOptions, so it splashes outward to the route (same shape as getPlanTypesOptions(organizationId)). Dropped the no-arg cases from the tests accordingly. (d6cb4a8)

— devain (Claude Opus 4.8)

Devin-Holland and others added 2 commits July 21, 2026 12:10
The edit-flow version picker injects a synthetic "current" entry for the cluster
being edited. Now that the backend also returns org-deployed versions, the current
cluster's own version comes back as a "deployed on <cluster>" entry — so exclude
every version the edited cluster runs (not just the max) to avoid a duplicate row.

Also copy the array before sort() (sort mutates) so the full instance-version set
is intact for the exclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…es axios params

Per review (dawsontoth):
- Make organizationId a required parameter of getHarperVersions/getHarperVersionsOptions
  — the picker never requests versions without an org, so require it (matches the
  sibling getPlanTypesOptions pattern) and let it splash outward to the route.
- Pass organizationId via axios `params` instead of hand-building the query string
  (axios handles encoding).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Devin-Holland
Devin-Holland marked this pull request as ready for review July 21, 2026 18:02
@Devin-Holland
Devin-Holland requested a review from a team as a code owner July 21, 2026 18:02
@Devin-Holland
Devin-Holland requested a review from dawsontoth July 21, 2026 18:02
@Devin-Holland
Devin-Holland requested a review from dawsontoth July 21, 2026 18:16

@DavidCockerill DavidCockerill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM — clean org-id threading. organizationId comes from the route (useParams, the same source already feeding the sibling plan/region queries, so it's the current org context, not a stale global), and the react-query cache is org-scoped (['HarperVersions', organizationId]) so switching orgs can't serve the previous org's version list. Making the param required + moving to axios params (your two earlier asks) both landed, and the undefined edge degrades safely to the old global behavior. Org id in the query is just an identifier, so nothing sensitive rides the URL. Tests cover the with-org param, cache key, and the deployed-tag/encoding cases.

One cosmetic nit, non-blocking: the cache key here is namespace-first (['HarperVersions', organizationId]) while the sibling getPlanTypesOptions is org-first ([organizationId, 'instancePlan']) — both correctly include the org, just inconsistent ordering.

— DAIvid (Claude Fable 5)

@dawsontoth

Copy link
Copy Markdown
Contributor

Sticking the org id first is more consistent across the repo, and allows for easier scoped cache busting.

Review (DavidCockerill, dawsontoth): use an org-first query key
`[organizationId, 'HarperVersions']` to match the sibling queries
(getPlanTypesQuery, getRegionLocationsQuery, …). Beyond consistency, this makes
the list participate in the repo's org-scoped cache busting
(`invalidateQueries({ queryKey: [organizationId] })`, used after cluster ops) —
with the old namespace-first key, prefix matching skipped it, so deployed
versions could go stale after a cluster change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Devin-Holland

Copy link
Copy Markdown
Member Author

Done in 7b8675e — switched to an org-first cache key [organizationId, HarperVersions], matching getPlanTypesQuery/getRegionLocationsQuery/etc.

Worth flagging it turned out to be more than cosmetic: the org-scoped busts already in the repo (invalidateQueries({ queryKey: [organizationId] }) in useClusterContainerOps, ClusterStateMenu, ClusterCard, domain mgmt) match by key prefix, so the old namespace-first key was skipped by them — after a cluster op changed deployed versions, this list could have gone stale. Org-first makes it participate. Thanks both.

— devain (Claude Opus 4.8)

@Devin-Holland
Devin-Holland added this pull request to the merge queue Jul 22, 2026
Merged via the queue into stage with commit 2ccba97 Jul 22, 2026
2 checks passed
@Devin-Holland
Devin-Holland deleted the feat/org-deployed-harper-versions branch July 22, 2026 14:20
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.

3 participants