Skip to content

feat: Migrate to the new update-flag contract, and flag update --weight - #92

Open
khvn26 wants to merge 13 commits into
mainfrom
feat/update-flag-weights
Open

feat: Migrate to the new update-flag contract, and flag update --weight#92
khvn26 wants to merge 13 commits into
mainfrom
feat/update-flag-weights

Conversation

@khvn26

@khvn26 khvn26 commented Aug 14, 2026

Copy link
Copy Markdown
Member

Closes #46. Moves every flagsmith flag mutation to the consolidated endpoint from Flagsmith/flagsmith#8102, and adds variant weights.

  • PATCH /api/__future__/environments/{key}/features/{feature_id}/ — feature in the path, and a response carrying the flag's whole state, so an update renders what the server wrote.
  • The write is partial, so nothing is read-then-echoed: --enable leaves the value alone, and flag reorder sends priorities only — one request fewer, and a move can no longer disturb a state.
  • flag delete --segment uses the endpoint's own DELETE verb (feat(__future__): Delete a flag's segment override flagsmith#8307), so removing one override neither reads nor restates the others.
  • --weight <key|id>=<percentage>, per environment or per segment, merged onto that scope's own weights. Unknown keys, a variant named twice, totals over 100 and control all fail before the write.
  • Flag detail views grow a Variants block, and every weight in human output now carries a %. RFC amended to match.

Needs the backend: #8102 and #8307 are both deployed to staging, not yet to SaaS production.

How did you test this code?

Unit tests for the wire contract, and command tests against a fake API modelling the endpoint's semantics — partial writes, per-scope weight merging, and deletion.

Then end to end against staging on a v2-versioned environment: weights per environment and per segment, override creation, priority moves, reorder, and the change-request refusal. Deletion too, once #8307 reached staging: the surviving overrides keep their weights and priorities untouched, with nothing restated. An earlier local run turned up the segment-naming bug fixed in 8d82b56.

khvn26 added 2 commits August 14, 2026 17:47
…ight`

The experimental update-flag endpoints are being consolidated into one
(Flagsmith/flagsmith#8102), and the CLI's flag mutations move to it:

    PATCH|PUT /api/__future__/environments/{key}/features/{feature_id}/

The feature moves from the body to the path, so mutations resolve it to
an id; segments are named by `{"segment": {"id": n}}`; a refused write
is a 400 with a `detail` rather than a 403; and the response carries the
flag's whole state in the environment, so the post-update render reads
what the server actually wrote.

PATCH writes only the properties it carries. That removes the read-then-
echo the old endpoint forced on every call: `--enable` no longer restates
the value, a segment edit no longer restates the environment default, and
`flag reorder` sends priorities alone — so a move can no longer disturb a
state, and it needs one request fewer.

Deleting an override loses its dedicated endpoint: a PUT replaces the set
and whatever it leaves out is gone. So `flag delete --segment` reads the
survivors and restates them in full, since a partial echo would reset the
state it omits. That makes it a read-modify-write, and a concurrent change
to another override is lost — the tradeoff the contract imposes. In
exchange, a segment with no override is now caught before the write.

`--weight <key|id>=<percentage>` re-weights a multivariate flag, per
environment or per segment, closing #46. Weights are per scope, so they
are read from the scope's feature state rather than from the variants'
project-level defaults, and merged: a partial `--weight` re-weights what
it names and keeps the rest, which the endpoint requires anyway since it
rejects a variant list that isn't complete. Everything the endpoint would
take as a new variant, or reject, is caught first — an unknown key or id,
a total over 100, and `control`, which is not a variant but the share the
weights leave unallocated.

The flag detail views grow the Variants block the RFC describes, showing
the weights in force for that scope; it costs one extra read, and only
for a feature that has variants.

The endpoint is not implemented server-side yet — #8102 is tests, types
and docs — so this is verified against the contract those tests describe,
not against a live API.
The RFC marked weights with a % in the flag views but not the feature
ones, so the same table read two ways. Human output now marks them all;
JSON keeps numeric weights, and a weight echoed back as the user typed it
(`--weight hero=40`) or read inside a sentence that carries its own % is
still bare.

RFC amended to match: the three `feature` examples in §9 now show 30%/50%.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@khvn26, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Limit details: You’ve used all 1 included review currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2169f97e-2a1c-4334-987e-131195c41f68

📥 Commits

Reviewing files that changed from the base of the PR and between 14cd5a6 and 714af62.

📒 Files selected for processing (6)
  • README.md
  • internal/api/client.go
  • internal/api/client_test.go
  • internal/cmd/cmd_test.go
  • internal/cmd/errors.go
  • internal/cmd/sentinel_test.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 51a0345a-2a93-47a3-96e2-1eacd5b900a1

📥 Commits

Reviewing files that changed from the base of the PR and between bab2f7a and 14cd5a6.

📒 Files selected for processing (1)
  • internal/api/client.go

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The update command now uses typed PATCH and DELETE API operations with partial state changes. It validates multivariate variant weight updates and preserves returned server state. Reorder and deletion operations use response data and numeric feature paths. Flag and segment detail views now display scoped variant allocations with percentage formatting. Tests cover request semantics, workflow gating, validation errors, override replacement, reordering, and variant allocation behaviour.

Estimated code review effort: 5 (Critical) | ~90 minutes

Merge Risk: 🔵 Low · up to 14cd5

The PR migrates flag updates to a new partial-write endpoint, but the command test double still contains an unreachable PUT path and does not fully match the endpoint it models, so tests may miss regressions in update behavior. This is a bounded merge-readiness risk that should have explicit owner follow-up.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@khvn26 khvn26 changed the title feat: migrate to the new update-flag contract, and flag update --weight feat: Migrate to the new update-flag contract, and flag update --weight Aug 14, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a57d9dae-07d7-4929-bc8c-df81130e0e80

📥 Commits

Reviewing files that changed from the base of the PR and between 110158a and 402de8f.

📒 Files selected for processing (9)
  • README.md
  • internal/api/client.go
  • internal/api/client_test.go
  • internal/cmd/cmd_test.go
  • internal/cmd/feature.go
  • internal/cmd/flag_identity.go
  • internal/cmd/flag_reorder.go
  • internal/cmd/flag_update.go
  • internal/cmd/flags.go

Comment thread internal/cmd/flag_update.go
Comment thread internal/cmd/flag_update.go Outdated
A new override has no feature-segment row yet, so there was no name to read
for the confirmation or the detail view, and both showed a bare id — right
after the user had named the segment themselves:

    ✓ Set banner_copy weights to hero=100, sub=0 for segment 1 in ...
    Segment    1

Resolving a name warms the segment name cache, so read the name back from
there, and keep the bare id for the case that is genuinely nameless: an
override referenced by id that does not exist yet.

Found testing against the update-flag implementation (Flagsmith/flagsmith#8102).
khvn26 added 4 commits August 15, 2026 11:08
The merged endpoint answers a workflow-gated write with 409 and
`"code": "change_requests_enabled"`, where the branch this was built
against used 400 and prose. Matching prose would have broken on the next
rewording anyway, so read the code, and don't lean on the status alone —
409 will cover any conflict added later.

Without this, an environment that simply uses change requests reports as
something to report as a bug.
`--weight hero=10,30011=20` names one variant twice when 30011 is hero's
id. The duplicate check keyed on the reference as typed, so this passed
and the last weight quietly won.

Check once the reference has resolved, and name both spellings in the
error — the point is that the user probably doesn't realise they are the
same variant.
Deleting an override replaces the whole set, and a replacing write that
omits `variants` inherits the environment default's weights rather than
leaving the override alone. So an override with no allocations of its own
could not be restated by omission, as it was — it came back weighted like
the environment.

Send the full list for any feature with variants, zeros included.
Priorities order the overrides but needn't be contiguous — the API's own
examples use 10/20/30 — and the merged endpoint now rejects two overrides
sharing one. Two things followed from the old assumption:

- a new override joined at `num_segment_overrides`, which collides with an
  existing override whenever the priorities are sparse. It now joins past
  the highest, read from the rows already fetched for the segment's name.
- `--priority` was range-checked against that count, rejecting a move the
  server would accept. Only a negative is rejected now; the server owns
  the rest.
@khvn26 khvn26 changed the title feat: Migrate to the new update-flag contract, and flag update --weight feat: migrate to the new update-flag contract, and flag update --weight Aug 15, 2026
@khvn26
khvn26 marked this pull request as ready for review August 15, 2026 11:33
@khvn26 khvn26 changed the title feat: migrate to the new update-flag contract, and flag update --weight feat: Migrate to the new update-flag contract, and flag update --weight Aug 15, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 16a3a368-a407-4e8c-9ba8-a0f6af9e233d

📥 Commits

Reviewing files that changed from the base of the PR and between 402de8f and d64324c.

📒 Files selected for processing (5)
  • internal/api/client.go
  • internal/api/client_test.go
  • internal/cmd/cmd_test.go
  • internal/cmd/flag_update.go
  • internal/cmd/flags.go

Comment thread internal/cmd/cmd_test.go Outdated
The check that a new override's detail names its segment was a negative
assertion on `"Segment    42\n"` — an exact run of four spaces, which
tabwriter sets from the widest label in the view. Any change to the
padding would have made it pass while testing nothing.

Match the row instead, with `\s+` for the padding, so the assertion says
where the name has to appear rather than only that the bare id is absent
from one particular rendering.
@khvn26
khvn26 marked this pull request as draft August 15, 2026 13:53
Flagsmith/flagsmith#8307 adds
`DELETE …/features/{id}/segment-overrides/{segment_id}/`, so removing one
override no longer means replacing the whole set.

That takes the read-modify-write out of `flag delete --segment`: the
surviving overrides are neither read nor restated, so a concurrent change
to one can't be lost, and the endpoint's answer is the whole flag, as it
is for the writes. `echoOverrides` and `echoVariants` go with it —
restating an override's weights only mattered because a replacing write
inherits the environment default's when `variants` is omitted.

The override rows are still read, for two reasons a 404 doesn't cover:
the prompt names the segment, and a segment with no override should be
caught before asking rather than after.
@khvn26
khvn26 marked this pull request as ready for review August 16, 2026 20:48

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/cmd/cmd_test.go (1)

671-700: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unreachable PUT replacement path.

The handler registers only PATCH, so r.Method == http.MethodPut is always false. Register PATCH directly and remove the replace parameters and PUT-only branches from applyFlagUpdate and writeScope.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b2314422-091c-4e68-bccb-a845b575ef99

📥 Commits

Reviewing files that changed from the base of the PR and between d64324c and bab2f7a.

📒 Files selected for processing (5)
  • internal/api/client.go
  • internal/api/client_test.go
  • internal/cmd/cmd_test.go
  • internal/cmd/flag_update.go
  • internal/cmd/sentinel_test.go

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.

gagantrivedi
gagantrivedi previously approved these changes Aug 17, 2026
Comment thread internal/api/client.go Outdated
khvn26 added 3 commits August 17, 2026 13:04
Every flag mutation goes through endpoints that arrive in Flagsmith
2.263.0, so against an older instance they all failed with

    Error: PATCH …/api/__future__/… returned 404 Not Found
    Think this shouldn't happen? Tell us: …/issues/new

which sends a self-hosted user to our issue tracker for a supported
configuration. They now get:

    Error: changing flags needs Flagsmith 2.263.0 or newer: this instance runs 2.259.0
    Upgrade the instance, or point --api-url at one new enough. Reading
    flags works on any version.

The version comes from `/version`, read only on this failure path, and
left out of the message when it can't be read.

Detection is the 404 carrying no detail — nothing rendered one, so the
route isn't there. The endpoints answer 404 themselves for an environment
or feature the caller can't see, and those must keep reading as not-found:
they are the common case on SaaS, where "upgrade" would be nonsense. The
same distinction is what tells a missing route from a missing override
when deleting.

Not a version pre-flight: staging serves these endpoints while reporting
2.262.0, being built from main between releases, and a self-hosted
instance may run any tag at all.
Two conflicts, both in the hint machinery #86 reworked:

- `errors.go`: main replaced several hint constants with functions that
  name the host-scoped variable, and added `hintAPIURL`. Took main's
  block, dropped the `hintEnvironmentKey` constant it turned into a
  function, and kept `hintUpgradeInstance` beside them.
- `sentinel_test.go`: both sides added sentinels. Kept all of them —
  main's `auth.ErrNoDiscovery` and this branch's two.
The merge brought in `api.ServerVersion`, added by #86 for the same
purpose, so the version lookup added here was a second one in the same
package. Uses the shared function instead, which also stops going through
the 301 that `/version` (no trailing slash) answers with.

Adopts its rule about the tag, too: an instance reporting "latest" or a
commit sha now has its tag left out of the message rather than quoted
back as though it were a version. Nothing here rests on the tag — the 404
already established the endpoint is absent — so it only decides whether
the version can be named, where `login` has to stay silent entirely
because it is inferring age.

Dropped the `package_versions` fallback with it: it would be a change to
what `login` gates on, which wants deciding on its own terms.
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.

Complete flagsmith flag update --weight flow

2 participants