Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 20 additions & 16 deletions src/NewsletterGenerator/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,20 +151,6 @@ public override async Task<int> ExecuteAsync(CommandContext context, CommandSett
var models = await NewsletterApp.PrintCopilotStartupStatusAsync();
var healthy = models != null && models.Count > 0;

// Lightweight connectivity ping
try
{
await using var client = new CopilotClient();
await client.StartAsync();
var ping = await client.PingAsync();
AnsiConsole.MarkupLine($"[green]✓[/] Ping: OK");
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"[yellow]⚠[/] Ping failed: {Markup.Escape(ex.Message)}");
healthy = false;
}

if (healthy)
AnsiConsole.MarkupLine("[green]Environment checks passed.[/]");
else
Expand Down Expand Up @@ -1385,6 +1371,8 @@ await AnsiConsole.Progress().AutoClear(false).HideCompleted(false).StartAsync(as
metrics,
"Generate: Project updates");

await Task.WhenAll(newsSectionTask, releaseSectionTask);

newsSection = await newsSectionTask;

releaseSection = await releaseSectionTask;
Expand Down Expand Up @@ -1466,10 +1454,26 @@ public static string FindRepoRoot(string startDir)

private static int CountSections(string content)
{
var headingCount = content.Split('\n')
var lines = content.Split('\n');

var headingCount = lines
.Count(line => line.StartsWith("## ", StringComparison.Ordinal) || line.StartsWith("### ", StringComparison.Ordinal));

return headingCount + 1; // Include the welcome section.
// Treat any non-empty content before the first heading as a "welcome" section.
var hasWelcomeSection = false;
foreach (var line in lines)
{
if (line.StartsWith("## ", StringComparison.Ordinal) || line.StartsWith("### ", StringComparison.Ordinal))
break;

if (!string.IsNullOrWhiteSpace(line))
{
hasWelcomeSection = true;
break;
}
}

return headingCount + (hasWelcomeSection ? 1 : 0);
}

private static async Task<T> RunTrackedTaskAsync<T>(
Expand Down
20 changes: 12 additions & 8 deletions src/NewsletterGenerator/Services/CacheService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ public class CacheService(ILogger<CacheService> logger, string? cacheDirectory =
private readonly bool _forceRefresh = forceRefresh;
private readonly ConcurrentDictionary<string, CacheSectionMetric> _sectionMetrics = new(StringComparer.OrdinalIgnoreCase);

public int CacheHits { get; private set; }
public int CacheMisses { get; private set; }
public int CacheSkips { get; private set; }
private int _cacheHits;
private int _cacheMisses;
private int _cacheSkips;

public int CacheHits => Volatile.Read(ref _cacheHits);
public int CacheMisses => Volatile.Read(ref _cacheMisses);
public int CacheSkips => Volatile.Read(ref _cacheSkips);

public IReadOnlyList<CacheSectionMetric> GetSectionMetrics() =>
_sectionMetrics.Values
Expand All @@ -39,7 +43,7 @@ public static string GetContentHash(string content)
{
if (_forceRefresh)
{
CacheSkips++;
Interlocked.Increment(ref _cacheSkips);
RecordReadOutcome(cacheKey, "skip");
ServiceLogMessages.CacheSkipForceRefresh(logger, cacheKey);
return null;
Expand All @@ -50,7 +54,7 @@ public static string GetContentHash(string content)

if (!File.Exists(cacheFile))
{
CacheMisses++;
Interlocked.Increment(ref _cacheMisses);
RecordReadOutcome(cacheKey, "miss");
ServiceLogMessages.CacheMissNoFile(logger, cacheKey);
return null;
Expand All @@ -63,19 +67,19 @@ public static string GetContentHash(string content)

if (cached?.SourceHash == sourceHash)
{
CacheHits++;
Interlocked.Increment(ref _cacheHits);
RecordReadOutcome(cacheKey, "hit", cached.Content.Length);
ServiceLogMessages.CacheHit(logger, cacheKey, sourceHash[..12], cached.Content.Length);
return cached.Content;
}

CacheMisses++;
Interlocked.Increment(ref _cacheMisses);
RecordReadOutcome(cacheKey, "mismatch");
ServiceLogMessages.CacheMissHashMismatch(logger, cacheKey, sourceHash[..12], cached?.SourceHash?[..12]);
}
catch (Exception ex)
{
CacheMisses++;
Interlocked.Increment(ref _cacheMisses);
RecordReadOutcome(cacheKey, "error");
ServiceLogMessages.CacheReadFailed(logger, ex, cacheKey);
}
Expand Down
17 changes: 7 additions & 10 deletions src/NewsletterGenerator/Services/NewsletterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,23 @@ public class NewsletterService(ILogger<NewsletterService> logger)
{
private SessionHooks CreateSessionHooks() => new()
{
OnErrorOccurred = async (input, invocation) =>
OnErrorOccurred = (input, invocation) =>
{
logger.LogWarning("Session error in {Context}: {Error}", input.ErrorContext, input.Error);
await Task.CompletedTask;
return new ErrorOccurredHookOutput
return Task.FromResult<ErrorOccurredHookOutput?>(new ErrorOccurredHookOutput
{
ErrorHandling = "retry"
};
});
},
OnSessionStart = async (input, invocation) =>
OnSessionStart = (input, invocation) =>
{
logger.LogDebug("Session started (source={Source})", input.Source);
await Task.CompletedTask;
return new SessionStartHookOutput();
return Task.FromResult<SessionStartHookOutput?>(new SessionStartHookOutput());
},
OnSessionEnd = async (input, invocation) =>
OnSessionEnd = (input, invocation) =>
{
logger.LogDebug("Session ended (reason={Reason})", input.Reason);
await Task.CompletedTask;
return null;
return Task.FromResult<SessionEndHookOutput?>(null);
}
};

Expand Down
Loading