Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d061d1a
Add GitHub App issue integration
G3root Aug 13, 2026
04296f1
Harden GitHub integration flows and error handling
G3root Aug 13, 2026
1ec4ad1
chore(effect): migrate Schema.TaggedErrorClass to Schema.TaggedError
G3root Aug 14, 2026
4d08d70
chore(deps): upgrade Effect to 4.0.0-beta.107 and reconcile catalogs
G3root Aug 14, 2026
8fe9dee
feat(github): use @distilled.cloud/github Effect-native SDK
G3root Aug 14, 2026
9676bf7
fix(auth): remove unused getDefaultFieldName destructure
G3root Aug 14, 2026
5c99f2f
chore: fix coding standard
G3root Aug 14, 2026
3effb56
chore: fix types
G3root Aug 14, 2026
f174126
refactor(integrations): export provider capability keys as named cons…
G3root Aug 14, 2026
1e1b484
fix(github): brand OAuth state ids and align startInstallation signature
G3root Aug 14, 2026
e4d646d
docs(integrations): document GitHub provider and inbound/bindings
G3root Aug 14, 2026
218b5d2
fix: bugs
G3root Aug 14, 2026
86aae63
fix: error validation
G3root Aug 14, 2026
2da0c70
fix: tests
G3root Aug 14, 2026
82fa490
fix: bugs
G3root Aug 14, 2026
c7c79a7
Merge branch 'main' into codex/add-github-integration
G3root Aug 14, 2026
d0c14b5
chore: regenerate lock file
G3root Aug 14, 2026
0eb1ab7
fix: invalid
G3root Aug 14, 2026
8f475b4
chore: fix migrations
G3root Aug 14, 2026
3884d3a
fix: lookup
G3root Aug 14, 2026
370b629
fix: migration
G3root Aug 14, 2026
cf18d55
fix(integrations): load GitHub manifest from browser-safe contracts e…
G3root Aug 14, 2026
4e7ce9e
feat(db): define the two supported GitHub sync rule shapes
G3root Aug 14, 2026
64979d3
feat(domain): hard-wire GitHub sync rules to the two supported shapes
G3root Aug 14, 2026
b043429
feat(web): render two fixed GitHub sync rule slots in settings
G3root Aug 14, 2026
bd08ce2
docs: describe the hard-wired GitHub sync rule shapes
G3root Aug 14, 2026
329b757
feat(integrations): carry sanitized post description in integration e…
G3root Aug 14, 2026
f66ed87
feat(integrations-github): use post description as issue body and sha…
G3root Aug 14, 2026
3f761d9
feat(domain): drop GitHub endpoint config, pass post descriptions to …
G3root Aug 14, 2026
3bb7ab2
feat(domain): propagate post descriptions through integration feedbac…
G3root Aug 14, 2026
5483e3d
refactor(server): derive GitHub App endpoint URLs and handle installa…
G3root Aug 14, 2026
ff95022
feat(web): gate GitHub post actions by integration permission and alw…
G3root Aug 14, 2026
6c12c49
fix: ui
G3root Aug 14, 2026
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
37 changes: 37 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,43 @@ DISCORD_PUBLIC_KEY=
# OPTIONAL: OAuth redirect URL. Defaults to <API_URL>/discord/oauth/callback.
DISCORD_OAUTH_REDIRECT_URL=

# [[GITHUB APP INTEGRATION]]
# These settings power the two-way GitHub issue integration and are separate
# from GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET under SOCIAL AUTH, which are
# used only for signing users into Feeblo.
#
# Create a GitHub App under GitHub Settings -> Developer settings -> GitHub
# Apps. Give it repository permissions "Issues: Read and write" and
# "Metadata: Read", subscribe it to the Issues event, and enable
# "Request user authorization (OAuth) during installation". Leave every
# value below empty to disable the GitHub integration.
#
# REQUIRED: Numeric App ID shown on the GitHub App's General page. This is not
# the OAuth Client ID.
GITHUB_INTEGRATION_APP_ID=
# REQUIRED: URL-friendly app name from the public installation URL. For
# https://github.com/apps/feeblo, the slug is "feeblo".
GITHUB_INTEGRATION_APP_SLUG=
# REQUIRED: Client ID and client secret from the GitHub App's General page.
# They are used only during installation to verify that the current installer
# can access the selected GitHub App installation. The temporary user token is
# discarded and is never stored.
GITHUB_INTEGRATION_CLIENT_ID=
GITHUB_INTEGRATION_CLIENT_SECRET=
# REQUIRED: PEM private key generated from the GitHub App's Private keys
# section. Preserve the complete BEGIN/END lines and line breaks. In a secret
# manager, store the PEM as a multiline secret rather than committing the
# downloaded .pem file.
GITHUB_INTEGRATION_PRIVATE_KEY=
# REQUIRED: Random webhook secret configured identically on the GitHub App's
# General page. GitHub signs the global App webhook with this value.
# Generate one with: openssl rand -hex 32
# Also register the fixed Feeblo endpoints in the GitHub App: the callback URL
# <API_URL>/github/app/installations/callback and the webhook URL
# <API_URL>/github/app/webhooks. Feeblo mounts both paths itself; there are no
# override variables for them.
GITHUB_INTEGRATION_WEBHOOK_SECRET=

# [[BILLING]]
# OPTIONAL: Enables Polar checkout, customer portal, and signed webhooks when
# both credentials are configured. Use sandbox while testing.
Expand Down
8 changes: 6 additions & 2 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"build": "dotenvx run -f ../../.env -- rolldown -c",
"dev": "dotenvx run -f ../../.env -- tsx watch src/index.ts",
"start": "dotenvx run -f ../../.env -- tsx src/index.ts",
"check-types": "tsc --noEmit"
"check-types": "tsc --noEmit",
"test": "vitest run"
},
"dependencies": {
"@effect/platform-node": "catalog:",
Expand All @@ -18,6 +19,7 @@
"@feeblo/id": "workspace:*",
"@feeblo/integration-core": "workspace:*",
"@feeblo/integration-discord": "workspace:*",
"@feeblo/integration-github": "workspace:*",
"@feeblo/integration-webhook": "workspace:*",
"@feeblo/transactional": "workspace:*",
"@feeblo/utils": "workspace:*",
Expand All @@ -27,9 +29,11 @@
"@feeblo/integration-slack": "workspace:*"
},
"devDependencies": {
"@effect/vitest": "catalog:",
"@feeblo/config": "workspace:*",
"rolldown": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:"
"typescript": "catalog:",
"vitest": "catalog:"
}
}
1 change: 1 addition & 0 deletions apps/server/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ServerConfig } from "./config";
const requiredServerEnvironment = {
APP_ROOT_DOMAIN: "example.test",
APP_URL: "https://example.test",
AUTH_ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef",
API_URL: "https://api.example.test",
};

Expand Down
46 changes: 46 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Redacted from "effect/Redacted";
import * as Schema from "effect/Schema";

export class ServerConfig extends Context.Service<ServerConfig>()(
Expand All @@ -16,6 +17,44 @@ export class ServerConfig extends Context.Service<ServerConfig>()(
const nodeEnv = yield* Config.string("NODE_ENV").pipe(
Config.withDefault("development")
);
const githubAppId = yield* Config.string(
"GITHUB_INTEGRATION_APP_ID"
).pipe(Config.option, Effect.map(Option.getOrUndefined));
const githubAppSlug = yield* Config.string(
"GITHUB_INTEGRATION_APP_SLUG"
).pipe(Config.option, Effect.map(Option.getOrUndefined));
const githubClientId = yield* Config.string(
"GITHUB_INTEGRATION_CLIENT_ID"
).pipe(Config.option, Effect.map(Option.getOrUndefined));
const githubClientSecret = yield* Config.redacted(
"GITHUB_INTEGRATION_CLIENT_SECRET"
).pipe(
Config.option,
Effect.map((value) => Option.getOrElse(value, () => Redacted.make("")))
);
const githubWebhookSecret = yield* Config.redacted(
"GITHUB_INTEGRATION_WEBHOOK_SECRET"
).pipe(
Config.option,
Effect.map((value) => Option.getOrElse(value, () => Redacted.make("")))
);
const githubPrivateKey = yield* Config.redacted(
"GITHUB_INTEGRATION_PRIVATE_KEY"
).pipe(
Config.option,
Effect.map((value) => Option.getOrElse(value, () => Redacted.make("")))
);
const githubEncryptionKey = yield* Config.redacted(
"INTEGRATION_ENCRYPTION_KEY"
).pipe(
Config.option,
Effect.flatMap(
Option.match({
onNone: () => Config.redacted("AUTH_ENCRYPTION_KEY"),
onSome: Effect.succeed,
})
)
);
// Outbound-webhook security configuration (encryption key and egress
// policy) is owned by WebhookIntegrationConfig in the domain package.
const integrationConnectionConcurrency = yield* Config.schema(
Expand Down Expand Up @@ -68,6 +107,13 @@ export class ServerConfig extends Context.Service<ServerConfig>()(
appUrl,
appRootDomain,
clientIpProxyTrust,
githubAppId,
githubAppSlug,
githubClientId,
githubClientSecret,
githubEncryptionKey,
githubPrivateKey,
githubWebhookSecret,
integrationConnectionConcurrency,
integrationGlobalConcurrency,
nodeEnv,
Expand Down
7 changes: 5 additions & 2 deletions apps/server/src/discord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
import type { IntegrationProviderRegistry } from "@feeblo/integration-core";
import { DiscordOAuthState } from "@feeblo/integration-discord";
import type { ParsedDiscordInboundRequest } from "@feeblo/integration-discord/inbound-schema";
import { discordProviderKey } from "@feeblo/integration-discord/manifest";
import {
discordInteractionsCapabilityKey,
discordProviderKey,
} from "@feeblo/integration-discord/manifest";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Layer from "effect/Layer";
Expand Down Expand Up @@ -55,7 +58,7 @@ const handleInteraction = (
) =>
Effect.gen(function* () {
const inboundHandler = registry.getInboundHandler({
capabilityKey: "interactions",
capabilityKey: discordInteractionsCapabilityKey,
provider: discordProviderKey,
});
if (inboundHandler === undefined) {
Expand Down
Loading
Loading