Skip to content

Commit 744fda9

Browse files
iammukeshmjarvis
andauthored
feat(cli): implement upgrade apply (Sprint 3) (#1197)
- Add PackageUpdater service for updating Directory.Packages.props - Implement 'fsh upgrade --apply' functionality: - Fetches latest release and compares versions - Creates backup before making changes - Updates package versions in Directory.Packages.props - Adds new packages from latest release - Shows warnings for removed packages (manual review) - Updates manifest with new version and timestamp - Supports --dry-run for preview without changes - Supports --skip-breaking to skip breaking changes - Supports --force to skip confirmation - Offers rollback on failure Sprint 3 deliverables: - [x] Package version updater - [x] Safe (non-breaking) auto-apply with --skip-breaking - [x] Backup and restore functionality - [x] Interactive confirmation (skippable with --force) - [x] Dry run mode Co-authored-by: jarvis <jarvis@codewithmukesh.com>
1 parent 2887f64 commit 744fda9

2 files changed

Lines changed: 521 additions & 13 deletions

File tree

src/Tools/CLI/Commands/UpgradeCommand.cs

Lines changed: 261 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -255,28 +255,276 @@ await AnsiConsole.Status()
255255

256256
private static async Task<int> ApplyUpgradesAsync(FshManifest manifest, Settings settings, CancellationToken cancellationToken)
257257
{
258-
// TODO: Sprint 3 - Implement upgrade apply
259-
// 1. Fetch latest release
260-
// 2. Update Directory.Packages.props
261-
// 3. For code changes, show diff and ask confirmation
262-
// 4. Update manifest with new versions
258+
using var githubService = new GitHubReleaseService();
259+
260+
// Fetch latest release
261+
GitHubRelease? latestRelease = null;
262+
await AnsiConsole.Status()
263+
.Spinner(Spinner.Known.Dots)
264+
.StartAsync("Fetching latest release...", async ctx =>
265+
{
266+
if (settings.IncludePrerelease)
267+
{
268+
var releases = await githubService.GetReleasesAsync(10, cancellationToken);
269+
latestRelease = releases.FirstOrDefault();
270+
}
271+
else
272+
{
273+
latestRelease = await githubService.GetLatestReleaseAsync(cancellationToken);
274+
}
275+
});
276+
277+
if (latestRelease == null)
278+
{
279+
AnsiConsole.MarkupLine("[red]Error:[/] Could not fetch release information from GitHub.");
280+
return 1;
281+
}
282+
283+
var latestVersion = latestRelease.Version;
284+
var comparison = VersionComparer.CompareVersions(manifest.FshVersion, latestVersion);
285+
286+
if (comparison >= 0)
287+
{
288+
AnsiConsole.MarkupLine("[green]✓[/] Already up to date!");
289+
return 0;
290+
}
291+
292+
AnsiConsole.MarkupLine($"[dim]Upgrading:[/] [yellow]{manifest.FshVersion}[/] → [green]{latestVersion}[/]");
293+
AnsiConsole.WriteLine();
294+
295+
// Get package diff
296+
var currentPackagesProps = await GetLocalPackagesPropsAsync(settings.Path);
297+
var latestPackagesProps = await githubService.GetPackagesPropsAsync(latestRelease.TagName, cancellationToken);
298+
299+
if (currentPackagesProps == null)
300+
{
301+
AnsiConsole.MarkupLine("[red]Error:[/] Could not read Directory.Packages.props");
302+
return 1;
303+
}
304+
305+
if (latestPackagesProps == null)
306+
{
307+
AnsiConsole.MarkupLine("[red]Error:[/] Could not fetch latest Directory.Packages.props from GitHub");
308+
return 1;
309+
}
310+
311+
var currentVersions = VersionComparer.ParsePackagesProps(currentPackagesProps);
312+
var latestVersions = VersionComparer.ParsePackagesProps(latestPackagesProps);
313+
var diff = VersionComparer.Compare(currentVersions, latestVersions);
314+
315+
if (!diff.HasChanges)
316+
{
317+
AnsiConsole.MarkupLine("[dim]No package changes detected.[/]");
318+
319+
// Still update manifest version
320+
if (!settings.DryRun)
321+
{
322+
await PackageUpdater.UpdateManifestAsync(settings.Path, latestVersion, cancellationToken);
323+
AnsiConsole.MarkupLine("[green]✓[/] Updated manifest version.");
324+
}
325+
return 0;
326+
}
263327

264-
AnsiConsole.MarkupLine("[yellow]⚠ Upgrade apply not yet implemented[/]");
328+
// Show what will be changed
329+
AnsiConsole.MarkupLine("[blue]Changes to apply:[/]");
265330
AnsiConsole.WriteLine();
266-
AnsiConsole.MarkupLine("[dim]Coming in Sprint 3:[/]");
267-
AnsiConsole.MarkupLine("[dim] • Package version updater[/]");
268-
AnsiConsole.MarkupLine("[dim] • Safe (non-breaking) auto-apply[/]");
269-
AnsiConsole.MarkupLine("[dim] • Interactive diff viewer[/]");
331+
332+
if (diff.Updated.Count > 0)
333+
{
334+
var updateTable = new Table()
335+
.Border(TableBorder.Simple)
336+
.AddColumn("Package")
337+
.AddColumn("From")
338+
.AddColumn("To")
339+
.AddColumn("Status");
340+
341+
foreach (var update in diff.Updated.OrderBy(u => u.Package))
342+
{
343+
var willSkip = settings.SkipBreaking && update.IsBreaking;
344+
345+
string status;
346+
if (!update.IsBreaking)
347+
status = "[green]Safe[/]";
348+
else if (willSkip)
349+
status = "[yellow]Skip (breaking)[/]";
350+
else
351+
status = "[red]Breaking[/]";
352+
353+
var packageName = willSkip ? $"[strikethrough dim]{update.Package}[/]" : update.Package;
354+
355+
updateTable.AddRow(
356+
packageName,
357+
update.FromVersion,
358+
$"[green]{update.ToVersion}[/]",
359+
status);
360+
}
361+
362+
AnsiConsole.Write(updateTable);
363+
}
364+
365+
if (diff.Added.Count > 0)
366+
{
367+
AnsiConsole.WriteLine();
368+
AnsiConsole.MarkupLine($"[green]+[/] {diff.Added.Count} new packages will be added");
369+
}
370+
371+
if (diff.Removed.Count > 0)
372+
{
373+
AnsiConsole.WriteLine();
374+
AnsiConsole.MarkupLine($"[yellow]![/] {diff.Removed.Count} packages are no longer in the latest release (manual review needed)");
375+
}
376+
270377
AnsiConsole.WriteLine();
271378

379+
// Dry run mode - stop here
272380
if (settings.DryRun)
273381
{
274-
AnsiConsole.MarkupLine("[dim]Dry run mode - no changes would be made[/]");
382+
AnsiConsole.MarkupLine("[yellow]Dry run mode[/] - no changes were made.");
383+
return 0;
275384
}
276385

277-
if (settings.SkipBreaking)
386+
// Confirm unless forced
387+
if (!settings.Force)
278388
{
279-
AnsiConsole.MarkupLine("[dim]Skip breaking mode - would skip breaking changes[/]");
389+
var confirm = await AnsiConsole.ConfirmAsync("Apply these changes?", false, cancellationToken);
390+
if (!confirm)
391+
{
392+
AnsiConsole.MarkupLine("[dim]Cancelled.[/]");
393+
return 0;
394+
}
395+
}
396+
397+
// Create backup
398+
AnsiConsole.MarkupLine("[dim]Creating backup...[/]");
399+
var backupPath = await PackageUpdater.CreateBackupAsync(settings.Path, cancellationToken);
400+
401+
if (backupPath == null)
402+
{
403+
AnsiConsole.MarkupLine("[yellow]⚠[/] Could not create backup. Continue anyway?");
404+
if (!settings.Force)
405+
{
406+
var continueAnyway = await AnsiConsole.ConfirmAsync("Continue without backup?", false, cancellationToken);
407+
if (!continueAnyway)
408+
{
409+
AnsiConsole.MarkupLine("[dim]Cancelled.[/]");
410+
return 0;
411+
}
412+
}
413+
}
414+
else
415+
{
416+
AnsiConsole.MarkupLine($"[dim]Backup created:[/] {backupPath}");
417+
}
418+
419+
// Apply updates
420+
var updateOptions = new UpdateOptions
421+
{
422+
DryRun = false,
423+
SkipBreaking = settings.SkipBreaking,
424+
Force = settings.Force
425+
};
426+
427+
UpdateResult result;
428+
await AnsiConsole.Status()
429+
.Spinner(Spinner.Known.Dots)
430+
.StartAsync("Applying updates...", async ctx =>
431+
{
432+
result = await PackageUpdater.UpdatePackagesPropsAsync(
433+
settings.Path,
434+
diff,
435+
updateOptions,
436+
cancellationToken);
437+
});
438+
439+
result = await PackageUpdater.UpdatePackagesPropsAsync(
440+
settings.Path,
441+
diff,
442+
updateOptions,
443+
cancellationToken);
444+
445+
// Show results
446+
AnsiConsole.WriteLine();
447+
448+
if (result.Success)
449+
{
450+
AnsiConsole.MarkupLine("[green]✓[/] Packages updated successfully!");
451+
AnsiConsole.WriteLine();
452+
453+
if (result.Updated.Count > 0)
454+
{
455+
AnsiConsole.MarkupLine($"[green]Updated:[/] {result.Updated.Count} packages");
456+
}
457+
458+
if (result.Added.Count > 0)
459+
{
460+
AnsiConsole.MarkupLine($"[green]Added:[/] {result.Added.Count} packages");
461+
}
462+
463+
if (result.Skipped.Count > 0)
464+
{
465+
AnsiConsole.MarkupLine($"[yellow]Skipped:[/] {result.Skipped.Count} packages (breaking changes)");
466+
}
467+
468+
// Update manifest
469+
var manifestUpdated = await PackageUpdater.UpdateManifestAsync(settings.Path, latestVersion, cancellationToken);
470+
if (manifestUpdated)
471+
{
472+
AnsiConsole.MarkupLine("[green]✓[/] Manifest updated.");
473+
}
474+
475+
// Show warnings
476+
if (result.Warnings.Count > 0)
477+
{
478+
AnsiConsole.WriteLine();
479+
AnsiConsole.MarkupLine("[yellow]Warnings:[/]");
480+
foreach (var warning in result.Warnings)
481+
{
482+
AnsiConsole.MarkupLine($" [yellow]![/] {warning}");
483+
}
484+
}
485+
486+
// Next steps
487+
AnsiConsole.WriteLine();
488+
AnsiConsole.MarkupLine("[dim]Next steps:[/]");
489+
AnsiConsole.MarkupLine(" 1. Run [green]dotnet restore[/] to restore packages");
490+
AnsiConsole.MarkupLine(" 2. Run [green]dotnet build[/] to verify the upgrade");
491+
AnsiConsole.MarkupLine(" 3. Review and test your application");
492+
493+
if (backupPath != null)
494+
{
495+
AnsiConsole.WriteLine();
496+
AnsiConsole.MarkupLine($"[dim]To rollback:[/] restore from {backupPath}");
497+
}
498+
}
499+
else
500+
{
501+
AnsiConsole.MarkupLine("[red]✗[/] Upgrade failed!");
502+
503+
foreach (var error in result.Errors)
504+
{
505+
AnsiConsole.MarkupLine($" [red]Error:[/] {error}");
506+
}
507+
508+
// Offer to restore backup
509+
if (backupPath != null)
510+
{
511+
AnsiConsole.WriteLine();
512+
var restore = await AnsiConsole.ConfirmAsync("Restore from backup?", true, cancellationToken);
513+
if (restore)
514+
{
515+
var restored = await PackageUpdater.RestoreBackupAsync(backupPath, cancellationToken);
516+
if (restored)
517+
{
518+
AnsiConsole.MarkupLine("[green]✓[/] Restored from backup.");
519+
}
520+
else
521+
{
522+
AnsiConsole.MarkupLine($"[red]✗[/] Could not restore. Manual restore needed from: {backupPath}");
523+
}
524+
}
525+
}
526+
527+
return 1;
280528
}
281529

282530
return 0;

0 commit comments

Comments
 (0)