-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Feature/GitHub export #2810
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/GitHub export #2810
Conversation
@Rish-it is attempting to deploy a commit to the Onlook Team on Vercel. A member of the Team first needs to authorize it. |
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a GitHub export UI (button + modal + editor state), server-side GitHub auth helpers and expanded tRPC github router (create repo, upload files, user info), adjusts GitHub OAuth scopes, updates import context and env formatting, and removes many Supabase migration and migration-snapshot files. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant C as Web Client (Top Bar + GithubExportModal)
participant T as tRPC githubRouter
participant G as GitHub API (Octokit)
Note over C: Open modal → check connection & user info
U->>C: Click "Export to GitHub"
C->>T: getUserInfo()
alt Auth OK
T->>G: GET /user (via Octokit)
T-->>C: { login, id, email, permissions }
else Auth fail
T-->>C: UNAUTHORIZED
C-->>U: Prompt sign-in with GitHub
end
Note over C: Step 1 - Create repo
U->>C: Provide name/owner
C->>T: createRepository({ name, owner?, isOrg? })
T->>G: POST /user/repos or POST /orgs/{owner}/repos
alt Success
T-->>C: { repository }
else Conflict/Forbidden
T-->>C: Error (422/403/401)
C-->>U: Show error
end
Note over C: Step 2 - Select branch
U->>C: Choose main or custom
Note over C: Step 3 - Upload files
C->>T: uploadProjectFiles({ owner, repo, files[], commitMessage, branch? })
T->>G: GET ref -> GET commit -> POST tree -> POST commit -> PATCH ref
alt Success
T-->>C: { commit: { sha, htmlUrl } }
C-->>U: Success toast + link
else Failure
T-->>C: INTERNAL_SERVER_ERROR
C-->>U: Show error, allow retry
end
sequenceDiagram
autonumber
actor U as User
participant C as Web Client (Login)
participant S as Supabase Auth
participant G as GitHub OAuth
U->>C: Sign in with GitHub
C->>S: signInWithOAuth({ provider: github, scopes: "user repo" })
S->>G: OAuth flow (request scopes)
G-->>S: Access token
S-->>C: Session including GitHub identity/token
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Suggested reviewers
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (35)
💤 Files with no reviewable changes (35)
✨ Finishing Touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
per_page: 100, | ||
page: 1, | ||
try { | ||
if (input?.owner) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an owner is provided in getRepositories, the code always calls listForOrg. Ensure that this logic correctly distinguishes between an organization and a personal account, or handle the user case separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/web/client/src/components/store/editor/state/index.ts (1)
30-33
: Fix self-rescheduling debounce causing endless timers and CPU churnThe debounced callback sets
this.canvasScrolling = false
, which calls the setter again and re-schedules itself indefinitely. Guard the setter and write to the backing field inside the debounced fn.- set canvasScrolling(value: boolean) { - this._canvasScrolling = value; - this.resetCanvasScrolling(); - } + set canvasScrolling(value: boolean) { + this._canvasScrolling = value; + if (value) { + this.resetCanvasScrolling(); + } + } - private resetCanvasScrollingDebounced = debounce(() => { - this.canvasScrolling = false; - }, 150); + private resetCanvasScrollingDebounced = debounce(() => { + // Write directly to the observable backing field to avoid re-scheduling. + this._canvasScrolling = false; + }, 150);Also applies to: 47-49
apps/web/client/src/app/login/actions.tsx (1)
13-15
: Replace dynamicOrigin
header with fixed site URL for redirect
Using the request’sOrigin
header is spoofable and could allow open-redirects if your Supabase allowlist is misconfigured. Inapps/web/client/src/app/login/actions.tsx
, update:- const origin = (await headers()).get('origin') ?? env.NEXT_PUBLIC_SITE_URL; - const redirectTo = `${origin}${Routes.AUTH_CALLBACK}`; + const redirectTo = `${env.NEXT_PUBLIC_SITE_URL}${Routes.AUTH_CALLBACK}`;apps/web/client/src/app/projects/import/github/_context/context.tsx (1)
176-179
: Remove FAKE_ORGANIZATIONS override before mergingThis hardcodes orgs and discards the real API response. It will mislead users in prod.
-// TODO: remove this -setOrganizations(FAKE_ORGANIZATIONS as GitHubOrganization[]); +setOrganizations(organizationsData as GitHubOrganization[]);
🧹 Nitpick comments (10)
apps/web/client/src/env.ts (1)
127-131
: Ensure GitHub env vars remain server-only
Client’s env.ts contains only the n8n mappings; noGITHUB_
keys are exposed here. Add any new GitHub secrets exclusively in the server’s runtimeEnv (e.g. apps/web/server/src/env.ts) and do not include them in the client schema.• Optional: group all server-only env mappings under a distinct interface or section for clearer separation.
apps/web/client/src/components/store/editor/state/index.ts (2)
26-28
: Auto-bind actions to prevent lostthis
when passing methods as callbacks
makeAutoObservable
without autoBind risksthis
being undefined if methods likesetGithubExportOpen
are passed directly to components.- makeAutoObservable(this); + makeAutoObservable(this, {}, { autoBind: true });
51-56
: Ensure delayed cleanup when invokingclear()
from providersPer project guidance,
clear()
should be invoked viasetTimeout(() => store.clear(), 0)
during navigation/unmount to avoid race conditions. Please confirm the provider follows this.apps/web/client/src/server/api/lib/github-auth.ts (2)
86-94
: Avoid exposing raw access tokens beyond this moduleReturning
accessToken
viauserInfo
increases accidental leakage risk (e.g., logging, tRPC responses). Prefer returning onlyoctokit
and non-sensitive user info; keep tokens internal.- const userInfo = await getGitHubUserFromSession(supabase); - const octokit = createGitHubOctokit(userInfo.accessToken); + const userInfo = await getGitHubUserFromSession(supabase); + const octokit = createGitHubOctokit(userInfo.accessToken); return { octokit, - userInfo + userInfo: { + githubId: userInfo.githubId, + githubUsername: userInfo.githubUsername, + email: userInfo.email + } };Follow-up: ensure routers never serialize the token to clients.
47-50
: Type the Supabase client instead ofany
Using the typed Supabase client improves API correctness and autocompletion.
Example (adjust import path as needed):
import { SupabaseClient } from '@supabase/supabase-js'; export const getGitHubTokenFromSession = async (supabase: SupabaseClient): Promise<string> => { ... }Also applies to: 55-61, 86-94
apps/web/client/src/app/projects/import/github/_context/context.tsx (1)
195-196
: Align param name with API: rename username → owner to avoid confusionYou’re already mapping to
{ owner }
. Rename the parameter and its call sites for clarity.-const fetchRepositories = async (username?: string) => { +const fetchRepositories = async (owner?: string) => { setIsLoadingRepositories(true); setRepositoriesError(null); try { - const repositoriesData = await clientApi.github.getRepositories.query( - username ? { owner: username } : undefined, - ); + const repositoriesData = await clientApi.github.getRepositories.query( + owner ? { owner } : undefined, + ); setRepositories(repositoriesData as GitHubRepository[]); } finally { setIsLoadingRepositories(false); } } -const fetchOrgRepositories = (orgName: string) => { - fetchRepositories(orgName); -}; +const fetchOrgRepositories = (orgName: string) => { + fetchRepositories(orgName); +};apps/web/client/src/app/project/[id]/_components/top-bar/github-export/index.tsx (1)
24-30
: Add an accessible label to the icon-only buttonScreen readers need an aria-label.
-<Button +<Button + aria-label="Export project to GitHub" variant="ghost" size="icon" className="size-7 rounded-full cursor-pointer hover:opacity-80 bg-gray-100 dark:bg-gray-800" >apps/web/client/src/app/project/[id]/_components/top-bar/github-export/modal.tsx (1)
134-142
: Consider a safer base64 conversion for large filesThe chunked String.fromCharCode approach can be slow for big binaries. Optional: use Buffer on the server (preferred) or a streaming/base64 lib. Since uploads go through the server, moving conversion server-side avoids heavy client work.
apps/web/client/src/server/api/routers/github.ts (2)
69-101
: Minor: drop unused userInfo and simplify control flow
userInfo
isn’t used; also, error wrapping is fine as-is, just remove the unused destructure.- const { octokit, userInfo } = await getGitHubServiceForUser(ctx.supabase); + const { octokit } = await getGitHubServiceForUser(ctx.supabase);
52-67
: Minor: remove unused input from getOrganizations
username
is unused; simplify the input to none.- getOrganizations: protectedProcedure - .input( - z.object({ - username: z.string().optional() - }).optional() - ) + getOrganizations: protectedProcedure .query(async ({ input, ctx }) => { try { const { octokit } = await getGitHubServiceForUser(ctx.supabase); const { data } = await octokit.rest.orgs.listForAuthenticatedUser(); return data; } catch (error: any) {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
apps/web/client/src/app/login/actions.tsx
(1 hunks)apps/web/client/src/app/project/[id]/_components/top-bar/github-export/index.tsx
(1 hunks)apps/web/client/src/app/project/[id]/_components/top-bar/github-export/modal.tsx
(1 hunks)apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx
(2 hunks)apps/web/client/src/app/projects/import/github/_context/context.tsx
(1 hunks)apps/web/client/src/components/store/editor/state/index.ts
(3 hunks)apps/web/client/src/env.ts
(2 hunks)apps/web/client/src/server/api/lib/github-auth.ts
(1 hunks)apps/web/client/src/server/api/routers/github.ts
(6 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.tsx
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.tsx
: In React providers managing MobX stores, create the observable store with useState(() => new Store()) to ensure a stable instance
Keep a ref (e.g., storeRef.current) to the MobX store to avoid stale closures in effects
Use setTimeout(() => store.clear(), 0) for delayed cleanup of MobX stores to avoid race conditions
Separate project changes from branch updates by using proper effect dependency arrays
Do not use useMemo to hold MobX observable instances; React may drop memoized values causing data loss
Do not clean up MobX stores synchronously during navigation; perform delayed cleanup instead
Do not include the MobX store instance in effect dependency arrays when it causes infinite loops
Files:
apps/web/client/src/app/project/[id]/_components/top-bar/github-export/index.tsx
apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx
apps/web/client/src/app/login/actions.tsx
apps/web/client/src/app/projects/import/github/_context/context.tsx
apps/web/client/src/app/project/[id]/_components/top-bar/github-export/modal.tsx
🧬 Code graph analysis (4)
apps/web/client/src/app/project/[id]/_components/top-bar/github-export/index.tsx (2)
apps/web/client/src/components/store/editor/index.tsx (1)
useEditorEngine
(9-13)apps/web/client/src/app/project/[id]/_components/top-bar/github-export/modal.tsx (1)
GithubExportModal
(34-484)
apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx (1)
apps/web/client/src/app/project/[id]/_components/top-bar/github-export/index.tsx (1)
GithubExportButton
(11-42)
apps/web/client/src/app/project/[id]/_components/top-bar/github-export/modal.tsx (5)
apps/web/client/src/components/store/editor/index.tsx (1)
useEditorEngine
(9-13)apps/web/client/src/trpc/react.tsx (1)
api
(23-23)packages/ui/src/components/sonner.tsx (1)
toast
(19-19)apps/web/client/src/components/store/editor/sandbox/index.ts (1)
files
(287-289)packages/ui/src/components/icons/index.tsx (1)
Icons
(137-3592)
apps/web/client/src/server/api/routers/github.ts (2)
apps/web/client/src/server/api/lib/github-auth.ts (3)
createUserGitHubService
(86-94)getGitHubUserFromSession
(55-81)createGitHubOctokit
(6-11)apps/web/client/src/server/api/trpc.ts (1)
protectedProcedure
(130-149)
🔇 Additional comments (4)
apps/web/client/src/env.ts (1)
40-44
: Verify server-side declaration of GitHub secrets
Confirm that your server-side environment schema (not the client-onlyapps/web/client/src/env.ts
) includes all required GitHub secrets—GITHUB_CLIENT_ID
,GITHUB_CLIENT_SECRET
,GITHUB_APP_ID
,GITHUB_PRIVATE_KEY
,GITHUB_WEBHOOK_SECRET
,GITHUB_REDIRECT_URI
, andGITHUB_DEFAULT_SCOPES
. Add any that are missing in a server-only block.apps/web/client/src/components/store/editor/state/index.ts (1)
15-15
: LGTM: state flag and setter/reset for GitHub exportThe
githubExportOpen
flag, its setter, and reset inclear()
look consistent with existing modal flags.Also applies to: 39-41, 54-54
apps/web/client/src/app/project/[id]/_components/top-bar/index.tsx (1)
21-21
: LGTM: clean integration of GithubExportButton into the TopBarImport and placement look correct and consistent with surrounding UI controls.
Also applies to: 54-54
apps/web/client/src/app/project/[id]/_components/top-bar/github-export/modal.tsx (1)
123-132
: Reset logic is good; also clear localStorage state on close is handledModal cleanup and store close look correct.
// Request necessary scopes for user identity and repository management | ||
...(provider === 'github' && { | ||
scopes: 'user repo' | ||
}), | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reduce overbroad GitHub OAuth scopes and compare against the enum, not a string
Requesting user
grants write access to profile; prefer least-privilege read:user user:email
. Keep repo
only if private repo access is required (otherwise use public_repo
). Also compare with SignInMethod.GITHUB
for resilience to refactors.
- ...(provider === 'github' && {
- scopes: 'user repo'
- }),
+ ...(provider === SignInMethod.GITHUB && {
+ // Use least-privilege scopes; switch `repo`->`public_repo` if private repos are not needed.
+ scopes: 'read:user user:email repo'
+ }),
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Request necessary scopes for user identity and repository management | |
...(provider === 'github' && { | |
scopes: 'user repo' | |
}), | |
}, | |
// Request necessary scopes for user identity and repository management | |
...(provider === SignInMethod.GITHUB && { | |
// Use least-privilege scopes; switch `repo`→`public_repo` if private repos are not needed. | |
scopes: 'read:user user:email repo' | |
}), |
🤖 Prompt for AI Agents
In apps/web/client/src/app/login/actions.tsx around lines 29 to 33, the code
currently compares provider to the string 'github' and requests overly broad
OAuth scopes ('user' and 'repo'); change the comparison to use the
SignInMethod.GITHUB enum constant for robustness, and reduce scopes to
least-privilege: use 'read:user user:email' instead of 'user', and replace
'repo' with 'public_repo' unless private repo access is actually required—if
private access is needed, gate adding 'repo' behind an explicit flag/condition
and document why.
const files = Object.entries(filesMap).map(([path, file]) => { | ||
if (file.type === 'text') { | ||
return { path: path.replace(/^\.\//, ''), content: file.content, encoding: 'utf-8' as const }; | ||
} | ||
if (!file.content) { | ||
throw new Error(`File content is null for ${path}`); | ||
} | ||
const base64 = encodeToBase64(file.content); | ||
return { path: path.replace(/^\.\//, ''), content: base64, encoding: 'base64' as const }; | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Binary upload risk: encoding sent, but server ignores it; binaries will be corrupted
You set encoding: 'base64'
for non-text files, but the backend builds the git tree using content
directly (no blob creation), so GitHub will treat base64 as literal text. Fix on the server to create blobs and reference them by SHA.
Apply the server-side diff in apps/web/client/src/server/api/routers/github.ts (see comments there) and then verify by exporting a project containing at least one binary (e.g., PNG) and confirming GitHub renders it correctly.
🏁 Script executed:
#!/bin/bash
set -e
# Locate the GitHub router file
echo "Locating GitHub router implementation:"
find . -type f | grep "apps/web/client/src/server/api/routers/github.ts" || { echo "File not found"; exit 1; }
# Show the first 200 lines to find entrypoint and imports
echo -e "\n--- Beginning of router file ---"
sed -n '1,200p' apps/web/client/src/server/api/routers/github.ts
# Search for any handling of 'encoding' in the router
echo -e "\n--- Searching for 'encoding' references ---"
rg -n "encoding" -C3 apps/web/client/src/server/api/routers/github.ts || echo "No 'encoding' references in router"
# Search for createBlob calls (to see if blobs are created for non-text files)
echo -e "\n--- Searching for createBlob usage ---"
rg -n "createBlob" -C3 .
# Search for the GitHub tree/commit creation logic
echo -e "\n--- Searching for tree or commit creation steps ---"
rg -n "createTree" -C3 apps/web/client/src/server/api/routers/github.ts
rg -n "createCommit" -C3 apps/web/client/src/server/api/routers/github.ts
Length of output: 7602
🏁 Script executed:
#!/bin/bash
# Searching repository for createBlob usage without exiting on failure
echo "### createBlob usage ###"
rg -n "createBlob" -C3 apps/web/client/src/server/api/routers/github.ts || echo "No createBlob usage in router"
echo "### createTree usage ###"
rg -n "createTree" -C3 apps/web/client/src/server/api/routers/github.ts || echo "No createTree usage in router"
echo "### createCommit usage ###"
rg -n "createCommit" -C3 apps/web/client/src/server/api/routers/github.ts || echo "No createCommit usage in router"
echo "### octokit.rest.git usage ###"
rg -n "rest.git" -C3 apps/web/client/src/server/api/routers/github.ts || echo "No rest.git usage in router"
echo "### Code around encoding schema and handler ###"
sed -n '180,350p' apps/web/client/src/server/api/routers/github.ts
Length of output: 8536
Create blobs for base64-encoded files instead of inlining content
In uploadProjectFiles
(apps/web/client/src/server/api/routers/github.ts), the server ignores file.encoding
and passes base64 payloads directly to createTree
, so GitHub will treat them as literal text and corrupt binaries. For entries where encoding==='base64'
, call octokit.rest.git.createBlob({ content: file.content, encoding: 'base64' })
, then reference the returned sha
in the tree entry instead of content
. Verify by exporting a project with a PNG and confirming it renders correctly on GitHub.
const branchToUse = selectedBranch === 'custom' ? customBranch : selectedBranch; | ||
|
||
const repositoryOwner = connectedRepo.fullName?.split('/')[0] || selectedOwner; | ||
|
||
await uploadFilesMutation.mutateAsync({ | ||
owner: repositoryOwner, | ||
repo: connectedRepo.name, | ||
files, | ||
commitMessage: `Initial project setup - ${branchToUse} branch`, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Branch selection is ignored on upload — server always updates main
You compute branchToUse
but don’t send it to the mutation. Result: uploads always land on main.
await uploadFilesMutation.mutateAsync({
owner: repositoryOwner,
repo: connectedRepo.name,
files,
+ branch: branchToUse,
commitMessage: `Initial project setup - ${branchToUse} branch`,
});
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const branchToUse = selectedBranch === 'custom' ? customBranch : selectedBranch; | |
const repositoryOwner = connectedRepo.fullName?.split('/')[0] || selectedOwner; | |
await uploadFilesMutation.mutateAsync({ | |
owner: repositoryOwner, | |
repo: connectedRepo.name, | |
files, | |
commitMessage: `Initial project setup - ${branchToUse} branch`, | |
}); | |
const branchToUse = selectedBranch === 'custom' ? customBranch : selectedBranch; | |
const repositoryOwner = connectedRepo.fullName?.split('/')[0] || selectedOwner; | |
await uploadFilesMutation.mutateAsync({ | |
owner: repositoryOwner, | |
repo: connectedRepo.name, | |
files, | |
branch: branchToUse, | |
commitMessage: `Initial project setup - ${branchToUse} branch`, | |
}); |
🤖 Prompt for AI Agents
In
apps/web/client/src/app/project/[id]/_components/top-bar/github-export/modal.tsx
around lines 207 to 216, the computed branchToUse is never sent to the
uploadFilesMutation so the server defaults to main; include the branch in the
mutation payload (e.g. add branch: branchToUse or ref: branchToUse depending on
the server API) when calling uploadFilesMutation.mutateAsync, ensuring the
payload key matches what the server expects and updating any types if necessary.
// Try multiple ways to get the access token | ||
const accessToken = session.provider_token || | ||
githubIdentity.access_token || | ||
githubIdentity.provider_token || | ||
session.access_token; | ||
|
||
if (!accessToken) { | ||
throw new Error('GitHub access token not found. Please re-authenticate with GitHub.'); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not fall back to Supabase JWT for GitHub API auth
session.access_token
is a Supabase JWT, not a GitHub OAuth token. Passing it to Octokit will 401 and may leak an unrelated token in logs.
- const accessToken = session.provider_token ||
- githubIdentity.access_token ||
- githubIdentity.provider_token ||
- session.access_token;
+ const accessToken =
+ session.provider_token ||
+ githubIdentity.access_token ||
+ githubIdentity.provider_token;
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Try multiple ways to get the access token | |
const accessToken = session.provider_token || | |
githubIdentity.access_token || | |
githubIdentity.provider_token || | |
session.access_token; | |
if (!accessToken) { | |
throw new Error('GitHub access token not found. Please re-authenticate with GitHub.'); | |
} | |
// Try multiple ways to get the access token | |
const accessToken = | |
session.provider_token || | |
githubIdentity.access_token || | |
githubIdentity.provider_token; | |
if (!accessToken) { | |
throw new Error('GitHub access token not found. Please re-authenticate with GitHub.'); | |
} |
🤖 Prompt for AI Agents
In apps/web/client/src/server/api/lib/github-auth.ts around lines 31 to 39, the
code currently falls back to session.access_token which is a Supabase JWT and
must not be used for GitHub API calls; remove session.access_token from the
accessToken fallback list so only GitHub OAuth tokens (session.provider_token,
githubIdentity.access_token, githubIdentity.provider_token) are used, and add a
defensive check to ensure the chosen token is a GitHub OAuth token (e.g.,
non-JWT pattern or presence of expected provider metadata) before returning it;
also avoid logging the token value anywhere and throw the existing error if no
valid GitHub token is found so we never pass a Supabase JWT to Octokit.
const githubId = githubIdentity.id || githubIdentity.identity_data?.provider_id; | ||
const githubUsername = githubIdentity.identity_data?.user_name || | ||
githubIdentity.identity_data?.login || | ||
githubIdentity.identity_data?.preferred_username || | ||
session.user?.user_metadata?.user_name || | ||
session.user?.user_metadata?.preferred_username || | ||
session.user?.user_metadata?.name; | ||
|
||
if (!githubId || !githubUsername) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use provider_id for GitHub user id; the identity id
is not the provider user id
identities[].id
is a Supabase identity UUID. Prefer identity_data.provider_id
and only fallback if necessary.
- const githubId = githubIdentity.id || githubIdentity.identity_data?.provider_id;
+ const githubId =
+ githubIdentity.identity_data?.provider_id ||
+ githubIdentity.id; // fallback (may be non-numeric/UUID)
Also consider handling non-numeric values safely:
- if (!githubId || !githubUsername) {
+ if (!githubId || isNaN(parseInt(githubId, 10)) || !githubUsername) {
throw new Error('GitHub user information incomplete. Please re-authenticate.');
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const githubId = githubIdentity.id || githubIdentity.identity_data?.provider_id; | |
const githubUsername = githubIdentity.identity_data?.user_name || | |
githubIdentity.identity_data?.login || | |
githubIdentity.identity_data?.preferred_username || | |
session.user?.user_metadata?.user_name || | |
session.user?.user_metadata?.preferred_username || | |
session.user?.user_metadata?.name; | |
if (!githubId || !githubUsername) { | |
// Prefer the GitHub provider’s numeric ID; fallback to Supabase identity UUID if absent | |
const githubId = | |
githubIdentity.identity_data?.provider_id || | |
githubIdentity.id; // fallback (may be non-numeric/UUID) | |
const githubUsername = githubIdentity.identity_data?.user_name || | |
githubIdentity.identity_data?.login || | |
githubIdentity.identity_data?.preferred_username || | |
session.user?.user_metadata?.user_name || | |
session.user?.user_metadata?.preferred_username || | |
session.user?.user_metadata?.name; | |
// Ensure we have a GitHub ID that parses as a number and a username | |
if (!githubId || isNaN(parseInt(githubId, 10)) || !githubUsername) { | |
throw new Error('GitHub user information incomplete. Please re-authenticate.'); | |
} |
🤖 Prompt for AI Agents
In apps/web/client/src/server/api/lib/github-auth.ts around lines 63-71, the
code currently uses identities[].id (a Supabase UUID) as the GitHub user id;
change the assignment to prefer identity_data.provider_id first and only fall
back to identities[].id if provider_id is missing, and validate provider_id so
non-numeric values are handled safely (e.g. detect numeric strings vs
non-numeric and normalize or keep as string accordingly). Also keep the username
fallbacks as-is but ensure provider_id is prioritized for githubId and do not
assume it's always numeric — if you need a numeric id convert only when it
matches /^\d+$/; otherwise use the raw provider_id or the UUID fallback.
uploadProjectFiles: protectedProcedure | ||
.input( | ||
z.object({ | ||
owner: z.string(), | ||
repo: z.string(), | ||
files: z.array( | ||
z.object({ | ||
path: z.string(), | ||
content: z.string(), | ||
encoding: z.enum(['utf-8', 'base64']).default('utf-8'), | ||
}) | ||
), | ||
commitMessage: z.string().default('Initial project setup'), | ||
}) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Support uploading to arbitrary branches (not just main)
Add a branch
input (default 'main') and use it when reading/updating refs. Also create the branch if it doesn’t exist.
- .input(
- z.object({
- owner: z.string(),
- repo: z.string(),
- files: z.array(
+ .input(
+ z.object({
+ owner: z.string(),
+ repo: z.string(),
+ branch: z.string().default('main'),
+ files: z.array(
z.object({
path: z.string(),
content: z.string(),
encoding: z.enum(['utf-8', 'base64']).default('utf-8'),
})
),
commitMessage: z.string().default('Initial project setup'),
})
)
.mutation(async ({ input, ctx }) => {
const { octokit } = await getGitHubServiceForUser(ctx.supabase);
try {
- // Get the current commit SHA from the default branch
- const { data: refData } = await octokit.rest.git.getRef({
- owner: input.owner,
- repo: input.repo,
- ref: `heads/main`,
- });
+ const branchRef = `heads/${input.branch}`;
+ let refData;
+ try {
+ ({ data: refData } = await octokit.rest.git.getRef({
+ owner: input.owner,
+ repo: input.repo,
+ ref: branchRef,
+ }));
+ } catch (e: any) {
+ // If branch doesn't exist, create it from the repo's default branch
+ const { data: repoData } = await octokit.rest.repos.get({
+ owner: input.owner,
+ repo: input.repo,
+ });
+ const { data: defaultRef } = await octokit.rest.git.getRef({
+ owner: input.owner,
+ repo: input.repo,
+ ref: `heads/${repoData.default_branch}`,
+ });
+ await octokit.rest.git.createRef({
+ owner: input.owner,
+ repo: input.repo,
+ ref: `refs/${branchRef}`,
+ sha: defaultRef.object.sha,
+ });
+ ({ data: refData } = await octokit.rest.git.getRef({
+ owner: input.owner,
+ repo: input.repo,
+ ref: branchRef,
+ }));
+ }
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
uploadProjectFiles: protectedProcedure | |
.input( | |
z.object({ | |
owner: z.string(), | |
repo: z.string(), | |
files: z.array( | |
z.object({ | |
path: z.string(), | |
content: z.string(), | |
encoding: z.enum(['utf-8', 'base64']).default('utf-8'), | |
}) | |
), | |
commitMessage: z.string().default('Initial project setup'), | |
}) | |
) | |
uploadProjectFiles: protectedProcedure | |
.input( | |
z.object({ | |
owner: z.string(), | |
repo: z.string(), | |
branch: z.string().default('main'), | |
files: z.array( | |
z.object({ | |
path: z.string(), | |
content: z.string(), | |
encoding: z.enum(['utf-8', 'base64']).default('utf-8'), | |
}) | |
), | |
commitMessage: z.string().default('Initial project setup'), | |
}) | |
) | |
.mutation(async ({ input, ctx }) => { | |
const { octokit } = await getGitHubServiceForUser(ctx.supabase); | |
try { | |
const branchRef = `heads/${input.branch}`; | |
let refData; | |
try { | |
// Try to get the ref for the requested branch | |
({ data: refData } = await octokit.rest.git.getRef({ | |
owner: input.owner, | |
repo: input.repo, | |
ref: branchRef, | |
})); | |
} catch (e: any) { | |
// If the branch doesn't exist, create it from the default branch | |
const { data: repoData } = await octokit.rest.repos.get({ | |
owner: input.owner, | |
repo: input.repo, | |
}); | |
const { data: defaultRef } = await octokit.rest.git.getRef({ | |
owner: input.owner, | |
repo: input.repo, | |
ref: `heads/${repoData.default_branch}`, | |
}); | |
await octokit.rest.git.createRef({ | |
owner: input.owner, | |
repo: input.repo, | |
ref: `refs/${branchRef}`, | |
sha: defaultRef.object.sha, | |
}); | |
// Fetch the newly created branch ref | |
({ data: refData } = await octokit.rest.git.getRef({ | |
owner: input.owner, | |
repo: input.repo, | |
ref: branchRef, | |
})); | |
} | |
// ...continue building the tree, commit, etc., using refData.object.sha |
🤖 Prompt for AI Agents
In apps/web/client/src/server/api/routers/github.ts around lines 219 to 233, the
uploadProjectFiles input schema only accepts owner, repo, files and
commitMessage and always targets main; add a branch: z.string().default('main')
field to the input, then use that branch value when reading/updating git refs
(replace hardcoded 'main' with the branch variable), and if the ref for that
branch does not exist create it from the repository’s default branch or the
default branch ref (create a new ref pointing to the base commit SHA) before
committing file updates.
// Create tree entries for all files | ||
const treeEntries = input.files.map(file => ({ | ||
path: file.path, | ||
mode: '100644' as const, | ||
type: 'blob' as const, | ||
content: file.content, | ||
})); | ||
|
||
// Create a new tree with all files | ||
const { data: treeData } = await octokit.rest.git.createTree({ | ||
owner: input.owner, | ||
repo: input.repo, | ||
tree: treeEntries, | ||
base_tree: commitData.tree.sha, | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Preserve binary files: create blobs and reference by SHA in the tree
git.createTree
with content
treats it as UTF-8 text. Use git.createBlob
per file (base64) and put sha
into the tree entry. This also makes encoding
meaningful.
- // Create tree entries for all files
- const treeEntries = input.files.map(file => ({
- path: file.path,
- mode: '100644' as const,
- type: 'blob' as const,
- content: file.content,
- }));
+ // Create tree entries for all files via blobs (binary-safe)
+ const treeEntries = await Promise.all(
+ input.files.map(async (file) => {
+ const contentBase64 =
+ file.encoding === 'base64'
+ ? file.content
+ : Buffer.from(file.content, 'utf8').toString('base64');
+ const { data: blob } = await octokit.rest.git.createBlob({
+ owner: input.owner,
+ repo: input.repo,
+ content: contentBase64,
+ encoding: 'base64',
+ });
+ return {
+ path: file.path,
+ mode: '100644' as const,
+ type: 'blob' as const,
+ sha: blob.sha,
+ };
+ })
+ );
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Create tree entries for all files | |
const treeEntries = input.files.map(file => ({ | |
path: file.path, | |
mode: '100644' as const, | |
type: 'blob' as const, | |
content: file.content, | |
})); | |
// Create a new tree with all files | |
const { data: treeData } = await octokit.rest.git.createTree({ | |
owner: input.owner, | |
repo: input.repo, | |
tree: treeEntries, | |
base_tree: commitData.tree.sha, | |
}); | |
// Create tree entries for all files via blobs (binary-safe) | |
const treeEntries = await Promise.all( | |
input.files.map(async (file) => { | |
const contentBase64 = | |
file.encoding === 'base64' | |
? file.content | |
: Buffer.from(file.content, 'utf8').toString('base64'); | |
const { data: blob } = await octokit.rest.git.createBlob({ | |
owner: input.owner, | |
repo: input.repo, | |
content: contentBase64, | |
encoding: 'base64', | |
}); | |
return { | |
path: file.path, | |
mode: '100644' as const, | |
type: 'blob' as const, | |
sha: blob.sha, | |
}; | |
}) | |
); | |
// Create a new tree with all files | |
const { data: treeData } = await octokit.rest.git.createTree({ | |
owner: input.owner, | |
repo: input.repo, | |
tree: treeEntries, | |
base_tree: commitData.tree.sha, | |
}); |
🤖 Prompt for AI Agents
In apps/web/client/src/server/api/routers/github.ts around lines 254 to 269, the
current use of git.createTree with file.content writes blobs as UTF-8 text and
will corrupt binary files; instead call git.createBlob for each file (set
encoding to "base64" when content is base64-encoded), capture each blob's sha,
then build treeEntries using { path, mode: '100644', type: 'blob', sha } (omit
content) and pass those entries to git.createTree; ensure binary files are
base64-encoded before createBlob and re-use the returned sha in the tree
creation.
// Update the reference to point to the new commit | ||
await octokit.rest.git.updateRef({ | ||
owner: input.owner, | ||
repo: input.repo, | ||
ref: 'heads/main', | ||
sha: newCommit.sha, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Update the correct ref (selected branch) and avoid hardcoding main
Use the computed branchRef
.
- await octokit.rest.git.updateRef({
- owner: input.owner,
- repo: input.repo,
- ref: 'heads/main',
- sha: newCommit.sha,
- });
+ await octokit.rest.git.updateRef({
+ owner: input.owner,
+ repo: input.repo,
+ ref: branchRef,
+ sha: newCommit.sha,
+ });
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// Update the reference to point to the new commit | |
await octokit.rest.git.updateRef({ | |
owner: input.owner, | |
repo: input.repo, | |
ref: 'heads/main', | |
sha: newCommit.sha, | |
}); | |
// Update the reference to point to the new commit | |
await octokit.rest.git.updateRef({ | |
owner: input.owner, | |
repo: input.repo, | |
ref: branchRef, | |
sha: newCommit.sha, | |
}); |
🤖 Prompt for AI Agents
In apps/web/client/src/server/api/routers/github.ts around lines 279 to 285, the
code updates a hardcoded 'heads/main' ref; change it to use the computed
branchRef variable (the selected branch reference) instead of 'heads/main' so
the update targets the correct branch. Replace the hardcoded string with the
branchRef identifier that was computed earlier and ensure branchRef is defined
and passed to octokit.rest.git.updateRef({ ref: branchRef, ... }) so the
operation updates the intended branch.
# Conflicts: # apps/web/client/src/components/store/editor/state/index.ts
Description
Related Issues
Type of Change
Testing
Screenshots (if applicable)
Additional Notes
Important
Add GitHub export feature with UI components, state management, and API integration for project export functionality.
GithubExportButton
andGithubExportModal
ingithub-export/index.tsx
andgithub-export/modal.tsx
for exporting projects to GitHub.GithubExportButton
intoTopBar
inindex.tsx
.githubExportOpen
state inStateManager
instate/index.ts
.setGithubExportOpen()
to manage modal visibility.github-auth.ts
for GitHub OAuth and session management.githubRouter
inrouters/github.ts
with routes for validating repos, fetching user info, creating repos, and uploading files.login()
inactions.tsx
to requestuser
andrepo
scopes for GitHub OAuth.This description was created by
for 1b1a6d1. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Improvements