Skip to content

Merge v17 into v18 (2026-09-08) - #1072

Merged
KevinJump merged 7 commits into
v18/mainfrom
merge/v17-to-v18-2026-09-08
Sep 8, 2026
Merged

Merge v17 into v18 (2026-09-08)#1072
KevinJump merged 7 commits into
v18/mainfrom
merge/v17-to-v18-2026-09-08

Conversation

@KevinJump

Copy link
Copy Markdown
Owner

Summary

Forward-ports the outstanding v17 work onto v18, plus migrates v18's nightly
feed to nightly.jumoo.uk (replacing v18's separate Azure Artifacts publish
job).

Since the last forward merge (#1050) was squash-merged, git's merge-base
between the branches is stale (predates the v17/v18 split), so a plain
git merge would replay years of already-diverged history as false
conflicts. Commits below were cherry-picked individually instead, onto a
branch off v18/main.

Plus a fix-up commit: TemplateSerializerTests used the old
TemplateContentParserService.MasterTemplateAlias name; Umbraco 18 renamed
it to LayoutTemplateAlias (the product code already used the new name via
ITemplateContentParserService, only these new tests needed updating).

Not ported (per discussion): the dependency-bump commits (#1057, #1061,
#1062, #1063, #1064) — v18's client lockfile has already diverged/moved
ahead independently, so these should go through dependabot on v18 directly
rather than being forward-ported.

Already applied to v18 independently, so skipped: #1052 (landed as
#1053), #1056 and #1058 (v18 already achieved the same effect via different
commits), and two purely-v17-specific commits (cbc3726e workflow renames,
69581edb nightly pack-list additions) that are moot once #1068 was
resolved against v18's own package-up job.

Test plan

  • dotnet build ./uSync.slnx -c Release — succeeds, 0 errors
  • dotnet test ./uSync.Tests/uSync.Tests.csproj -c Release — 218/218 passed
  • Client: npm ci && npm run typescript:build && npm run build — succeeds
  • CI: Build and Package / PR checks

Please merge with a merge commit, not squash, so the next forward-port
run can find this baseline directly from ancestry instead of parsing a
squashed commit body.

🤖 Generated with Claude Code

KevinJump and others added 7 commits September 8, 2026 10:51
* Fix and finish uSync's background processing mode (opt-in, default unchanged)

The background sync path (SyncProcessingMode.Background) existed but was
half-wired: it passed the per-run requestId as the long-running-operation
*type* (so allowConcurrentExecution never worked), discarded the enqueue
result, and returned an unrelated id the client could never use to check on
the run. There was no way to poll progress or reattach after a page reload,
and export-to-file downloaded before the export had actually finished.

- Give each run a stable operation type (uSync:<Action>) and thread the real
  operation id back to the client; surface an "already running" rejection
  instead of reporting a phantom success.
- Add GET Status/Running endpoints backed by a small per-run progress cache,
  so the client can poll for progress and reattach to a run it didn't start
  itself (e.g. after F5). Cache entries expire after an hour.
- Fix export-to-file timing in the client - it now downloads on actual
  completion instead of immediately after the background enqueue returns.
- De-static a shared Stopwatch in SyncActionService that made elapsed-time
  tracking a cross-run/cross-user race; key it by requestId instead.
- Make status polling staleness-aware: a crashed/restarted server used to
  leave the client stuck reporting "running" indefinitely (Umbraco's
  GetStatusAsync doesn't expire a stale Enqueued/Running row the way
  GetByTypeAsync does). Now self-heals within Umbraco's expiration window,
  plus a manual "reset this view" escape hatch in the UI for when that's not
  fast enough.
- Poll only as a SignalR fallback (not unconditionally every 2s) and back
  off while a run drags on - the earlier fixed interval was itself enough
  concurrent DB traffic to trigger SQLite 'table is locked' errors during a
  background import.
- Keep ProcessingMode defaulting to Normal - this is opt-in, not a default
  change mid-version. Document the SQLite locking caveat for anyone who
  turns Background mode on (docs/perf/background-processing-mode.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Polish the background-run banner UI

- Replace the full-width "reset this view" button with a compact
  icon-only dismiss (top-right of the alert), styled to match the
  banner's own colours so it blends in rather than competing for
  attention - it's a rarely-needed escape hatch, not a primary action.
- Move the banner to the top of the page so it's the first thing seen
  regardless of scroll position or other banners/content below it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Make ISyncManagementService additions truly non-breaking

Give GetOperationStatusAsync/GetRunningOperationAsync default interface
implementations instead of requiring every implementer to add them.
ISyncManagementService is public, so an external implementation of it
would otherwise fail to compile against this change; the defaults report
"not supported"/"nothing running" - safe, honest answers for an
implementation that predates background processing - and uSyncManagementService's
own explicit overrides still take precedence as normal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Support a load-balanced backoffice in background processing mode

Background mode already ran the full handler pipeline on one server, but
progress/status was served from an in-process cache, so a status request
landing on a different server (round-robin, no sticky sessions) came back
empty. The client also gated its polling fallback on the SignalR socket
being "connected", which under a load-balanced backoffice is true even when
the socket is pinned to the wrong server - so a completed run could hang the
UI forever.

- Mirror background-run progress into the shared umbracoLongRunningOperation
  row (summaries on every step, full results once complete) so status/running
  checks resolve correctly from any server.
- Poll on SignalR message *liveness*, not socket state, so a dead-in-practice
  connection falls back to polling instead of hanging.
- Wrap the startup import in ILongRunningOperationService.RunAsync so exactly
  one server runs it - the previous ServerRole.Subscriber-only guard doesn't
  fire under the documented load-balanced-backoffice setup, where every
  server reports SchedulingPublisher.
- Warn at startup if running Normal mode with multiple active servers, since
  that mode can't be made safe under a load-balanced backoffice.
- Document the load-balancing requirements and limitations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Tighten background-mode poll interval to 1s (from 5s)

Now that polling is gated on SignalR message liveness rather than being
the sole progress mechanism, a 5s starting interval leaves the progress
bar visibly frozen for the first several seconds of any run whose socket
isn't delivering (e.g. under a load-balanced backoffice with no SignalR
backplane). Confirmed via manual load-balanced testing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Surface a progress message during polling fallback, not just SignalR

usync-progress-box only ever populated its message line from a live
SignalR 'update' push. Under polling fallback (e.g. a background run
whose SignalR socket is pinned to a different server behind a load
balancer), that left the box blank for the whole run even though the
handler icons themselves were updating correctly.

Add a small public setter on uSyncSignalRContext that publishes into the
same update state/observable the progress box already consumes, so no
component changes are needed. The status poll calls it with the
per-handler-step message ("Processing Import"/"Completed") whenever a
poll returns one - coarser than SignalR's per-item message, but visibly
better than nothing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* update message / fix obsolete

* Change processing message.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…views fallback (#1059)

When importing a template we now ask Umbraco's ITemplateService for the file
content first (the way Umbraco does it), before falling back to the view
filesystem, and finally to a placeholder when the views are compiled.

Two things had to be fixed for that fallback to actually work:

* TemplateService.GetFileContentStreamAsync never returns null - the repository
  hands back Stream.Null when the file is missing. Checking for null meant we
  always took the "found it" branch, read an empty string, and created the
  template with no content - so the view filesystem fallback and the
  compiled-views placeholder below it were unreachable, and a genuinely missing
  template file was silently imported as an empty one instead of failing.

* The placeholder we hand Umbraco is parsed by TemplateContentParserService to
  work out the master template, and its regex requires a trailing semi-colon.
  "{ Layout = "master" }" never matched, so the parent was never set. It is now
  written as valid razor - @{ Layout = "master.cshtml"; } - and root templates
  get Layout = null; rather than an empty alias.

Also aligns ViewPath (and the handler's equivalent) with how Umbraco names the
view file - the alias verbatim, see TemplateRepository.SetVirtualPath - rather
than stripping spaces, so we don't look for or delete the wrong file.

Tidy up while in here:
 * remove the CleanseNode override - it looked for a "Content" element, but the
   element is "Contents", so it has never done anything. TemplateTracker
   explicitly tracks /Contents, so stripping it now would make the hash and the
   tracker disagree - behaviour is unchanged, the dead code is gone.
 * drop the unused _shortStringHelper field (constructor signature kept)
 * flatten the dead null checks in ShouldGetContentFromNode
 * make GetContentFromFile private and tidy its stream disposal
 * remove a stray Lucene using

Tests: covers creating a child template when there is no file on disk, asserting
the placeholder parses back to the parent alias using Umbraco's own parser, plus
the root-template and views-not-compiled cases. BuildFileSystems now sets up
IIOHelper.PathStartsWith, without which PhysicalFileSystem throws instead of
reporting a missing file.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…view (#1060)

Word-diffing pretty-printed JSON gave users a wall of text for a single
block insertion or property edit inside a block list/grid. Classify each
change detail (masked, single-sided, JSON, XML/text lines, word, scalar)
and render JSON changes as a flat list of leaf changes, matching block
array entries by identity (key/udi/alias+culture) rather than index so a
single insertion doesn't make every element look changed. Long text/XML
gets a hand-rolled line diff with collapsed unchanged runs, since Umbraco
only re-exports diffWords. Each change is now an expandable row instead
of a table cell, collapsed by default once there is more than one, with
a raw-values toggle for the full old/new text.

Also fixes two accidental-precedence bugs in the change count check, and
a case where Error/Warning notice details were word-diffed against ''.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Wires up package-build.yml per Jumoo/nightly-feed-config: repacks
each package with the {majorMinorPatch}-build.{run_number} version
scheme and pushes to https://nightly.jumoo.uk on every push to
v17/main, gated to push events so a workflow_dispatch rehearsal
never publishes a real nightly build.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
It still listed the old gulp build toolchain (gulp, gulp-sourcemaps,
etc.) even though package.json dropped those dependencies long ago.
No workflow installs from the repo root (only usync-assets and
history-client), so the lockfile was just dead weight — and it's what
Dependabot's security-update job kept failing on, since it can't
upgrade postcss past 7.x while @gulp-sourcemaps/identity-map pins it
to ^7.0.16.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…terTemplateAlias renamed to LayoutTemplateAlias

Forward-ported from v17's #1059 (Get template content from the template
service, and fix the compiled-views fallback). Umbraco 18's
ITemplateContentParserService renamed MasterTemplateAlias to
LayoutTemplateAlias; uSync.BackOffice/SyncHandlers/Handlers/TemplateHandler.cs
already used the new name, only these tests still referenced the old one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@KevinJump
KevinJump merged commit ec9a9a4 into v18/main Sep 8, 2026
5 checks passed
@KevinJump
KevinJump deleted the merge/v17-to-v18-2026-09-08 branch September 8, 2026 10:02
@KevinJump KevinJump mentioned this pull request Sep 8, 2026
5 tasks
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.

1 participant