Repository navigation
Conversation
Roslyn analyzer findings
|
|
IDE MCP code analysis (Rider) was unavailable for the #423 implementation pass: Rider is connected but the BuildBot window has no solution loaded (only "Miscellaneous Files"), so every file reports "not included in any project of the current solution". The full solution build with warnings as errors (all analyzers) passed instead. |
|
Owner decisions from live chat:
Both will be done as separate changes on this PR after the current review round. |
Owner request Q9 on PR #428. The Rx .Delay(InterMessageDelay) only shifted each message by 1s; it did not space messages. Concat still sends one at a time in order, and Discord rate limiting is handled by DiscordWebhookClient's 429 retry_after retry. BotServiceTests now wait on TaskCompletionSource signals bounded by WaitAsync timeouts instead of fixed 3s sleeps. Pattern Sweep: searched src for Rx .Delay, Task.Delay, Thread.Sleep, Observable.Timer/Interval, Throttle, Sample, SpinWait. Only remaining hit is DiscordWebhookClient's 429 back-off via the injected TimeProvider (different construct, required). Construct: Rx .Delay / fixed sleeps used as inter-message spacing Swept: none Refs #423.
Owner request Q10 on PR #428. With no bot connection left, the hosted service only drains the message queue and sends each message through the webhook client, so it is renamed to DiscordMessageSender (file, tests, DI registration, DI test name and a comment). DiscordBot, IDiscordBot and BotMessage are unchanged. Pattern Sweep: grep for BotService across the repo (excluding bin/obj/.git/CHANGELOG.md) finds no remaining references. Refs #423.
|
Re the Q9/Q10 owner decisions (#428 (comment)): |
|
Owner decision from live chat on simplify suggestion A (Q11):
It does use |
|
Owner decision from live chat on simplify suggestion 2:
Q12: |
|
Owner direction from live chat:
Q14: once this PR merges, raise a follow-up issue to enable |
|
Correction to my previous comment: the owner clarified in live chat
so the AOT/trimming follow-up is raised now as #430, to be worked after this PR merges. |
Owner decision Q11 on PR #428. The named IHttpClientFactory client's Polly policy now handles transient errors plus 429 Too Many Requests, waiting on the Retry-After header (delta or date, capped at 60s) and otherwise the existing exponential backoff. DiscordWebhookClient.SendAsync is a single POST; the hand-written retry loop, RateLimitResponse and the rate-limit log message are removed. Pattern Sweep: searched production src for hand-written HTTP retry loops (while(true), attempt counters, Task.Delay, Retry-After, TooManyRequests, AddHttpClient/CreateClient/new HttpClient, WaitAndRetry). The only other HTTP client (CloudFormation subscription confirmation) already uses a Polly policy; nothing else to change. Refs #423.
63f6664 to
c2e6699
Compare
Owner request Q9 on PR #428. The Rx .Delay(InterMessageDelay) only shifted each message by 1s; it did not space messages. Concat still sends one at a time in order, and Discord rate limiting is handled by DiscordWebhookClient's 429 retry_after retry. BotServiceTests now wait on TaskCompletionSource signals bounded by WaitAsync timeouts instead of fixed 3s sleeps. Pattern Sweep: searched src for Rx .Delay, Task.Delay, Thread.Sleep, Observable.Timer/Interval, Throttle, Sample, SpinWait. Only remaining hit is DiscordWebhookClient's 429 back-off via the injected TimeProvider (different construct, required). Construct: Rx .Delay / fixed sleeps used as inter-message spacing Swept: none Refs #423.
Owner request Q10 on PR #428. With no bot connection left, the hosted service only drains the message queue and sends each message through the webhook client, so it is renamed to DiscordMessageSender (file, tests, DI registration, DI test name and a comment). DiscordBot, IDiscordBot and BotMessage are unchanged. Pattern Sweep: grep for BotService across the repo (excluding bin/obj/.git/CHANGELOG.md) finds no remaining references. Refs #423.
Owner decision Q11 on PR #428. The named IHttpClientFactory client's Polly policy now handles transient errors plus 429 Too Many Requests, waiting on the Retry-After header (delta or date, capped at 60s) and otherwise the existing exponential backoff. DiscordWebhookClient.SendAsync is a single POST; the hand-written retry loop, RateLimitResponse and the rate-limit log message are removed. Pattern Sweep: searched production src for hand-written HTTP retry loops (while(true), attempt counters, Task.Delay, Retry-After, TooManyRequests, AddHttpClient/CreateClient/new HttpClient, WaitAndRetry). The only other HTTP client (CloudFormation subscription confirmation) already uses a Polly policy; nothing else to change. Refs #423.
CI Debugger: re-run started for build-pre-release |
Owner decision Q12 on PR #428. The sender now awaits the message channel in ExecuteAsync and sends each message in order, logging (never the webhook URL) and continuing when a send fails, so one exception can no longer end delivery; cancellation of the stopping token ends the loop cleanly. The Rx pipeline, constructor-started work and manual cancellation/dispose are removed. Pattern Sweep: searched src for Rx subscribe chains and constructor-started or unawaited background work (Observable/Subscribe/ToObservable, IHostedService/BackgroundService, Task.Run, timers, ContinueWith, Task.Factory, ThreadPool); DiscordMessageSender was the only hit. Refs #423.
9635ade to
8950a0b
Compare
Owner request Q9 on PR #428. The Rx .Delay(InterMessageDelay) only shifted each message by 1s; it did not space messages. Concat still sends one at a time in order, and Discord rate limiting is handled by DiscordWebhookClient's 429 retry_after retry. BotServiceTests now wait on TaskCompletionSource signals bounded by WaitAsync timeouts instead of fixed 3s sleeps. Pattern Sweep: searched src for Rx .Delay, Task.Delay, Thread.Sleep, Observable.Timer/Interval, Throttle, Sample, SpinWait. Only remaining hit is DiscordWebhookClient's 429 back-off via the injected TimeProvider (different construct, required). Construct: Rx .Delay / fixed sleeps used as inter-message spacing Swept: none Refs #423.
Owner request Q10 on PR #428. With no bot connection left, the hosted service only drains the message queue and sends each message through the webhook client, so it is renamed to DiscordMessageSender (file, tests, DI registration, DI test name and a comment). DiscordBot, IDiscordBot and BotMessage are unchanged. Pattern Sweep: grep for BotService across the repo (excluding bin/obj/.git/CHANGELOG.md) finds no remaining references. Refs #423.
Owner decision Q11 on PR #428. The named IHttpClientFactory client's Polly policy now handles transient errors plus 429 Too Many Requests, waiting on the Retry-After header (delta or date, capped at 60s) and otherwise the existing exponential backoff. DiscordWebhookClient.SendAsync is a single POST; the hand-written retry loop, RateLimitResponse and the rate-limit log message are removed. Pattern Sweep: searched production src for hand-written HTTP retry loops (while(true), attempt counters, Task.Delay, Retry-After, TooManyRequests, AddHttpClient/CreateClient/new HttpClient, WaitAndRetry). The only other HTTP client (CloudFormation subscription confirmation) already uses a Polly policy; nothing else to change. Refs #423.
Rename DiscordColour to DiscordColor, DiscordEmbed.Colour to Color and restore GithubStatusNotificationHandler.GetEmbedColor, per the cs-template Code Identifiers rule. Refs #423
Pattern Sweep for 97c618a (UK-spelled code identifiers for the Discord color concept, against the language rule that identifiers follow platform convention). Renamed the pre-existing ColourMapping record struct, its Colour member and the ColourMappings field to ColorMapping / Color / ColorMappings. Searched the repo for Colour excluding .git/obj/bin/CHANGELOG.md; no Colour identifiers remain in src. Construct: UK-spelled Colour identifiers for the Discord color concept src/BuildBot.GitHub/Publishers/GithubStatusNotificationHandler.cs: declares the ColourMapping record struct, its Colour member and the ColourMappings field. Refs #423.
Owner request Q9 on PR #428. The Rx .Delay(InterMessageDelay) only shifted each message by 1s; it did not space messages. Concat still sends one at a time in order, and Discord rate limiting is handled by DiscordWebhookClient's 429 retry_after retry. BotServiceTests now wait on TaskCompletionSource signals bounded by WaitAsync timeouts instead of fixed 3s sleeps. Pattern Sweep: searched src for Rx .Delay, Task.Delay, Thread.Sleep, Observable.Timer/Interval, Throttle, Sample, SpinWait. Only remaining hit is DiscordWebhookClient's 429 back-off via the injected TimeProvider (different construct, required). Construct: Rx .Delay / fixed sleeps used as inter-message spacing Swept: none Refs #423.
Owner request Q10 on PR #428. With no bot connection left, the hosted service only drains the message queue and sends each message through the webhook client, so it is renamed to DiscordMessageSender (file, tests, DI registration, DI test name and a comment). DiscordBot, IDiscordBot and BotMessage are unchanged. Pattern Sweep: grep for BotService across the repo (excluding bin/obj/.git/CHANGELOG.md) finds no remaining references. Refs #423.
Replaces the placeholder with Changed, Removed and Deployment Changes entries for the Discord.Net to webhook refactor. Refs #423.
Simplify (AI review loop Phase A). RateLimitResponse.Global was deserialised from Discord's 429 body but never read; the serialiser skips the unmapped "global" key, so behaviour is unchanged. Pattern Sweep: RateLimitResponse is the only response-deserialised type in the PR; the Webhook* records are outbound payloads. Nothing else to change. Refs #423.
Simplify (AI review loop Phase A). Startup already fails unless Discord:WebhookUrl is an absolute https URL, and FromWebhookUrl is now the only way to build DiscordBotConfiguration (private constructor), so IsConfigured, the PublishAsync not-configured guard, LogWebhookNotConfigured and the Yes/No startup line could never vary. Health reports Discord Ok. Pattern Sweep: construct = Discord configuration checks always true after FromWebhookUrl validation. Removed the redundant IsAbsoluteUri check in the same file; no other hits in BuildBot.Discord or ServerStartup.LoadDiscordConfig. EmbedUrl's scheme check validates embed URLs (different construct). Refs #423.
Health no longer varies with configuration; the entry now states only that startup fails on a missing or non-https webhook URL. Refs #423.
Simplify (AI review loop Phase A). Both arms of the GetFields ternary rebuilt the ARN/Status field list; it is now built once and the Version field appended when present. Output unchanged. Pattern Sweep: searched src/**/*.cs for other ternaries whose arms are both collection expressions; none found. Refs #423.
Owner decision Q11 on PR #428. The named IHttpClientFactory client's Polly policy now handles transient errors plus 429 Too Many Requests, waiting on the Retry-After header (delta or date, capped at 60s) and otherwise the existing exponential backoff. DiscordWebhookClient.SendAsync is a single POST; the hand-written retry loop, RateLimitResponse and the rate-limit log message are removed. Pattern Sweep: searched production src for hand-written HTTP retry loops (while(true), attempt counters, Task.Delay, Retry-After, TooManyRequests, AddHttpClient/CreateClient/new HttpClient, WaitAndRetry). The only other HTTP client (CloudFormation subscription confirmation) already uses a Polly policy; nothing else to change. Refs #423.
Owner decision Q12 on PR #428. The sender now awaits the message channel in ExecuteAsync and sends each message in order, logging (never the webhook URL) and continuing when a send fails, so one exception can no longer end delivery; cancellation of the stopping token ends the loop cleanly. The Rx pipeline, constructor-started work and manual cancellation/dispose are removed. Pattern Sweep: searched src for Rx subscribe chains and constructor-started or unawaited background work (Observable/Subscribe/ToObservable, IHostedService/BackgroundService, Task.Run, timers, ContinueWith, Task.Factory, ThreadPool); DiscordMessageSender was the only hit. Refs #423.
Simplify (AI review loop Phase A). The http(s) scheme check on embed URLs ran per producer through EmbedUrl.FromString, and CloudFormation's Uri skipped it. EmbedMapper now drops any URL that is not an absolute http or https URL when mapping to the wire model, so Discord never rejects an embed over its URL; EmbedUrl only parses a string safely. Pattern Sweep: searched src for UriScheme, .Scheme, IsAbsoluteUri, UriKind and every EmbedUrl caller. GitHub push/status and CloudFormation producers are now covered centrally with no edits; DiscordBotConfiguration's https check validates the webhook endpoint (different construct). Refs #423.
Simplify (AI review loop Phase A). "Discord Webhook configured" always printed because DiscordBotConfiguration.FromWebhookUrl throws otherwise, so it carried no information. The GitHub and SNS config blocks print nothing at startup either. Pattern Sweep: the PR diff adds no other always-identical startup console line; existing progress markers on main (App Created, Endpoints configured, Configuring ... Endpoint, Validating cultures) are a different construct. Refs #423.
Simplify (AI review loop Phase A). Every CreateBot call passed the static Config, so the parameter is removed. Pattern Sweep: CreateBot exists only in DiscordBotTests; no other test helper takes an always-identical argument. Refs #423.
Simplify (AI review loop Phase A). Send failures were caught twice: in DiscordBot.PublishAsync and again in the DiscordMessageSender loop, whose catch could never run. DiscordBot now lets failures propagate and the sender loop, its only caller, logs and continues; the unused LogSendFailed/LogSendCancelled messages are removed. A send cancelled at shutdown now simply ends the loop without a warning. Construct: duplicate catch-all around DiscordBot.PublishAsync. Pattern Sweep: searched src for catch (Exception; CloudFormationSubscriptionConfirmationNotificationHandler and AwsCloudFormation each catch once with no outer catch, and Program.cs is the top-level host handler. No other double guards. Refs #423.
Simplify (AI review loop Phase A). The public OnRetryAsync method only returned a completed task to fill Polly's onRetryAsync argument; it is now an inline static lambda. Behaviour unchanged. Pattern Sweep: no other no-op public callbacks in the PR. CloudformationSetup.OnRetryAsync on main is the same construct but outside this PR (and has its own test), so it is not changed here. Refs #423.
EmbedLimiter truncates the embed title to 256 characters, the description to 4096, field names to 256 and field values to 1024. Fields are capped at 25 and the total embed length is kept at or under 6000 characters by shrinking the description first, then field values from the last field, then dropping trailing fields. Truncation appends "..." and is surrogate-safe, so no surrogate pair is split. Discord therefore no longer rejects oversized notifications. Push notifications use only the first line of each commit message as the field name. Construct: building Discord embeds without length limits Swept: all embeds go through EmbedMapper.ToWebhookEmbed, so the single choke point covers every publisher (GitHub, CloudFormation, Watchtower); no other occurrences. Refs #423
Retry-After (seconds or date) is now used for every retried response (5xx, 408, 429), not only 429. A negative or past value means retry immediately. A Retry-After over 60s is no longer clamped: the retry is refused and the response returned, so a long rate-limit ban is not hammered. The retry predicate is built by hand (HttpRequestException, 5xx, 408, 429) because HandleTransientHttpError cannot refuse a retry. The named webhook client's HttpClient.Timeout is set to 300s (3 waits of up to 60s plus 4 attempts of 30s) so the retries can complete before the client times out. Tests use MockDateTimeSources in place of a hardcoded date. Construct: Polly retry delay/budget on an IHttpClientFactory named client Swept: none Searched: WaitAndRetry|Retry-After|RetryAfter|AddPolicyHandler|AddTransientHttpErrorPolicy|HandleTransientHttpError|.Timeout|AddHttpClient( in production src. Hits: src/BuildBot.Discord/DiscordSetup.cs (this fix). Referred: src/BuildBot.CloudFormation/CloudformationSetup.cs has the same construct but is outside this change and is referred to the owner (Q22); not fixed here. Refs #423
…cted On a non-success response the webhook client reads at most 1024 characters of the body and LogSendRejected includes it, so Discord's validation message (e.g. which embed field is too long) appears in the logs. The request is sent with HttpCompletionOption.ResponseHeadersRead so an error body is never fully buffered, and the body read is bounded by a 10s TimeProvider timeout because HttpClient.Timeout no longer applies once headers arrive. If the body read fails or times out the status is still reported with "(response body unavailable)"; caller cancellation still propagates. The webhook URL/token is never logged. Pattern Sweep record: Construct: non-success HTTP response body discarded before the failure is logged. Searched IsSuccessStatusCode|EnsureSuccessStatusCode|PostAsync|GetAsync|SendAsync(|ReadAsStringAsync|HttpClient in production src. Hits: DiscordWebhookClient.cs (this fix); src/BuildBot.CloudFormation/Publishers/CloudFormationSubscriptionConfirmationNotificationHandler.cs is the same construct, outside this PR's changes, referred to the owner (Q22), not fixed here. Refs #423
…essage Discord rejects the whole webhook message with 400 when a field name or value is empty (Discord.Net's EmbedFieldBuilder used to reject this before sending). EmbedMapper now replaces an empty or whitespace-only field name or value with "-" and omits a blank title or description, so one blank value (e.g. an empty CloudFormation status or branch name) no longer loses the notification. Construct: blank Discord embed text sent instead of being omitted or replaced. Swept: none Refs #423
Each webhook attempt now has its own 30s Polly timeout inside the retry policy, and a timed-out attempt (TimeoutRejectedException) is retried. A connection that stalls before sending headers no longer uses up the whole 300s HttpClient budget and drops the message while every queued message waits behind it. Caller cancellation still propagates and is not retried. The policy is built by CreateWebhookPolicy so tests can run the exact registered composition with a short attempt timeout and zero backoff, because Polly v7 timeouts and sleeps use its own clock, not TimeProvider. The backoff delegate is held in a static field, avoiding a compiler-cached method-group branch. Construct: Polly HTTP retry policy without a per-attempt timeout - src/BuildBot.Discord/DiscordSetup.cs: the webhook retry policy this fix changes (this fix) - src/BuildBot.Discord.Tests/DiscordSetupTests.cs: tests for the per-attempt timeout and retry composition (this fix) Sweep note: CloudformationSetup.cs SNS client is left as is by owner decision (Q22 on PR #428). Refs #423
The webhook retry policy's onRetry was a no-op, so rate-limit storms and slow delivery left no trace before the final failure. Each retry now logs a Warning with the attempt number, the delay and the reason: the HTTP status code, or the exception type for HttpRequestException/TimeoutRejectedException. Exception messages and the request URL are never logged, because they can contain the webhook token. Construct: Polly retry policy whose onRetry does nothing. Refs #423
IsTransientFailure treated every 5xx as transient, so permanent failures such as 501 Not Implemented and 505 were retried 3 times with backoff while the sequential sender waited. The webhook now retries only 408, 429, 500, 502, 503 and 504 (plus connection failures and per-attempt timeouts); the Retry-After over 60s refusal is unchanged. Construct: HTTP retry rule treating every 5xx as transient Swept: searched >= HttpStatusCode|InternalServerError|TooManyRequests|RequestTimeout|Transient|WaitAndRetry|Handle<HttpRequestException|OrResult in src - src/BuildBot.Discord/DiscordSetup.cs: this fix (IsTransientFailure status code rule) - src/BuildBot.Discord.Tests/DiscordSetupTests.cs: tests for the transient status code rule - CloudformationSetup.cs (AddTransientHttpErrorPolicy): left as is by owner decision (Q22 on PR #428) Refs #423
…dget WebhookTimeout equalled the worst-case retry budget exactly (4 x 30s attempts + 3 x 60s Retry-After = 300s), so scheduling and logging overhead could let HttpClient.Timeout cancel the final attempt and lose the message. A 30s slack is added (330s), and the test now asserts that the timeout is strictly greater than the worst-case budget instead of checking a literal value. Construct: overall HttpClient.Timeout set to exactly the retry budget. Searched `Timeout =` in src; only DiscordSetup.cs (this fix). CloudFormation is left as is by owner decision (Q22 on PR #428). Refs #423
86fea97 to
28145f8
Compare
The final-failure log in DiscordMessageSender passed the exception object to the logger. An HttpRequestException message can include the webhook URL, which embeds its token, so any sink that rendered the exception could leak it. Log only the exception type name, as the retry logger already does (EventIds 8/9). EventId 7 and the Error level are unchanged. Construct: LoggerMessage method taking an Exception whose message can carry the webhook URL; searched Discord path log calls in src, no further hits Refs #423
AI Review Loop: Phase B (code review) not convergingRound 3 mostly re-raised findings from earlier rounds. Both new findings are now fixed:
These findings are still open and go forward for human review. Each one has a reply on its own inline comment.
Moving on to Phase C (security review). |
Summary
DiscordWebhookClient): a named client with transient retries and explicit 429retry_afterhandling. Request logging is removed so the token is never logged.DiscordEmbed,DiscordEmbedField,DiscordColor) replacesEmbedBuilderin the GitHub, CloudFormation and Watchtower publishers.BotServicerenamedDiscordMessageSender, and its 1s Rx delay removed.Discord:WebhookUrl(DISCORD__WEBHOOKURLfrom Secrets Manager keyDISCORD_WEBHOOK). Startup fails if the URL is missing or not https. Health reflects configuration only.src/publishupdated.Deployment
The
DISCORD_WEBHOOKkey must exist in the BuildBot secret before deploying. The oldDISCORD_TOKEN,DISCORD_SERVER,DISCORD_CHANNELandDISCORD_RELEASE_CHANNELkeys are unused.Follow-up
When #426 merges, fold the temporary README
## Discord Configurationrow into #426's## Configurationtable.Closes #423