Skip to content

Commit e7b8851

Browse files
feat(docs): add CompatibilityTestBot and document webhook deduplication differences (#10)
Adds CompatibilityTestBot example that disables idempotency middleware to match Probot (Node.js) behavior, which does not automatically deduplicate webhooks by delivery ID. This behavioral difference was previously undocumented in the migration guide and comparison materials. Documentation updates: - Add comprehensive Section 7 to Probot-to-ProbotSharp-Guide.md explaining the deduplication difference with code examples for both frameworks - Update README.md comparison table to highlight webhook deduplication difference - Add Probot comparison notes to Architecture.md, AdapterConfiguration.md, ConfigurationBestPractices.md, and Operations.md - Document when to enable/disable idempotency middleware CompatibilityTestBot features: - HTTP-based integration testing against real server process - TestEventTracker to verify webhook delivery processing - Tracks all deliveries including duplicates (Probot-compatible behavior) - Demonstrates proper ASP.NET Core Minimal API patterns - Comprehensive README with troubleshooting and testing guidance This ensures developers migrating from Probot understand the idempotency difference and can choose appropriate behavior for their use case.
1 parent f4320bd commit e7b8851

File tree

12 files changed

+1094
-0
lines changed

12 files changed

+1094
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ While Probot-Sharp maintains API familiarity with the original Node.js Probot fr
398398
| **Resilience** | Manual retry logic | Built-in circuit breakers, retry policies with exponential backoff, and timeout handling |
399399
| **Testing** | Jest/Mocha | xUnit with strong mocking (NSubstitute), property-based testing support |
400400
| **Dependency Injection** | Manual wiring | First-class DI container with lifetime scoping |
401+
| **Webhook Deduplication** | Manual (app responsibility) | Automatic `UseIdempotency()` middleware with dual-layer strategy (database + distributed lock) |
401402

402403
**Key Architectural Benefits:**
403404

docs/AdapterConfiguration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ Following **Hexagonal Architecture** and **Cloud Design Patterns**, adapter conf
6767

6868
### Idempotency Adapters
6969

70+
> **Probot Comparison:** Unlike Probot (Node.js), which requires manual deduplication tracking, ProbotSharp provides automatic webhook deduplication via the idempotency adapter. This can be disabled by removing `app.UseIdempotency()` from your Program.cs if you need Probot-compatible behavior.
71+
7072
| Provider | Use Case | Dependencies | Data Loss on Restart |
7173
|----------|----------|--------------|---------------------|
7274
| **InMemory** | Development, testing, single instance | None | Yes |

docs/Architecture.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1294,6 +1294,8 @@ Shared -> Domain -> Application -> Adapters (Inbound) -> Bootstraps
12941294

12951295
## Idempotency Strategy
12961296

1297+
**Architectural Difference from Probot (Node.js):** Probot does NOT automatically deduplicate webhooks - the application is responsible for tracking processed delivery IDs. ProbotSharp takes a more opinionated approach with built-in deduplication.
1298+
12971299
Probot-Sharp implements a dual-layer idempotency strategy to prevent duplicate webhook processing:
12981300

12991301
### Layer 1: Database-Level (IWebhookStoragePort)
@@ -1378,6 +1380,8 @@ DLQ items should trigger alerts for operational teams to investigate and resolve
13781380

13791381
**Context**: GitHub may deliver the same webhook multiple times. The application must prevent duplicate processing while supporting horizontal scaling across multiple instances.
13801382

1383+
**Note:** This differs from Probot (Node.js), which does not provide automatic deduplication and requires applications to implement their own tracking of processed delivery IDs (typically using Redis or a database).
1384+
13811385
**Decision**: Implement a dual-layer idempotency strategy:
13821386
1. Database-level: DeliveryId as primary key in IWebhookStoragePort
13831387
2. Distributed lock: IIdempotencyPort using Redis or in-memory cache

docs/ConfigurationBestPractices.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,28 @@ builder.Configuration.AddSecretsManager(
235235
});
236236
```
237237

238+
## Middleware Configuration
239+
240+
### Idempotency Middleware
241+
242+
**Enabled by default in all production examples.** This prevents duplicate webhook processing.
243+
244+
```text
245+
// Production (recommended):
246+
app.UseIdempotency();
247+
248+
// Development/testing only (Probot-compatible):
249+
// Comment out for Probot Node.js behavioral compatibility
250+
```
251+
252+
**Trade-offs:**
253+
-**Enabled:** Prevents accidental duplicate actions in production
254+
-**Disabled:** Matches Probot (Node.js) behavior for integration testing
255+
256+
**See Also:**
257+
- [Architecture docs - Idempotency Strategy](Architecture.md#idempotency-strategy)
258+
- [Probot-to-ProbotSharp Guide - Section 7](Probot-to-ProbotSharp-Guide.md#7-webhook-deduplication-architectural-difference)
259+
238260
## Complete Example
239261

240262
Here's a production-ready `appsettings.json` following all best practices:

docs/Operations.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,14 @@ curl http://localhost:8080/health
925925
- Average processing time
926926
- Retry attempts distribution
927927

928+
**Idempotency Metrics:**
929+
- Idempotency hits (duplicate deliveries blocked)
930+
- Idempotency misses (unique deliveries processed)
931+
- Idempotency errors (lock acquisition failures)
932+
- Duplicate delivery rate (hits / total deliveries)
933+
934+
> **Note:** High duplicate rates may indicate GitHub infrastructure issues, webhook delivery retries (normal for failures), or load balancer misconfiguration. Duplicates are expected when GitHub retries failed webhook deliveries (status codes 500, 503), during network timeouts, or after application restarts during request handling.
935+
928936
### Alert Thresholds
929937

930938
| Metric | Warning | Critical |

docs/Probot-to-ProbotSharp-Guide.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,123 @@ services.AddTransient<IGitHubOAuthPort, GitHubOAuthClient>();
293293

294294
---
295295

296+
### 7) Webhook Deduplication (Architectural Difference)
297+
298+
**This is a critical behavioral difference between Probot and ProbotSharp.**
299+
300+
#### Probot (Node.js) Behavior
301+
302+
```javascript
303+
// Probot does NOT deduplicate webhooks automatically
304+
export default (app) => {
305+
app.on("push", async (context) => {
306+
// This will be called for EVERY webhook delivery
307+
// Even if GitHub sends the same delivery ID multiple times
308+
app.log.info("Processing push", context.id);
309+
});
310+
};
311+
```
312+
313+
- Does NOT automatically deduplicate webhooks by delivery ID
314+
- All webhook deliveries are processed, including duplicates
315+
- Application is responsible for implementing deduplication if needed
316+
- Common pattern: Store processed delivery IDs in Redis/database
317+
318+
#### ProbotSharp (.NET) Behavior
319+
320+
```text
321+
// ProbotSharp deduplicates by default (production safety)
322+
var app = builder.Build();
323+
app.UseProbotSharpMiddleware();
324+
app.UseIdempotency(); // Prevents duplicate processing
325+
```
326+
327+
- Automatic deduplication via `UseIdempotency()` middleware (enabled by default in all examples)
328+
- Prevents duplicate processing for horizontal scaling and reliability
329+
- Uses dual-layer strategy: database-level + distributed lock (Redis/in-memory)
330+
- Configured via `Adapters.Idempotency` in appsettings.json
331+
332+
#### How to Disable (Probot-Compatible Behavior)
333+
334+
```text
335+
var app = builder.Build();
336+
app.UseProbotSharpMiddleware();
337+
// Comment out or remove this line for Probot-compatible behavior:
338+
// app.UseIdempotency();
339+
```
340+
341+
#### When to Disable Idempotency
342+
343+
**Disable for:**
344+
- ✅ Integration testing against Probot behavior
345+
- ✅ Apps that implement custom deduplication logic
346+
- ✅ Single-instance deployments where duplicates are acceptable
347+
- ✅ Testing scenarios that verify duplicate handling
348+
349+
**Enable for (RECOMMENDED for production):**
350+
- ✅ Production deployments
351+
- ✅ Horizontal scaling (multiple instances)
352+
- ✅ High-reliability requirements
353+
- ✅ Preventing accidental duplicate actions
354+
355+
#### Configuration
356+
357+
**In-Memory (Development/Testing):**
358+
```json
359+
{
360+
"ProbotSharp": {
361+
"Adapters": {
362+
"Idempotency": {
363+
"Provider": "InMemory",
364+
"Options": {
365+
"ExpirationHours": "24"
366+
}
367+
}
368+
}
369+
}
370+
}
371+
```
372+
373+
**Redis (Production):**
374+
```json
375+
{
376+
"ProbotSharp": {
377+
"Adapters": {
378+
"Idempotency": {
379+
"Provider": "Redis",
380+
"Options": {
381+
"ConnectionString": "localhost:6379",
382+
"ExpirationHours": "24"
383+
}
384+
}
385+
}
386+
}
387+
}
388+
```
389+
390+
**Database (Enterprise):**
391+
```json
392+
{
393+
"ProbotSharp": {
394+
"Adapters": {
395+
"Idempotency": {
396+
"Provider": "Database",
397+
"Options": {
398+
"ExpirationHours": "24"
399+
}
400+
}
401+
}
402+
}
403+
}
404+
```
405+
406+
**See Also:**
407+
- [Architecture docs - Idempotency Strategy](Architecture.md#idempotency-strategy)
408+
- [Architecture docs - ADR-002: Dual-Layer Idempotency](Architecture.md#adr-002-dual-layer-idempotency-strategy)
409+
- [Adapter Configuration Guide](AdapterConfiguration.md)
410+
411+
---
412+
296413
Refer to `docs/Architecture.md`, `docs/Extensions.md`, and `docs/LocalDevelopment.md` for deeper dives and runnable examples.
297414

298415

0 commit comments

Comments
 (0)