Skip to content

Various Publish Profile Fixes Plus Multi-Profile Publish operation - #257

Open
aritchie wants to merge 2 commits into
mainfrom
aritchie/pub-profiles
Open

Various Publish Profile Fixes Plus Multi-Profile Publish operation#257
aritchie wants to merge 2 commits into
mainfrom
aritchie/pub-profiles

Conversation

@aritchie

Copy link
Copy Markdown
Collaborator

This pull request introduces several enhancements and fixes across the publish profiles, secrets publishing, and UI layers. The most significant changes include improving publish profile loading performance and responsiveness, adding batch publishing UI, and addressing various UI and code reliability issues.

Publish Profile Loading and Caching Improvements:

  • Refactored PublishProfileService to load profiles from multiple providers in parallel, reporting progress incrementally so the UI can display fast providers' profiles without waiting for slow ones. Added new overloads for GetProfilesAsync and a new RefreshProfilesAsync method, and improved cache invalidation logic. [1] [2] [3] [4]
  • Introduced a maximum parallel secret read limit (MaxParallelSecretReads) to prevent resource exhaustion when reading profiles.
  • Added a utility method GetComparableStorageKey in SecretItemAdapterHelper to reliably compare profile keys across providers with different key formats.

Secrets Publisher Reliability:

  • Updated SecretsPublisherService to ensure publisher instances are resolved asynchronously, preventing failures when the cache is uninitialized. This affects methods like TestConnectionAsync, ListRepositoriesAsync, and PublishSecretsAsync. [1] [2] [3]

UI and Modal Enhancements:

  • Added a new BatchPublishPage modal for publishing multiple profiles at once, with platform-specific sizing for macOS and Linux.
  • Improved the wizard form page's primary button to update its width when the label changes, preventing text clipping (e.g., switching from "Next →" to "Publish"). [1] [2]
  • Updated toolbar and UI components to add "Select" and "Publish Selected" actions and improved popover positioning in the provider picker to handle dynamic content. [1] [2] [3]

Bug Fixes and Minor Improvements:

  • Fixed profile loading spinner and empty state logic to better handle loading and empty results.
  • Added a RequestCancellation method to the IRunnable interface for improved operation cancellation support.

These changes collectively improve the responsiveness, reliability, and usability of publish profile management and secrets publishing in the application.

Add a batch publish flow for publish profiles, including selection mode in the secrets UI and a new modal that re-resolves and publishes each profile sequentially. The publish profile service now supports progressive refresh/loading, avoids redundant provider reads, and handles cache invalidation during sync without returning an empty list. Also reposition secret provider popovers while open so they stay aligned as content or viewport size changes.
Ensure secrets publishing works from a cold start by loading configured publishers before test/list/publish calls, with coverage for the missing-publisher path. Also avoid rebuilding the native toolbar when only enabled state changes, add the missing macOS toolbar actions, and polish the publish UI so button labels resize correctly and batch publish errors wrap and stand out.
Copilot AI lite review requested due to automatic review settings September 10, 2026 21:09

Copilot AI 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.

🟡 Changes recommended

Unresolved moderate issues affect profile loading, refresh behavior, selection, and batch publishing.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request improves publish-profile loading, caching, publisher resolution, batch publishing, and related UI behavior.

Changes:

  • Adds provider loading, progress reporting, caching, throttling, and key normalization.
  • Adds multi-profile selection and sequential batch publishing.
  • Improves publisher initialization, popovers, toolbar actions, wizard buttons, and cancellation support.
File summaries
File Summary Final findings
tests/MauiSherpa.Core.Tests/Services/SecretsPublisherServiceTests.cs Tests cold publisher resolution. None.
tests/MauiSherpa.Core.Tests/Services/PublishProfileServiceTests.cs Tests cache invalidation and provider deduplication. None.
src/MauiSherpa/wwwroot/js/mauiSherpaPopovers.js Adds dynamic popover positioning. None.
src/MauiSherpa/Pages/SecretsPublish.razor Adds profile selection and progressive loading. 5 moderate findings: loading-state handling, cancellation disposal race, partial-snapshot selection loss, refresh visibility, and stale progress callbacks. 1 nit: selection checkbox lacks an accessible name.
src/MauiSherpa/Pages/Modals/BatchPublishPage.cs Defines the batch publish modal page. None.
src/MauiSherpa/Pages/Modals/BatchPublishModal.razor Implements sequential batch publishing. Moderate (1 vote): preserving provider-specific RepositoryId is required for GitLab and Azure DevOps publishing.
src/MauiSherpa/Pages/Forms/WizardFormPage.cs Fixes primary-button measurement after label changes. None.
src/MauiSherpa/Components/SecretProviderPicker.razor Repositions dynamic provider popovers. None.
src/MauiSherpa.MacOS/BlazorContentPage.cs Registers native toolbar actions. None.
src/MauiSherpa.Core/Services/SecretsPublisherService.cs Resolves publishers asynchronously. None.
src/MauiSherpa.Core/Services/SecretItemAdapterHelper.cs Adds comparable storage-key normalization. None.
src/MauiSherpa.Core/Services/PublishProfileService.cs Implements multi-provider loading and caching. 3 moderate findings: IDs are claimed before successful reads, provider reads remain effectively serialized, and empty partial snapshots can prematurely clear the profile state.
src/MauiSherpa.Core/Interfaces.cs Extends service and cancellation contracts. None.
Review details

Suppressed comments (7)

src/MauiSherpa.Core/Services/PublishProfileService.cs:145

  • This loop awaits provider listings and then reads each provider in priority order. Starting the listing tasks together does not make the profile reads parallel: a slow first provider blocks every later provider's GetSecretAsync calls and progress report, so a fast provider cannot paint until the slow one finishes. Load provider results as they complete and apply the priority rule when merging duplicate profiles.
        foreach (var listingTask in listings)
        {
            var listing = await listingTask;
            if (listing.Provider is null)

src/MauiSherpa.Core/Services/PublishProfileService.cs:163

  • This reports an empty partial snapshot after a provider contributes no readable profiles. SecretsPublish treats every progress callback as the end of initial loading and applies the snapshot, so an empty fast provider can show the “No Publish Profiles” state while a slower provider still has profiles to return. Suppress empty progress snapshots (or add an explicit completion signal) and let the final result clear the list.
            progress?.Report(Snapshot(profiles));

src/MauiSherpa/Pages/Modals/BatchPublishModal.razor:506

  • This drops the stored RepositoryId and always publishes using RepositoryFullName. That only works for providers whose API uses a path; GitLab parses a numeric project ID and Azure DevOps parses a GUID, while their repository listings explicitly store those values in Id and the display name in FullName. Batch publishing any GitLab or Azure DevOps destination will therefore fail; preserve RepositoryId in DestinationVM and pass the provider-specific identifier when publishing.
                    destination.RepositoryFullName,

src/MauiSherpa/Pages/SecretsPublish.razor:253

  • The previous CTS is disposed immediately after cancellation, but its already-queued InvokeAsync delegate can still reach debounce.Token at line 259. Reading Token from a disposed CancellationTokenSource throws ObjectDisposedException, which is not caught here, producing an unobserved task failure during a burst of sync events. Capture the token before scheduling so disposal cannot race with token access.
        previous?.Dispose();

src/MauiSherpa/Pages/SecretsPublish.razor:276

  • An explicit toolbar refresh calls LoadProfilesAsync with background set to false. When profiles already exist, this branch sets only isLoading, but the page displays that indicator only when profiles.Count == 0; isRefreshing remains false, so the refresh runs with no visible status while the grid is stale. Treat any reload with existing profiles as a refresh.
        if (background && profiles.Count > 0)
            isRefreshing = true;
        else
            isLoading = true;

src/MauiSherpa/Pages/SecretsPublish.razor:76

  • The profile-selection checkbox is wrapped in a label with no text and has no accessible name. The label's title is not a reliable accessible name for the input, so screen readers will announce an unlabeled checkbox; add an aria-label that includes the profile name.
                            <input type="checkbox"
                                   checked="@selectedProfileIds.Contains(profile.Id)"
                                   @onchange="@(e => ToggleProfileSelection(profile.Id, e.Value is bool on && on))" />

src/MauiSherpa/Pages/SecretsPublish.razor:287

  • Progress<T>.Report is asynchronous here, and the handler adds another fire-and-forget InvokeAsync. The final load can therefore apply the complete list and clear isRefreshing before this queued callback runs; because the sequence is unchanged, the stale partial snapshot can overwrite the final list and leave the page showing refresh indefinitely. Synchronize partial updates with finalization or invalidate callbacks once the final result is applied.
            var progress = new Progress<IReadOnlyList<PublishProfile>>(partial =>
                _ = InvokeAsync(() =>
                {
                    if (sequence != loadSequence)
                        return;
  • Files reviewed: 13/13 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +151 to +153
var keysToRead = listing.Keys
.Where(key => !TryGetComparableProfileId(key, out var id) || claimedIds.Add(id))
.ToList();
</div>
}
else if (isLoading)
else if (isLoading && profiles.Count == 0)
Comment on lines +323 to +327
selectedProfileIds.IntersectWith(profiles.Select(profile => profile.Id));
if (isSelectionMode && profiles.Count == 0)
{
isSelectionMode = false;
UpdateToolbarItems();
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