Skip to main content

GitHub Integration

GitHub OAuth, project export, repository creation, and practices.

GitHub OAuth Setup

1. OAuth App

  1. Open GitHub Developer SettingsOAuth AppsNew OAuth App.
  2. Homepage URL: https://your-domain or http://localhost:3000 for local.
  3. Authorization callback URL:
    • Production: https://your-domain/api/github/callback
    • Local: http://localhost:3000/api/github/callback

2. Environment variables

GITHUB_CLIENT_ID=your_client_id
GITHUB_CLIENT_SECRET=your_client_secret
GITHUB_CALLBACK_URL=https://your-domain/api/github/callback
For local dev: GITHUB_CALLBACK_URL=http://localhost:3000/api/github/callback. If GITHUB_CALLBACK_URL is unset, the code uses
${NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'}/api/github/callback.

OAuth Flow

1. Start authorization

GET /api/github/authorize?sessionId=...
  • Requires sessionId (workflow session).
  • Builds state from { sessionId } (base64), stores it in cookie github_oauth_state (HttpOnly, 10 min).
  • Redirects to:
    https://github.com/login/oauth/authorize?client_id=...&redirect_uri=...&scope=repo&state=...
    
    
  • scope=repo — full control of private repos (create, push, etc.).

2. Callback

GET /api/github/callback?code=...&state=...
  • Verifies state against github_oauth_state cookie.
  • Decodes state to get sessionId.
  • Exchanges code for access_token via https://github.com/login/oauth/access_token.
  • Fetches user with https://api.github.com/user.
  • Sets cookies:
    • github_access_token — HttpOnly, 30 days
    • github_user — username
    • Clears github_oauth_state
  • Redirects to: /execution?sessionId=...&githubConnected=true (or &githubError=... on failure).

3. Connection status

GET /api/github/status
  • Reads github_access_token and github_user from cookies.
  • If token present, calls https://api.github.com/user to check it. On 4xx, clears the auth cookies and returns connected: false.
  • Response: { connected: true, username } or { connected: false, error? }.

Project Export and Upload

Download (ZIP)

GET /api/projects/download?sessionId=...
  • Prefer: read ZIP from ZipFile (DB) for sessionId. If found, stream fileData as application/zip with Content-Disposition: attachment; filename="...".
  • Fallback: build ZIP from projects/{sessionId} on disk (or PROJECTS_DIR/projects/{sessionId} if PROJECTS_DIR is set). Project name for the filename comes from refinedRequirements.title or sessionId.
The generate step (POST /api/projects/generate) writes files under {PROJECTS_DIR||os.tmpdir()}/projects/{sessionId} and can also store the ZIP in ZipFile. For download to use the filesystem fallback, that path must exist and match what the server can read.

Upload to GitHub

POST /api/github/upload Body: { sessionId, repoName?, repoDescription?, isPrivate? }
  • Auth: github_access_token cookie (from OAuth). If missing → 401.
  • Repository:
    • repoName default: scriptonia-project-{first 8 chars of sessionId}.
    • repoDescription default: Project generated by Scriptonia.
    • private: isPrivate === true.
  • Source of files: join(process.cwd(), 'projects', sessionId). The handler reads from that directory recursively (skips node_modules, .git, .next, ., etc.). Text files use utf-8, others base64.
  • GitHub flow:
    1. POST /user/repos — create repo, auto_init: false.
    2. For each file: POST /repos/{full_name}/git/blobs (content + encoding).
    3. POST /repos/{full_name}/git/trees with the blob SHAs.
    4. POST /repos/{full_name}/git/commits with the tree and message Initial commit: Project generated by Scriptonia.
    5. PATCH /repos/{full_name}/git/refs/heads/main (or POST /refs if the ref does not exist) to point at the commit.
  • Response: { status: 'success', message, repositoryUrl } or { status: 'error', error }.
For upload to find files, the project must exist at projects/{sessionId} under the app’s process.cwd(). Ensure the generate/execution pipeline writes there, or that you configure PROJECTS_DIR and any file-writing logic so the same directory is used.

Repository Creation

  • Repos are created via GitHub’s Create a repository API with:
    • name, description, private, auto_init: false
  • First commit uses the Git Data API: blobs → tree → commit → update main (or create the ref). No .git on disk is required.

Best Practices

1. Callback URL

  • Must exactly match the OAuth App’s Authorization callback URL (including /api/github/callback).
  • Use HTTPS in production. For localhost, http://localhost:3000 is fine.

2. State and cookies

  • state is bound to sessionId and checked in the callback. Keep sessionId in state so the redirect can return the user to the right workflow.
  • github_oauth_state is short-lived (10 min) and cleared after use. github_access_token is 30 days; /api/github/status invalidates it when GitHub returns 4xx.

3. Scope

  • repo is broad (private repos, delete, etc.). If you only need to create and push to new repos, consider whether a more limited GitHub App or a different scope is better; the current implementation uses repo.

4. Where files come from

  • Upload reads from process.cwd()/projects/{sessionId}.
  • Generate writes to {PROJECTS_DIR||os.tmpdir()}/projects/{sessionId} and can also store a ZIP in ZipFile.
  • Align these (e.g. set PROJECTS_DIR to process.cwd() and ensure generate writes there) so upload finds the project. If you only persist in ZipFile, you’d need to extract to projects/{sessionId} (or change upload) before calling upload.

5. Token storage

  • Tokens are in HttpOnly cookies. For stronger security, move to a server-side session or DB keyed by user/session, and limit cookie scope/path.

6. Errors

  • Upload can fail with “Project files not found” if projects/{sessionId} is missing or empty.
  • “GitHub not connected” means the auth cookies are missing or cleared.
  • GitHub API errors (e.g. repo name taken, 403) are returned in error.