code icon Code

Upload Archive to GitHub Releases

Upload an existing archive file to GitHub Releases for download

Source Code

import fs from "fs";

const [repoName = "sauna-exports", archivePath] = process.argv.slice(2);

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

if (!fs.existsSync(archivePath)) {
  console.error(`Error: Archive not found: ${archivePath}`);
  process.exit(1);
}

// Sanitize repo name
const sanitizedName = repoName
  .toLowerCase()
  .replace(/[^a-z0-9-]/g, "-")
  .replace(/-+/g, "-")
  .replace(/^-|-$/g, "");

if (!sanitizedName) {
  console.error("Error: Invalid repository name after sanitization");
  process.exit(1);
}

// Get archive info
const archiveStats = fs.statSync(archivePath);
const archiveSizeMB = (archiveStats.size / (1024 * 1024)).toFixed(1);
const archiveFilename = archivePath.split("/").pop();

// Generate release tag with timestamp
const now = new Date();
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1].split(".")[0].replace(/:/g, "");
const releaseTag = `export-${dateStr}-${timeStr}`;

console.log(`Uploading archive to GitHub Releases...`);
console.log(`  Archive: ${archivePath}`);
console.log(`  Size: ${archiveSizeMB} MB`);
console.log(`  Repository: ${sanitizedName}`);

// GitHub Releases API supports up to 2GB
if (archiveStats.size > 2 * 1024 * 1024 * 1024) {
  console.error(`Error: Archive exceeds GitHub's 2GB release asset limit`);
  process.exit(1);
}

// 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;
}

try {
  // Step 1: Get authenticated user
  console.log("\nConnecting to GitHub...");
  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(`  Authenticated as: ${username}`);

  // Step 2: Check if repo exists
  const repoCheckResponse = await githubFetch(
    `https://api.github.com/repos/${username}/${sanitizedName}`
  );

  let repoExists = repoCheckResponse.ok;

  // Step 3: Create repo if it doesn't exist
  if (!repoExists) {
    console.log(`  Repository ${sanitizedName} 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: sanitizedName,
          description: "Workspace exports from Sauna",
          private: true,
          auto_init: 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 (private)");
    await new Promise((resolve) => setTimeout(resolve, 2000));
  } else {
    console.log(`  Repository ${sanitizedName} exists`);
  }

  // Step 4: Create a release
  console.log(`\nCreating release ${releaseTag}...`);

  const createReleaseResponse = await githubFetch(
    `https://api.github.com/repos/${username}/${sanitizedName}/releases`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        tag_name: releaseTag,
        name: `Documents Export - ${dateStr}`,
        body: `Workspace documents export created on ${now.toISOString()}`,
        draft: false,
        prerelease: false,
      }),
    }
  );

  if (!createReleaseResponse.ok) {
    const error = await createReleaseResponse.text();
    console.error("Failed to create release");
    console.error(error);
    throw new Error("Release creation failed");
  }

  const release = await createReleaseResponse.json();
  console.log(`  Release created: ${release.html_url}`);

  // Step 5: Upload archive as release asset
  console.log(`\nUploading archive (${archiveSizeMB} MB)...`);

  const archiveBuffer = fs.readFileSync(archivePath);
  const uploadUrl = release.upload_url.replace(
    "{?name,label}",
    `?name=${encodeURIComponent(archiveFilename)}`
  );

  const uploadResponse = await fetch(uploadUrl, {
    method: "POST",
    headers: {
      Authorization: "Bearer PLACEHOLDER_TOKEN",
      Accept: "application/vnd.github.v3+json",
      "Content-Type": "application/gzip",
      "Content-Length": archiveBuffer.length.toString(),
      "User-Agent": "Sauna-Agent",
    },
    body: archiveBuffer,
  });

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

  const asset = await uploadResponse.json();
  console.log(`  Upload complete!`);

  // Output results
  const repoUrl = `https://github.com/${username}/${sanitizedName}`;
  const downloadUrl = asset.browser_download_url;

  console.log("\nโœ… Export successful!");
  console.log(`\n๐Ÿ“ฆ DOWNLOAD LINK: ${downloadUrl}`);
  console.log(`๐Ÿ“ Repository: ${repoUrl}`);
  console.log(`๐Ÿท๏ธ  Release: ${release.html_url}`);
  console.log(`๐Ÿ“Š Archive size: ${archiveSizeMB} MB`);

  const output = {
    success: true,
    downloadUrl: downloadUrl,
    repository: repoUrl,
    releaseUrl: release.html_url,
    releaseTag: releaseTag,
    repoName: sanitizedName,
    username: username,
    archiveFilename: archiveFilename,
    archiveSize: archiveStats.size,
    archiveSizeMB: parseFloat(archiveSizeMB),
    timestamp: new Date().toISOString(),
    isPrivate: true,
  };

  console.log("\n--- RESULT ---");
  console.log(JSON.stringify(output, null, 2));
} catch (error) {
  console.error(`\nโŒ Export failed: ${error.message}`);
  process.exit(1);
}