* feat(fork): merge a fork deletion on evidence, not on the counters
`workspace_diff.ahead`/`.behind` record that a write happened on a side,
not what it was or who made it. That leaves one row shape undecidable: an
item the parent has and the fork does not can mean the parent added it,
the fork deleted it, or a git-sync pull reverted a deploy that had just
brought it in. #10467 kept every such row out of the merge direction,
which killed the phantom but also dropped the only way to propagate a
fork-side deletion and left a rename's old path behind in the parent.
Record the evidence instead:
- `workspace_diff` gains, per side, the last event's kind (`write` /
`delete` / `rename_from`) and origin (`authored` / `sync`). Rows
written before the migration have neither and keep #10467's behavior.
- The kind is probed from whether the path still holds an item once the
write has committed; an item kind the probe doesn't map records no
evidence rather than a deletion. Create and update are not split —
nothing at that point tells them apart for every kind, and the
comparison already recomputes existence per side.
- The origin comes from an `X-Windmill-Deploy-Origin` header the API
scopes into a task-local for the request. It is the load-bearing half:
recording `delete` alone would read a git-sync revert as a fork
deletion and reproduce the original bug. Two clients set it — `wmill
sync push` (which the git-sync auto-pull runs inside a job) and the
compare page's parent→fork "Update fork". Merging the other way stays
authored so a deletion keeps propagating up a fork chain.
- The merge direction admits a parent-only row only when the fork's last
event was an authored delete or rename-away. Such a row stays opt-in,
never bulk-selected, and reads "Removes in <parent>"; the update
direction keeps offering it back as "New".
A fork deletion and a rename now merge into the parent, a rename leaves
no duplicate behind, and a fork the parent also edited surfaces in both
directions instead of the parent silently winning.
Fixes WIN-2289
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(fork): address review — detached tallies, enum wire values, doc duplication
Codex P1: a dependency job tallies its deploy whenever it happens to finish,
and the event kind is probed from the state at that moment. If anything
removed the path in between (a git-sync revert), the stale tally read that
deletion as its own and filed it as authored — handing the merge exactly the
removal this is meant to withhold. `tally_deployed_object_changes` now takes
`Option<DeployOrigin>`; `None` bumps the counter and leaves the evidence
columns as the last vouching tally left them, and the worker path passes it.
Covered by extending the removal-origin test: a detached tally after the sync
archive must not disturb `(delete, sync)`.
Also from review:
- `fork_removed_it` compares through `DeployOrigin::as_str()` /
`DeployEventKind::as_str()` rather than repeating their wire values, so a
renamed variant can't silently make the predicate always false.
- `deploy_origin`'s module doc no longer claims `sync` is inert: it cannot
make the merge propose a removal, but it does drop a row out of both sides
of the `all_ahead_items_visible` comparison.
- `WorkspaceDiffRow` says why only the fork half of the evidence is consumed.
- The delete-vs-revert rationale is stated once (the migration) instead of
restated in eight files.
- `PATH_KEYED_TABLES` is swept by a test: its query is built at runtime, so a
wrong table name is not a compile error and would only surface as a failed
tally for that trigger kind in a fork.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(fork): let only a request task vouch for a deploy event
Round 2 found the first fix incomplete. Detaching only the failed/cancelled
dependency path left the common route untouched: a dependency job that
succeeds calls `handle_deployment_metadata` from the worker, where
`deploy_origin::current()` read as `Authored`. A sync archiving the script
while its lock generation was pending then had its deletion probed on
completion and refiled as authored — the same fabricated removal, on the
path most deploys actually take.
`current()` now returns `Option`, `Some` only inside the request scope the
API always enters. Having no scope means "not the task that served this
write", which is true of every worker-side call and needs no marking at the
call site. The integration test drives the real `handle_deployment_metadata`
off a request task instead of the tally directly, and fails without this.
Two more from the same round:
- The script dependency handler passed no `renamed_from`, unlike the flow
and app handlers next to it. A lock-generating create has no earlier
tally, so that was the only chance for the path a rename vacated to be
recorded at all — renames of Python/TS scripts left the old path in the
parent, which the bash-only manual check missed.
- The tally now drops a `renamed_from` equal to the path itself. Callers
pass the previous path whether or not the deploy moved the item, so an
unfiltered one both counted the path twice and stamped it `rename_from`
when nothing was renamed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(fork): carry a deploy's origin into the dependency job it queues
Round 3 caught the previous fix cutting too deep. Refusing a detached tally
any claim also refused its rename evidence, and a lock-generating deploy has
no other tally — so the `renamed_from` added alongside it was inert, and a
renamed flow, app or Python script still left its old path in the parent
with nothing to merge. Flows and apps always generate, so renames worked
essentially nowhere.
The two capabilities are now separate. `TallyEvidence` says whether the
tallying task served the write (`Served`, may probe what the path holds now)
or is reporting one that committed earlier (`Deferred`, may not), and each
column is written only from a source that answers for it. The origin itself
is a fact of the deploy either way, so the request stamps it into the
dependency job's args and the worker re-enters the scope with it — the last
place that knows it handing it to the only tally that will run.
Also from round 3: `WorkspaceDiffRow`'s event fields skip serializing `None`
rather than emitting `null`, matching what the schema declares (OpenAPI
3.0.3 ignores a `description` sibling of `$ref`, so those moved onto the
shared schemas).
Verified against a live worker: renaming a flow in a fork records
`(rename_from, authored)` on the vacated path and the merge offers its
removal, while the deployed path claims nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(fork): mark the CLI's parent-to-fork merge as sync
`wmill workspace merge --direction to-fork` is the CLI's "Update fork" and
deletes items in the fork, but without the marker the compare page sets. Its
deletions were recorded as authored fork decisions, so once the parent
recreated such a path the merge would offer deleting it there.
Also from review: an unrecognized deploy-origin arg now reads as no evidence
rather than as authored — strict where a request header is lenient, since an
unmarked request really is authored but an unreadable stored value is skew.
Reading the arg moved next to `stamp_origin_arg`, the half that writes it, so
the round trip a lock-generating deploy depends on is covered by one test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: drop the imports the shared arg reader made unused
CI compiles with `-D warnings`, so this was four red Backend jobs rather
than a lint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(fork): stop a stale deferred rename from restating a removed path
Nothing orders these events. A tally that served the write made its claim
inside its own commit, but a deferred one reports a write that landed at an
unknown remove. So a lock-generating rename whose dependency job finished
after a sync had removed the vacated path could overwrite `(delete, sync)`
with `(rename_from, authored)` — the path is gone either way, so the merge
would then offer removing it from the parent on the strength of the older
event.
A deferred claim now only writes where the side has none, which is the case
it exists for: a vacated path that nothing else has spoken for. The
regression asserts the ordering directly, and fails without the guard.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(fork): record a rename's vacated path from the request that made it
The deferred mechanism could not be made correct, as round 7 showed: its
guard protected an existing row, but that row is deleted as soon as the two
workspaces agree on the path — so a rename job finishing after the
reconciliation inserted fresh, and the stale claim reappeared against
whatever the parent later recreated there. Ordering cannot be recovered
outside the row, because the row is disposable.
So the vacated path is now recorded by the request, which is inside its own
commit and whose row shares the counter's lifetime. A deploy that hands its
metadata to a dependency job — every flow and app, and any script needing a
lock — calls `tally_rename_vacated_path` once its transaction has committed;
scripts reach it through the post-commit hook they already had, which grew a
second variant rather than new plumbing.
That lets the whole deferred apparatus go: `TallyEvidence`, the origin job
arg and its round trip. `deploy_origin::current` is `Some` only inside a
request scope again, and `handle_deployment_metadata` hands `renamed_from`
to the tally only when it can answer for it — git-sync still gets it either
way, so the rename keeps naming itself in the commit message.
The vacated path's kind now reads `delete` rather than `rename_from` for
these deploys, since it is probed rather than declared. The merge treats the
two alike; only the row's tooltip is less specific.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(fork): cover raw-app renames, and stop firing CI before the lock exists
Two things the vacated-path call broke or missed:
- `create_script` reads its third return value as "no lock generation
needed" to decide whether the script is runnable now, and the new
`VacatedPath` variant made that true for renames that do generate. Those
fired dependent CI tests from the API against a version with no lockfile,
and again from the dependency job. The variant now decides it explicitly.
- Raw apps rename through `update_app_raw`, a separate route into
`update_app_internal`, which the new call had not been attached to. Both
routes now go through one helper.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(fork): assert the kind only an inline rename can record
`rename_from` is what a deploy says when it knows it moved the item, which
only the path that reports both halves from its own request can. Nothing
pinned it, and that is the side the vacated-path change touched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to a45bec03922d305aad5893ed354dc029c7f97bb4
This commit updates the EE repository reference after PR #709 was merged in windmill-ee-private.
Previous ee-repo-ref: 62f494b2a51de0dfc0cfa0c3530ff19a1d32667c
New ee-repo-ref: a45bec03922d305aad5893ed354dc029c7f97bb4
Automated by sync-ee-ref workflow.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: auto-sync data table migrations to the linked git repo
* fix: deploy data table migrations when a data table is renamed or deleted
* fix: hold new data table names to the git-sync-safe charset
* fix: reject leading-dot data table names and warn on unsyncable legacy names
* chore: update ee-repo-ref to 15c9eef2a4f867eb90d841aee1ce762f4b725589
This commit updates the EE repository reference after PR #702 was merged in windmill-ee-private.
Previous ee-repo-ref: e2fb073a3d0057683666424e463b2ce423664caa
New ee-repo-ref: 15c9eef2a4f867eb90d841aee1ce762f4b725589
Automated by sync-ee-ref workflow.
* feat: make data table migrations a git-sync object type with its own toggle
* fix: never let an untracked checkout delete data table migrations on push
* fix: confirm ambiguous data table migration deletions instead of dropping them
* fix: settle ambiguous migration deletions before the dry-run preview prints
* fix: restore the split shared-UI comment and count migration records in prompts
* chore: keep the deletion-safety doc block attached to its function
* fix: trust git history, not the working tree, for migration deletions
* fix: scope migration history to HEAD, detect shallow clones and subdir roots
* chore: give the unattested-history case a remedy that applies to it
* chore: pair each unattested-history cause with its own remedy
* fix: treat a sparse checkout as unattested history for migration deletions
* fix: normalize the sparse-checkout boolean and give it a remedy that works
* chore: describe both shapes of unattested migration history
* chore: update ee-repo-ref to a786cd42b5aaf0aa6789fbb723d956560f93b1b3
This commit updates the EE repository reference after PR #703 was merged in windmill-ee-private.
Previous ee-repo-ref: 4f312642b5d8fd37ab5e20473a011d6f1d299cf6
New ee-repo-ref: a786cd42b5aaf0aa6789fbb723d956560f93b1b3
Automated by sync-ee-ref workflow.
---------
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* feat: make the fork lineage the only deploy relationship
`workspace_settings.deploy_to` (2023) and `workspace.parent_workspace_id` (2025)
both expressed "which workspace does this one deploy into". Fork creation and
dev-workspace attach seeded both, but nothing kept them in agreement, so every
reader picked one and they disagreed.
Drop `deploy_to`. A migration folds surviving pairs into the lineage: a sole
claimant on a target with no dev workspace becomes that target's dev workspace
and keeps its own job tags, while many-to-one pairs become plain forks. Pairs
that the lineage cannot express -- dangling target, self-reference, chain,
mutual -- are reported and left unlinked.
Job tags were never lineage-aware: `per_workspace_tag` mapped any parented
workspace to its parent while `$workspace` interpolated the raw id, so a fork
running a script tagged `<tag>-$workspace` produced a tag no worker serves and
the job queued forever. Both paths now resolve to the nearest ancestor whose id
an admin would provision workers for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: preserve unconvertible deploy links and sweep tag caches on reparent
Review findings on the deploy_to unification:
- convert chains instead of discarding them, and keep whatever the lineage
cannot express in workspace_deploy_to_unmigrated so the down migration can
restore it
- ignore soft-deleted workspaces when choosing between a dev workspace and a
plain fork; an archived claimant was demoting live pairs
- mirror attach_dev_workspace's git-sync strip, which the migration skipped
- sweep the tag cache over whole subtrees on rename and delete: tag resolution
now walks ancestors, so a nested fork kept a tag nothing serves
- call a dev workspace a dev workspace in the settings copy
- redirect a root away from ?tab=deploy_to instead of rendering an empty target
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: detect lineage cycles and record archived links in the deploy_to migration
Second review round on the unification:
- detect cycles over the lineage as it would exist after conversion, not over
the deploy_to graph alone: a root whose target was one of its own forks
closed a loop that no deploy_to edge revealed
- record an archived source's link instead of filtering it out entirely, which
dropped it with the column
- treat a fork whose deploy_to merely repeats its parent as redundant rather
than reporting every pre-existing fork as unmigrated
- read the row count from the lineage update rather than the git-sync one
- sweep the tag cache when archiving a dev workspace, the last site that
mutates is_dev_workspace without one
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: resolve $workspace on preprocessed flow tags regardless of $args
Third review round on the unification:
- a flow tag containing only `$workspace` skipped interpolation entirely on the
preprocessed path, because the branch that ran it keys on `$args`. The raw
tag was written back and named a queue no worker serves. Resolve `$workspace`
before the branch and leave `$args` to it.
- record the new table's foreign key in the schema summary
- describe what the archive tag sweep actually does: the dev flag is cleared for
any archived workspace, which is why it is unconditional
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep the deploy_to leftovers table only when it holds something
* fix: sweep tag caches on archive only where the dev flag actually changes
* feat: broadcast lineage changes and walk ws_specific ancestors only
- propagate tag-cache invalidation across processes over notify_events: the
cache is per-process, so replicas kept resolving stale lineage for the TTL.
The listener clears the whole cache rather than tracking ids, since a single
mutation invalidates an unbounded set of descendants and lineage changes are
rare admin actions.
- narrow list_ws_specific_versions to ancestors: walking down as well made a
root fan out over its entire live fork subtree, and each member costs an
identity lookup plus an RLS switch and probe. Ancestors are bounded by the
fork depth limit.
- probe the leftovers table unqualified so rollback restores on a PG_SCHEMA
install, where search_path is not public
- drop the nativets client method for the removed edit_deploy_to endpoint
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: let a prod see its dev workspace in ws_specific, and stop the walk oscillating
Descending into plain forks made a root fan out over its whole live fork
subtree, but a dev workspace is the paired editable environment rather than a
throwaway copy, so a prod should still see it. There is at most one per parent
and attach rejects nested dev chains, so that edge stays bounded.
The edges run both ways, so the recursion never converged: it bounced
parent<->dev until the depth cap on every call, 33 rows for a two-member set.
A visited-path guard ends the walk when nothing new is reachable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: keep dev pairings unnested, gate the delete broadcast, cover the ws_specific walk
Fifth review round:
- a root that already owns a dev workspace no longer converts: linking it under
its deploy target would leave that dev nested beneath a fork, the shape
attach_dev_workspace refuses to create. The link is preserved instead.
- broadcast a lineage change on delete only when descendants are orphaned.
Deleting a leaf, which ephemeral fork churn does constantly, changes nobody
else's resolution and was making every replica drop its whole tag cache.
- call list_ws_specific_versions in a test. plpgsql defers everything past a raw
parse to the first call, so replaying the migration only proved it parses.
- use unwrap_or_default for the descendant sweeps, which run after the
transaction has committed; a transient failure must not fail the request
- trim the traversal comment to the four-line limit
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: cache the renamed tally query and clear instance alerts on conversion
The integration test's query was never cached: `cargo sqlx prepare` without
--all-targets skips test targets entirely, and renaming its fixture workspace
changed the query text. Regenerated with --all-targets --features
all_sqlx_features,private, which is what lets the EE-gated otel test compile.
Also from review:
- clear error_handler_fallback_to_instance_alerts on converted workspaces.
Dispatch ignores it once a parent exists, but the settings page keeps
submitting the stored true, which the API rejects on a fork.
- restore the schema summary row to the file's name: columns format and put it
back in alphabetical order
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: never cache an unresolvable tag workspace, and unadvertise the removed endpoint
- lookup_tag_workspace cached a "no row" result as self-resolution. A rename
resolves the new id before its row lands, so a fork could be pinned to its own
wm-fork-* id -- which nothing serves -- for the whole TTL, and its schedules
kept re-pushing onto that dead tag. Fall back for the call without caching,
matching how the error path already behaved.
- change_workspace_id swept its children but never itself. Sweep the new and old
ids and broadcast unconditionally, since a rename always changes lineage.
- openapi-deref.{json,yaml} are served to clients via include_str!, so they were
advertising edit_deploy_to after it started 404ing. The audit-action enum
keeps the entry: historical rows still carry it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: align the served YAML spec with the JSON one and correct two comments
- the YAML deref lost the removed path but kept deploy_to on get_settings,
so the two served specs disagreed. Both are now identical.
- the rename-sweep comment blamed cached-unresolvable lookups, which the same
commit stopped caching. The real reason is that workspace ids are
reclaimable, so a new id can carry a previous occupant's resolution.
- the instance-alert comment claimed the settings page submits the stored true
and gets a 400. It hides the option on a fork and sends false; the hazard is
the value outliving the pairing and re-enabling alerts after a detach.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: update ee-repo-ref to 82da6cb2bafeda18acd6b70c599013a12117ecb0
This commit updates the EE repository reference after PR #694 was merged in windmill-ee-private.
Previous ee-repo-ref: f9ddf6a75aa13d1c13a3d7216a361a96f75ca435
New ee-repo-ref: 82da6cb2bafeda18acd6b70c599013a12117ecb0
Automated by sync-ee-ref workflow.
* fix: grant the deploy_to preservation table to the windmill roles
* test: drop the one-shot migration tests, keep the ws_specific execution guard
The two conversion tests replayed the migration against the fully-migrated
schema, which is not how it runs -- in production it runs mid-sequence against
the schema as of that point. A later migration touching workspace or
workspace_settings would break them without breaking anything real, and sqlx
checksums already freeze a released migration. They earned their keep finding
the archived-claimant and nested-dev cases during development; there is nothing
left for them to guard.
list_ws_specific_versions is different: it is live, no caller exercises it, and
plpgsql only parses a function body until first call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: invalidate a reclaimed fork id cluster-wide without flushing every entry
Gating the delete broadcast on orphaned descendants stopped leaf churn flushing
every replica, but fork ids are reclaimable: the deleting process invalidated
locally while every other replica kept the old parent for the TTL, so a job
pushed in a recreated fork routed to the previous parent's tag.
The broadcast payload now carries meaning. A workspace id drops that one entry,
used for leaf deletion where exactly one id changed what it denotes. The `*`
sentinel drops everything, used for attach, detach, archive, rename and
deletions that orphan descendants -- reshaping a subtree no single id names.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: name the right broadcast for each invalidation case
* docs: attach does invalidate the tag cache; the resolver walks the whole chain
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
* fix: do not apply workspace display name on git-sync pull
The workspace display name is stored in settings.yaml and was re-applied on
every pull via changeWorkspaceName. Because settings.yaml is shared across the
branches of a repo, a workspace could have its name overwritten by another
workspace that syncs the same repo. Keep name in settings.yaml for reference
(written on push) but stop applying it on pull.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: consolidate workspace-name rationale to one comment (review nit)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump git-sync hub scripts to windmill-cli 1.769.1
Repin GIT_SYNC_PULL_SCRIPT_PATH (28795->28808), LATEST_GIT_SYNC_SCRIPT_PATH
(28796->28809) and frontend gitInitRepo to the hub scripts bundling
windmill-cli@1.769.1, so backend automatic git pulls no longer apply the
workspace display name (the CLI fix in this PR only reaches auto-pull via the
pinned hub script bundle).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>