code icon Code

Publish to GitHub Pages

Upload an HTML file to a GitHub repository for GitHub Pages hosting

Source Code

import fs from "fs";

const [projectName, htmlPath, commitMessage = "Deploy web app", trackingPath] =
  process.argv.slice(2);

// Validate inputs
if (!projectName) {
  console.error("Error: projectName is required");
  process.exit(1);
}

if (!htmlPath) {
  console.error("Error: htmlPath is required");
  process.exit(1);
}

if (!fs.existsSync(htmlPath)) {
  console.error(`Error: File not found: ${htmlPath}`);
  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);
}

// Read the HTML file
const htmlContent = fs.readFileSync(htmlPath, "utf-8");
const contentBase64 = Buffer.from(htmlContent).toString("base64");

console.log(`Publishing "${sanitizedName}" to GitHub Pages...`);
console.log(`  Source: ${htmlPath}`);
console.log(`  Size: ${htmlContent.length} bytes`);

try {
  // Step 1: Get authenticated user
  const userResponse = await fetch("https://api.github.com/user", {
    headers: {
      Authorization: "Bearer PLACEHOLDER_TOKEN",
      Accept: "application/vnd.github.v3+json",
      "User-Agent": "Sauna-Agent",
    },
  });

  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(`  Authenticated as: ${username}`);

  // Step 2: Check if repo exists
  const repoName = sanitizedName;
  const repoCheckResponse = await fetch(
    `https://api.github.com/repos/${username}/${repoName}`,
    {
      headers: {
        Authorization: "Bearer PLACEHOLDER_TOKEN",
        Accept: "application/vnd.github.v3+json",
        "User-Agent": "Sauna-Agent",
      },
    }
  );

  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 fetch(
      "https://api.github.com/user/repos",
      {
        method: "POST",
        headers: {
          Authorization: "Bearer PLACEHOLDER_TOKEN",
          Accept: "application/vnd.github.v3+json",
          "Content-Type": "application/json",
          "User-Agent": "Sauna-Agent",
        },
        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: Check for existing index.html to get SHA
  const getFileResponse = await fetch(
    `https://api.github.com/repos/${username}/${repoName}/contents/index.html`,
    {
      headers: {
        Authorization: "Bearer PLACEHOLDER_TOKEN",
        Accept: "application/vnd.github.v3+json",
        "User-Agent": "Sauna-Agent",
      },
    }
  );

  let sha = null;
  if (getFileResponse.ok) {
    const fileData = await getFileResponse.json();
    sha = fileData.sha;
    console.log("  Updating existing index.html");
  } else {
    console.log("  Creating new index.html");
  }

  // Step 5: Upload/update index.html
  const uploadBody = {
    message: commitMessage,
    content: contentBase64,
  };

  if (sha) {
    uploadBody.sha = sha;
  }

  const uploadResponse = await fetch(
    `https://api.github.com/repos/${username}/${repoName}/contents/index.html`,
    {
      method: "PUT",
      headers: {
        Authorization: "Bearer PLACEHOLDER_TOKEN",
        Accept: "application/vnd.github.v3+json",
        "Content-Type": "application/json",
        "User-Agent": "Sauna-Agent",
      },
      body: JSON.stringify(uploadBody),
    }
  );

  if (!uploadResponse.ok) {
    const error = await uploadResponse.text();
    console.error("Failed to upload file");
    console.error(error);
    throw new Error("File upload failed");
  }

  const result = await uploadResponse.json();

  // Step 6: Enable GitHub Pages if new repo
  if (!repoExists) {
    console.log("  Enabling GitHub Pages...");

    // Try to enable Pages - may fail if already enabled or needs time
    try {
      await fetch(
        `https://api.github.com/repos/${username}/${repoName}/pages`,
        {
          method: "POST",
          headers: {
            Authorization: "Bearer PLACEHOLDER_TOKEN",
            Accept: "application/vnd.github.v3+json",
            "Content-Type": "application/json",
            "User-Agent": "Sauna-Agent",
          },
          body: JSON.stringify({
            source: {
              branch: "main",
              path: "/",
            },
          }),
        }
      );
    } catch {
      // Pages may already be enabled or need manual setup
      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āœ… Published successfully!");
  console.log(`\nšŸ“ Live URL: ${publishedUrl}`);
  console.log(`šŸ“ Repository: ${repoUrl}`);
  console.log(`šŸ“ Commit: ${result.commit.sha.substring(0, 7)}`);
  console.log(
    "\nNote: GitHub Pages may take 1-2 minutes to deploy after the first push."
  );

  // Output structured result for downstream use
  const output = {
    success: true,
    url: publishedUrl,
    repository: repoUrl,
    project: sanitizedName,
    username: username,
    commit: result.commit.sha,
    timestamp: new Date().toISOString(),
  };

  console.log("\n--- RESULT ---");
  console.log(JSON.stringify(output, null, 2));

  // Step 7: 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, ""),
      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āŒ Publishing failed: ${error.message}`);
  process.exit(1);
}