Files
twenty/packages/twenty-apps/community/github-connector/src/modules/shared/retry.ts
T
Charles Bochet 3ebeb3a3e8 feat(community): add github-connector example app (#19961)
## Summary

Adds a new community app at
`packages/twenty-apps/community/github-connector` that demonstrates a
complete, production-style GitHub integration built on the Twenty SDK.

It is extracted (and decoupled) from the internal `twenty-eng` workspace
so external developers can use it as a reference for their own
connectors.

What it ships:

- **Six synced objects**: `pullRequest`, `pullRequestReview`,
`pullRequestReviewEvent`, `issue`, `projectItem`, `engineer`
- **Logic functions** for periodic backfills (PRs, reviews, issues,
project items, contributors) and a single signed-webhook route trigger
(`POST /github/webhook`) that performs idempotent upserts for
`pull_request`, `pull_request_review`, `issues`, and `projects_v2_item`
events
- **Views, navigation menu items and a GitHub folder** so the data is
discoverable in the UI out of the box
- **Configurable repos / project numbers** via `GITHUB_REPOS` and
`GITHUB_PROJECT_NUMBERS` application variables — no hardcoded org

## Authentication

Two interchangeable modes (PAT preferred for quick setup, GitHub App
recommended for production):

1. **Personal Access Token** — set `GITHUB_TOKEN`. Used as-is for both
REST and GraphQL.
2. **GitHub App** — set `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`,
`GITHUB_APP_INSTALLATION_ID`. Issues a signed JWT, exchanges it for a
short-lived installation token, and caches the token until expiry.

Webhook signature verification (`X-Hub-Signature-256`) is enforced when
`GITHUB_WEBHOOK_SECRET` is set.

## Notes

- Built on `twenty-sdk@2.0.0` / `twenty-client-sdk@2.0.0`
- Decoupled from internal modules (`quality/bug`, `discord`, `release`,
`code-build`, `project-management`) — `mustBeQa` is inlined and a local
`github` nav folder replaces shared ones
- `npx twenty typecheck`, `yarn lint`, and `npx twenty build` all run
cleanly
- Includes a comprehensive README with setup, env vars, webhook
configuration, and the auth resolution flow
2026-04-22 18:17:08 +02:00

50 lines
1.4 KiB
TypeScript

export type RetryOptions = {
retries?: number;
baseDelayMs?: number;
maxDelayMs?: number;
shouldRetry?: (err: unknown) => boolean;
};
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
export async function retry<T>(
label: string,
fn: () => Promise<T>,
options: RetryOptions = {},
): Promise<T> {
const requestedRetries = options.retries ?? 3;
const retries = Math.max(
Math.floor(Number.isFinite(requestedRetries) ? requestedRetries : 1),
1,
);
const baseDelayMs = options.baseDelayMs ?? 500;
const maxDelayMs = options.maxDelayMs ?? 5_000;
const shouldRetry = options.shouldRetry ?? (() => true);
let lastErr: unknown;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (err) {
lastErr = err;
const msg = err instanceof Error ? err.message : String(err);
if (attempt === retries || !shouldRetry(err)) {
console.log(
`[retry] ${label} attempt ${attempt}/${retries} failed (giving up): ${msg}`,
);
throw err;
}
const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
const jittered = delay + Math.floor(Math.random() * baseDelayMs);
console.log(
`[retry] ${label} attempt ${attempt}/${retries} failed, retrying in ${jittered}ms: ${msg}`,
);
await sleep(jittered);
}
}
throw lastErr;
}