Deploy Web App with Assets
Upload a directory of files (HTML, images, assets) to GitHub Pages. Preserves folder structure and supports binary files.
Source Code
import fs from "fs";
import path from "path";
const [
projectName,
sourceDir,
commitMessage = "Deploy web app",
trackingPath,
] = process.argv.slice(2);
// Validate inputs
if (!projectName) {
console.error("Error: projectName is required");
process.exit(1);
}
if (!sourceDir) {
console.error("Error: sourceDir is required");
process.exit(1);
}
if (!fs.existsSync(sourceDir)) {
console.error(`Error: Directory not found: ${sourceDir}`);
process.exit(1);
}
if (!fs.statSync(sourceDir).isDirectory()) {
console.error(`Error: Not a directory: ${sourceDir}`);
process.exit(1);
}
// Sanitize project name
const sanitizedName = projectName
.toLowerCase()
.replace(/[^a-z0-9-]/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
if (!sanitizedName) {
console.error("Error: Invalid project name after sanitization");
process.exit(1);
}
// Walk directory recursively and collect all files
function walkDir(dir, baseDir = dir) {
const files = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...walkDir(fullPath, baseDir));
} else if (entry.isFile()) {
const relativePath = path.relative(baseDir, fullPath);
files.push({ fullPath, relativePath });
}
}
return files;
}
// Collect all files to upload
const files = walkDir(sourceDir);
if (files.length === 0) {
console.error("Error: No files found in source directory");
process.exit(1);
}
// Check for index.html
const hasIndex = files.some(
(f) => f.relativePath === "index.html" || f.relativePath === "index.htm"
);
if (!hasIndex) {
console.warn(
"Warning: No index.html found. GitHub Pages may not serve correctly."
);
}
console.log(`Deploying "${sanitizedName}" to GitHub Pages...`);
console.log(` Source: ${sourceDir}`);
console.log(` Files: ${files.length}`);
files.forEach((f) => console.log(` - ${f.relativePath}`));
// GitHub API helper
async function githubFetch(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
Authorization: "Bearer PLACEHOLDER_TOKEN",
Accept: "application/vnd.github.v3+json",
"User-Agent": "Sauna-Agent",
...options.headers,
},
});
return response;
}
// Upload a single file to GitHub
async function uploadFile(username, repoName, filePath, relativePath) {
// Read file as binary buffer
const fileBuffer = fs.readFileSync(filePath);
const contentBase64 = fileBuffer.toString("base64");
const fileSize = fileBuffer.length;
// Check for existing file to get SHA (needed for updates)
const getFileResponse = await githubFetch(
`https://api.github.com/repos/${username}/${repoName}/contents/${relativePath}`
);
let sha = null;
if (getFileResponse.ok) {
const fileData = await getFileResponse.json();
sha = fileData.sha;
}
// Upload/update file
const uploadBody = {
message: `${sha ? "Update" : "Add"} ${relativePath}`,
content: contentBase64,
};
if (sha) {
uploadBody.sha = sha;
}
const uploadResponse = await githubFetch(
`https://api.github.com/repos/${username}/${repoName}/contents/${relativePath}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(uploadBody),
}
);
if (!uploadResponse.ok) {
const error = await uploadResponse.text();
throw new Error(`Failed to upload ${relativePath}: ${error}`);
}
return { relativePath, size: fileSize, updated: !!sha };
}
try {
// Step 1: Get authenticated user
const userResponse = await githubFetch("https://api.github.com/user");
if (!userResponse.ok) {
const error = await userResponse.text();
console.error("Failed to authenticate with GitHub");
console.error(error);
throw new Error("GitHub authentication failed");
}
const user = await userResponse.json();
const username = user.login;
console.log(`\n Authenticated as: ${username}`);
// Step 2: Check if repo exists
const repoName = sanitizedName;
const repoCheckResponse = await githubFetch(
`https://api.github.com/repos/${username}/${repoName}`
);
let repoExists = repoCheckResponse.ok;
// Step 3: Create repo if it doesn't exist
if (!repoExists) {
console.log(` Repository ${repoName} does not exist. Creating...`);
const createRepoResponse = await githubFetch(
"https://api.github.com/user/repos",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: repoName,
description: `Web app: ${projectName}`,
homepage: `https://${username}.github.io/${repoName}`,
private: false,
auto_init: true,
has_pages: true,
}),
}
);
if (!createRepoResponse.ok) {
const error = await createRepoResponse.text();
console.error("Failed to create repository");
console.error(error);
throw new Error("Repository creation failed");
}
console.log(" Repository created successfully");
// Wait for repo initialization
await new Promise((resolve) => setTimeout(resolve, 2000));
} else {
console.log(` Repository ${repoName} exists`);
}
// Step 4: Upload all files
console.log("\n Uploading files...");
const uploadResults = [];
for (const file of files) {
const result = await uploadFile(
username,
repoName,
file.fullPath,
file.relativePath
);
uploadResults.push(result);
console.log(
` โ ${result.relativePath} (${result.size} bytes)${result.updated ? " [updated]" : ""}`
);
}
// Step 5: Enable GitHub Pages if new repo
if (!repoExists) {
console.log("\n Enabling GitHub Pages...");
try {
await githubFetch(
`https://api.github.com/repos/${username}/${repoName}/pages`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
source: {
branch: "main",
path: "/",
},
}),
}
);
console.log(" GitHub Pages enabled");
} catch {
console.log(" Note: GitHub Pages may need manual activation");
}
}
// Output results
const publishedUrl = `https://${username}.github.io/${repoName}`;
const repoUrl = `https://github.com/${username}/${repoName}`;
console.log("\nโ
Deployed successfully!");
console.log(`\n๐ Live URL: ${publishedUrl}`);
console.log(`๐ Repository: ${repoUrl}`);
console.log(`๐ฆ Files deployed: ${uploadResults.length}`);
console.log(
"\nNote: GitHub Pages may take 1-2 minutes to deploy after push."
);
// Output structured result for downstream use
const output = {
success: true,
url: publishedUrl,
repository: repoUrl,
project: sanitizedName,
username: username,
files: uploadResults.map((r) => r.relativePath),
totalFiles: uploadResults.length,
totalSize: uploadResults.reduce((sum, r) => sum + r.size, 0),
timestamp: new Date().toISOString(),
};
console.log("\n--- RESULT ---");
console.log(JSON.stringify(output, null, 2));
// Step 6: Update tracking file if provided
if (trackingPath) {
let sites = [];
if (fs.existsSync(trackingPath)) {
sites = JSON.parse(fs.readFileSync(trackingPath, "utf-8"));
}
const existing = sites.findIndex((s) => s.project === sanitizedName);
const entry = {
project: sanitizedName,
url: publishedUrl,
repository: repoUrl,
description: commitMessage.replace(/^(Deploy|Update):\s*/i, ""),
files: output.files,
deployedAt:
existing === -1 ? output.timestamp : sites[existing].deployedAt,
lastUpdated: output.timestamp,
};
if (existing === -1) {
sites.push(entry);
} else {
sites[existing] = entry;
}
fs.writeFileSync(trackingPath, JSON.stringify(sites, null, 2));
console.log(`\n๐ Updated tracking: ${trackingPath}`);
}
} catch (error) {
console.error(`\nโ Deployment failed: ${error.message}`);
process.exit(1);
}