Skip to content

Commit 74cd584

Browse files
punkouter26Copilot
andcommitted
feat: prune removed Azure-hosted apps from apps.json during sync
Co-authored-by: Copilot <copilot@github.com>
1 parent e647533 commit 74cd584

3 files changed

Lines changed: 66 additions & 25 deletions

File tree

‎PoPunkouterSoftware.Client/wwwroot/data/apps.json‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,6 @@
7878
"url": "https://porobotstocks-porobotstocks-web.azurewebsites.net",
7979
"githubRepo": "punkouter5/PoRobotStocks"
8080
},
81-
{
82-
"id": "poappidea",
83-
"name": "PoAppIdea",
84-
"description": "App idea brainstorming tool",
85-
"category": "productivity",
86-
"status": "disabled",
87-
"technologies": [
88-
"Azure App Service",
89-
"Blazor"
90-
],
91-
"url": "https://poappidea-web.azurewebsites.net",
92-
"githubRepo": null
93-
},
9481
{
9582
"id": "pocouplequiz",
9683
"name": "PoCoupleQuiz",

‎PoPunkouterSoftware/Features/Azure/AppsJsonSyncer.cs‎

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,16 @@ public static async Task SyncAsync(AzureReport report, string appsJsonPath, Canc
4949

5050
var services = report.WebServices?.Services ?? new List<WebService>();
5151
var discoveredIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
52+
var discoveredHosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
5253

5354
// ── Process every service discovered in Azure ─────────────────────────
5455
foreach (var svc in services)
5556
{
5657
if (string.IsNullOrWhiteSpace(svc.Url)) continue;
5758
var slug = Slugify(svc.Name);
5859
discoveredIds.Add(slug);
60+
if (TryGetHost(svc.Url, out var discoveredHost))
61+
discoveredHosts.Add(discoveredHost);
5962

6063
var techs = InferTechnologies(svc.ResourceType);
6164

@@ -103,19 +106,24 @@ public static async Task SyncAsync(AzureReport report, string appsJsonPath, Canc
103106
}
104107
}
105108

106-
// ── Apps in apps.json that no longer exist in Azure ───────────────────
107-
// We deliberately do NOT auto-demote apps that aren't found — the user
108-
// controls status. Mark removed apps with a warning annotation in the ID.
109-
// (Deleted Azure resources stay visible until the user manually removes them.)
110-
var skippedUrls = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
111-
foreach (var app in apps)
109+
// ── Prune Azure-hosted apps that were removed from Azure ───────────────
110+
// Only prune when discovery returned at least one service to avoid wiping
111+
// apps.json due to transient auth/config failures that produce empty data.
112+
if (services.Count > 0)
112113
{
113-
// If this app's URL matches a discovered URL, skip marking
114-
if (!string.IsNullOrWhiteSpace(app.Url) && discoveredIds.Contains(app.Id)) continue;
115-
// If this app's slug matches a discovered slug, skip
116-
if (discoveredIds.Contains(app.Id)) continue;
117-
// Otherwise it's a stale entry — we leave it in place but flag it
118-
// (no auto-demotion as per intentional design)
114+
apps = apps
115+
.Where(app =>
116+
{
117+
if (!IsAzureHostedUrl(app.Url))
118+
return true;
119+
120+
var idMatched = !string.IsNullOrWhiteSpace(app.Id) && discoveredIds.Contains(app.Id);
121+
if (idMatched)
122+
return true;
123+
124+
return TryGetHost(app.Url, out var appHost) && discoveredHosts.Contains(appHost);
125+
})
126+
.ToList();
119127
}
120128

121129
// ── Sort and write back ───────────────────────────────────────────────
@@ -195,6 +203,27 @@ private static string Slugify(string name)
195203
return s.Trim('-');
196204
}
197205

206+
private static bool TryGetHost(string? url, out string host)
207+
{
208+
host = string.Empty;
209+
if (string.IsNullOrWhiteSpace(url))
210+
return false;
211+
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || string.IsNullOrWhiteSpace(uri.Host))
212+
return false;
213+
host = uri.Host;
214+
return true;
215+
}
216+
217+
private static bool IsAzureHostedUrl(string? url)
218+
{
219+
if (!TryGetHost(url, out var host))
220+
return false;
221+
222+
return host.EndsWith(".azurewebsites.net", StringComparison.OrdinalIgnoreCase)
223+
|| host.EndsWith(".azurestaticapps.net", StringComparison.OrdinalIgnoreCase)
224+
|| host.EndsWith(".azurecontainerapps.io", StringComparison.OrdinalIgnoreCase);
225+
}
226+
198227
// ── Local model types (only used for apps.json serialisation) ─────────────
199228

200229
private class AppsWrapper

‎tests/PoPunkouterSoftware.Tests.Unit/AppsJsonSyncerTests.cs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,31 @@ public async Task SyncAsync_EmptyReport_PreservesExistingApps()
261261
doc.RootElement.GetProperty("apps").GetArrayLength().Should().Be(1);
262262
}
263263

264+
[Fact]
265+
public async Task SyncAsync_RemovedAzureHostedApp_IsPruned_WhenDiscoverySucceeds()
266+
{
267+
var path = TempPath("apps.json");
268+
var initial =
269+
"{" +
270+
"\"apps\":[" +
271+
"{\"id\":\"poappidea\",\"name\":\"PoAppIdea\",\"description\":\"desc\",\"status\":\"inactive\",\"url\":\"https://poappidea-web.azurewebsites.net\",\"technologies\":[\"Azure\"]}," +
272+
"{\"id\":\"keep-app\",\"name\":\"KeepApp\",\"description\":\"desc\",\"status\":\"active\",\"url\":\"https://keep-app.azurewebsites.net\",\"technologies\":[\"Azure\"]}" +
273+
"]}";
274+
await File.WriteAllTextAsync(path, initial);
275+
276+
var report = BuildReport(("keep-app", "https://keep-app.azurewebsites.net", "microsoft.web/sites"));
277+
await AppsJsonSyncer.SyncAsync(report, path);
278+
279+
var json = await File.ReadAllTextAsync(path);
280+
var doc = JsonDocument.Parse(json);
281+
var ids = doc.RootElement.GetProperty("apps")
282+
.EnumerateArray()
283+
.Select(a => a.GetProperty("id").GetString())
284+
.ToList();
285+
286+
ids.Should().ContainSingle().Which.Should().Be("keep-app");
287+
}
288+
264289
[Fact]
265290
public async Task SyncAsync_MissingAppsJson_CreatesNewFile()
266291
{

0 commit comments

Comments
 (0)