Documentation for publishing build output to GitHub through fds-agent.
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:
1. OAuth authorization -> access token
2. JWT generation -> publish authorization
3. Build -> /process endpoint
4. Publish to GitHub -> git clone/commit/push
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.
Endpoint: GET /auth/github/callback?code=...&state=...
Process:
/user)/user/orgs)/user/repos?per_page=100&affiliation=owner,organization_member)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.
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
Endpoint: POST /auth/create-repo
Request body:
{
"sessionId": "64-char-hex-session-id",
"owner": "username-or-org",
"name": "repo-name",
"private": false
}
Process:
^[A-Za-z0-9._-]+$)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
}
}
Endpoint: POST /auth/generate-jwt
Request body:
{
"sessionId": "64-char-hex-session-id",
"repo": "owner/repo-name",
"branch": "main",
"path": "dist"
}
Process:
{
"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"
}
Endpoint: POST /process
Request body:
{
"projectName": "my-project",
"target": "www",
"recipe": { ... },
"githubJwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
branch, license, and gqlSchemaMinVersion are set automatically by the agent.
Process:
Implementation: agent.js:550-787
Function: publishToGithub()
Implementation: agent.js:238-321
Steps:
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}").
Copy files
Configure git
git config user.email "{authorEmail}"
git config user.name "{authorName}"
Check for changes
git status --porcelain
{status: "skipped"}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..."
}
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 |
Function: normalizeGithubConfig()
Implementation: agent.js:148-168
Checks:
repo must match: ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$path must not include ..token is requiredbranch defaults to "main"Function: createRedactor()
Implementation: agent.js:185-192
All tokens are replaced with [secure] in logs.
JWT_SECRET{
"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:
pending - waitingin_progress - runningdone - finished successfullyskipped - skipped (no changes)error - error// 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..." }
Common publish errors:
Token missing
github token is missing
Invalid configuration
github repo is required in JWT payload (format: org/repo)
github path must not contain '..'
Build output missing
Build output path not found: /tmp/{hash}-build/...
Git errors
git {command} exited with code {code}
Functions for URL and OAuth handling:
resolveBaseUrl() - resolve base URLbuildGithubAuthorizeUrl() - build authorize URLbuildGithubCallbackUrl() - build callback URLbuildAuthSuccessUrl() - build success URLFile: githubAuth.js
Session helpers:
isValidRepoName() - validate repository nameresolveOwner() - resolve owner (User or Organization)addRepoToSession() - add repository to sessionFile: githubSession.js