GitHub Publishing Workflow

Documentation for publishing build output to GitHub through fds-agent.

Overview

fds-agent supports automatic publishing of built projects to GitHub repositories. The process includes OAuth authorization, server-side session storage, JWT token generation for deployment, and automatic git push.

Session architecture:

Process Flow

1. OAuth authorization -> access token
2. JWT generation -> publish authorization
3. Build -> /process endpoint
4. Publish to GitHub -> git clone/commit/push

Steps

1. OAuth Authorization

Start authorization

Endpoint: GET /auth/github/start

Response:

{
  "redirectUrl": "https://github.com/login/oauth/authorize?client_id=...",
  "state": "random-state-token"
}

Implementation: agent.js:324-340

Creates a random state token for CSRF protection and returns the GitHub OAuth redirect URL.

GitHub callback

Endpoint: GET /auth/github/callback?code=...&state=...

Process:

  1. Validates the state token
  2. Exchanges the code for an access token via GitHub API
  3. Fetches user data (/user)
  4. Fetches organizations (/user/orgs)
  5. Fetches repositories (/user/repos?per_page=100&affiliation=owner,organization_member)
  6. Stores the session data

Implementation: agent.js:342-450

Redirect: /auth/success.html?session={sessionId}

Note: sessionId is a short random identifier (64 hex chars), and session data is stored on the server.

Get session data

Endpoint: GET /auth/session/:sessionId

Response:

{
  "user": {
    "login": "username",
    "id": 123,
    "name": "User Name",
    "type": "User"
  },
  "orgs": [{
    "login": "org-name",
    "id": 456,
    "name": "Organization",
    "avatar_url": "...",
    "type": "Organization"
  }],
  "repos": [{
    "full_name": "owner/repo",
    "default_branch": "main",
    "permissions": { "admin": true, "push": true, "pull": true },
    "owner": { "login": "owner", "type": "User" },
    "private": false
  }]
}

Implementation: agent.js:452-466

2. Create repository (optional)

Endpoint: POST /auth/create-repo

Request body:

{
  "sessionId": "64-char-hex-session-id",
  "owner": "username-or-org",
  "name": "repo-name",
  "private": false
}

Process:

  1. Validates repository name (pattern: ^[A-Za-z0-9._-]+$)
  2. Resolves owner type (User or Organization)
  3. Creates repository via GitHub API
  4. Adds the new repository to the session

Implementation: agent.js:513-575

Response:

{
  "repo": {
    "full_name": "owner/repo-name",
    "default_branch": "main",
    "permissions": { "admin": true, "push": true, "pull": true },
    "owner": { "login": "owner", "type": "User" },
    "private": false
  }
}

3. Generate JWT token

Endpoint: POST /auth/generate-jwt

Request body:

{
  "sessionId": "64-char-hex-session-id",
  "repo": "owner/repo-name",
  "branch": "main",
  "path": "dist"
}

Process:

  1. Validates session existence
  2. Validates repository access
  3. Uses default_branch when branch is not provided
  4. Generates JWT with payload:
    {
      "repo": "owner/repo-name",
      "branch": "main",
      "path": "dist",
      "user": "username",
      "iat": 1234567890
    }
    

Implementation: agent.js:468-511

Response:

{
  "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "payload": {
    "repo": "owner/repo-name",
    "branch": "main",
    "path": "dist",
    "user": "username",
    "iat": 1234567890
  },
  "note": "Use this JWT in the 'githubJwt' field when calling /process endpoint"
}

4. Publish via /process

Endpoint: POST /process

Request body:

{
  "projectName": "my-project",
  "target": "www",
  "recipe": { ... },
  "githubJwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

branch, license, and gqlSchemaMinVersion are set automatically by the agent.

Process:

  1. Decodes the JWT token
  2. Validates GitHub configuration (agent.js:558-577)
  3. Runs the build
  4. Runs publish after successful build

Implementation: agent.js:550-787

5. GitHub publish steps

Function: publishToGithub()

Implementation: agent.js:238-321

Steps:

  1. Clone repository

    BASIC_TOKEN=$(printf "x-access-token:%s" "{token}" | base64)
    git -c "http.extraheader=Authorization: Basic ${BASIC_TOKEN}" \
      clone --branch {branch} --single-branch \
      https://github.com/{repo}.git {cloneDir}
    

    token is the GitHub OAuth or PAT token, encoded as base64("x-access-token:{token}").

  2. Copy files

    • Removes the target folder in the repository
    • Copies build output into the publish path
  3. Configure git

    git config user.email "{authorEmail}"
    git config user.name "{authorName}"
    
  4. Check for changes

    git status --porcelain
    
    • If there are no changes, returns {status: "skipped"}
  5. Commit and push

    git add --all
    git commit -m "{commitMessage}"
    git push origin {branch}
    git rev-parse HEAD  # get commit hash
    

Return value:

{
  "status": "done",
  "repo": "owner/repo",
  "branch": "main",
  "path": "dist",
  "commit": "abc123..."
}

Configuration

Environment variables

Defined in agent.js:124-132:

Variable Description Default
GITHUB_APP_CLIENT_ID OAuth App Client ID (required for OAuth) -
GITHUB_APP_CLIENT_SECRET OAuth App Client Secret (required for OAuth) -
GITHUB_APP_TOKEN GitHub Personal Access Token for git push -
GITHUB_APP_AUTHOR_NAME Commit author name "fds-agent"
GITHUB_APP_AUTHOR_EMAIL Commit author email "fds-agent@local"
GITHUB_APP_BRANCH Default branch for deployments -
GITHUB_APP_COMMIT_MESSAGE Default commit message "Publish {projectName} [{target}] {hash}"
JWT_SECRET JWT signing secret "change-me-in-production"
BASE_URL Service base URL http://localhost:4000

GitHub config validation

Function: normalizeGithubConfig()

Implementation: agent.js:148-168

Checks:

Security

Secrets

Function: createRedactor()

Implementation: agent.js:185-192

All tokens are replaced with [secure] in logs.

Sessions

JWT tokens

Publish Statuses

{
  "publish": {
    "archive": {
      "status": "done",
      "type": "archive",
      "url": "/dist/{hash}/{filename}.tar"
    },
    "github": {
      "status": "done",
      "type": "github",
      "repo": "owner/repo",
      "branch": "main",
      "path": "dist",
      "commit": "abc123..."
    }
  }
}

Possible statuses:

Usage Example

// 1. Authorization
const { redirectUrl } = await fetch('/auth/github/start').then(r => r.json());
window.location.href = redirectUrl;

// 2. After callback, get sessionId
const sessionId = new URLSearchParams(window.location.search).get('session');

// 3. Create repository (optional)
const { repo } = await fetch('/auth/create-repo', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    sessionId,  // Short session ID, not a JWT
    owner: 'my-org',
    name: 'my-new-repo',
    private: false
  })
}).then(r => r.json());

// 4. Generate JWT
const { jwt } = await fetch('/auth/generate-jwt', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    sessionId,  // Short session ID, not a JWT
    repo: 'owner/repo',
    path: 'dist'
  })
}).then(r => r.json());

// 5. Start build and publish
const result = await fetch('/process', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    projectName: 'my-project',
    target: 'www',
    recipe: { /* ... */ },
    githubJwt: jwt
  })
}).then(r => r.json());

// 6. Check status
console.log(result.publish.github);
// { status: "done", repo: "owner/repo", commit: "abc123..." }

Error Handling

Common publish errors:

  1. Token missing

    github token is missing
    
  2. Invalid configuration

    github repo is required in JWT payload (format: org/repo)
    github path must not contain '..'
    
  3. Build output missing

    Build output path not found: /tmp/{hash}-build/...
    
  4. Git errors

    git {command} exited with code {code}
    

Helper Modules

githubAuth.js

Functions for URL and OAuth handling:

File: githubAuth.js

githubSession.js

Session helpers:

File: githubSession.js

See Also