GitHub Sync for Landing Pages
Status: [Live Apr 2026]
Implemented and verified working. GitHub tab appears on the landing page detail page alongside HTML Preview, Copy Review, Full Copy, Versions. Connect modal supports both linking an existing repo and creating a new one. Auto-sync fires on client approval when configured.
Env requirement:
PROVIDER_CONFIG_ENCRYPTION_KEYmust be present inapps/dashboard/.env.local(copy fromapps/api/.env) for token decryption to work in server actions.
Allows a landing page to be linked to a GitHub repository so that the rendered HTML is pushed to the repo whenever the page is approved or manually triggered. This is the push direction (platform → GitHub), complementary to the existing pull direction (GitHub source files → platform RAG) documented in roadmap.md.
Concept
A LandingPageGitHubSync record links one landing page to one GitHub ConnectedChannel. When a sync occurs, the landing page’s rendered htmlContent is written to a configurable file path in the repo via GitHubService.putFile().
Two connection modes are supported:
- Link existing repo — pick from repos accessible to the connected GitHub account
- Create new repo — create a new GitHub repo via
POST /user/reposthen link immediately
Data Model
LandingPageGitHubSync
model LandingPageGitHubSync {
id String @id @default(cuid())
landingPageId String @unique
tenantId String
connectedChannelId String // ConnectedChannel of type "GitHub"
repoOwner String
repoName String
branch String @default("main")
filePath String @default("index.html")
autoSync Boolean @default(true) // push on client_approved
lastSyncedAt DateTime?
lastCommitSha String?
lastSyncStatus String? // "success" | "failed"
lastSyncError String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
landingPage LandingPage @relation(fields: [landingPageId], references: [id], onDelete: Cascade)
tenant Tenant @relation(fields: [tenantId], references: [id])
connectedChannel ConnectedChannel @relation(fields: [connectedChannelId], references: [id])
@@index([tenantId])
@@map("landing_page_github_sync")
}Back-relations required:
LandingPage.githubSync LandingPageGitHubSync?ConnectedChannel.landingPageGitHubSyncs LandingPageGitHubSync[]Tenant.landingPageGitHubSyncs LandingPageGitHubSync[]
Provider Changes
packages/providers/github/src/github.ts — add createRepo():
async createRepo(name: string, isPrivate: boolean): Promise<GitHubRepo>Calls POST /user/repos with { name, private: isPrivate, auto_init: true }. The auto_init: true creates an initial commit so putFile() can immediately write to the default branch without an empty-repo error.
UI — GitHub Tab on Landing Page Detail
Location: apps/dashboard/src/app/(dashboard)/landing-pages/[id]/GitHubSyncTab.tsx
Added as a new “GitHub” tab alongside HTML Preview, Copy Review, Full Copy, Versions.
No sync config (empty state)
- Message: “Connect this landing page to a GitHub repo to push the HTML on every approval.”
- Button: “Connect GitHub Repo” → opens the connection modal
If the tenant has no connected GitHub channels at all, a banner is shown instead: “Connect a GitHub account first on the Channels page.”
Has sync config (connected state)
Shows a card with:
- Repo:
owner/repoonbranch - File:
filePath - Auto-sync badge: “Auto-sync on approval” or “Manual sync only”
- Last sync: timestamp + status badge (Success / Failed / Never)
- Error message (if last sync failed)
- “Sync to GitHub” button — pushes current
htmlContentimmediately - “Unlink” button (secondary) — deletes the sync config
Connection modal
Two-mode toggle: Link existing repo | Create new repo
Common fields:
- GitHub Channel — dropdown of connected GitHub channels (
ConnectedChannel.type = "GitHub") - Branch — text input, default
"main" - File path — text input, default
"index.html" - Auto-sync on approval — toggle (default on)
Link existing repo:
- Repo — dropdown populated by
listGitHubRepos(channelId)server action once channel is selected
Create new repo:
- Repo name — text input
- Visibility — toggle: Public / Private
Server Actions
All actions live in apps/dashboard/src/app/(dashboard)/landing-pages/actions.ts.
getLandingPageGitHubConfig(pageId)
Returns the LandingPageGitHubSync record (or null) plus the list of connected GitHub channels for the tenant. Called on GitHubSyncTab mount.
listGitHubRepos(channelId)
Decrypts the token from ConnectedChannel.tokenInfo, calls GitHubService.listRepos(), returns repo list. Called when user selects a channel in the modal.
linkLandingPageGitHub(pageId, data)
Creates a LandingPageGitHubSync record. Validates that:
- The landing page belongs to the tenant
- The connected channel belongs to the tenant and is of type “GitHub”
createAndLinkGitHubRepo(pageId, data)
- Decrypt token, instantiate
GitHubService - Call
createRepo(name, isPrivate) - Create
LandingPageGitHubSyncrecord using the new repo’s owner + name
unlinkLandingPageGitHub(pageId)
Deletes the LandingPageGitHubSync record.
syncLandingPageToGitHub(pageId)
- Load sync config + landing page
htmlContent - Error if
htmlContentis null (page not yet approved — no HTML generated) - Decrypt token, instantiate
GitHubService - Call
putFile(owner, repo, filePath, branch, htmlContent, message)- Commit message:
"chore: update landing page via Leadmetrics [${new Date().toISOString()}]"
- Commit message:
- Update
lastSyncedAt,lastSyncStatus = "success",lastCommitSha - On error: update
lastSyncStatus = "failed",lastSyncError
Auto-Sync on Approval
In clientApproveLandingPage() (actions.ts), after htmlContent is generated and the landing page status is updated to client_approved:
const ghSync = await db.landingPageGitHubSync.findUnique({ where: { landingPageId: pageId } });
if (ghSync?.autoSync && htmlContent) {
pushHtmlToGitHub(ghSync, htmlContent).catch(() => {});
}pushHtmlToGitHub() is a non-exported async helper that contains the actual putFile() call. The .catch(() => {}) means auto-sync failures never block or fail the approval action.
Files Changed
| File | Change |
|---|---|
packages/db/prisma/schema.prisma | Add LandingPageGitHubSync model + back-relations |
packages/providers/github/src/github.ts | Add createRepo() method |
apps/dashboard/package.json | Add @leadmetrics/provider-github dependency |
apps/dashboard/src/app/(dashboard)/landing-pages/actions.ts | Add 5 server actions + pushHtmlToGitHub helper |
apps/dashboard/src/app/(dashboard)/landing-pages/[id]/GitHubSyncTab.tsx | New — full GitHub tab UI |
apps/dashboard/src/app/(dashboard)/landing-pages/[id]/LandingPageDetail.tsx | Add “GitHub” tab |