Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

pi/agent/skillz/github/github-cli/scripts/gh_pages_deploy.mjs

Raw
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, isAbsolute, resolve } from "node:path";
import { fileURLToPath } from "node:url";

function arg(name, def) {
	const i = process.argv.indexOf(name);
	return i === -1 ? def : process.argv[i + 1];
}
function has(name) {
	return process.argv.includes(name);
}
function json(x) {
	return JSON.stringify(x, null, 2);
}
function msg(s, style = "info") {
	return `${{ info: "ℹ", success: "✓", error: "✗", warning: "⚠" }[style] ?? ""} ${s}`;
}
function checkAuth(run = spawnSync) {
	const r = run("gh", ["auth", "status"], { encoding: "utf8" });
	return !r.error && r.status === 0;
}
function errorMessage(res) {
	return res.message ?? res.raw ?? "Unknown GitHub CLI error";
}

class Pages {
	constructor(repo, dryRun = false, jsonOut = false, run = spawnSync) {
		this.repo = repo;
		this.dryRun = dryRun;
		this.jsonOut = jsonOut;
		this.run = run;
		if (!checkAuth(run)) throw new Error("GitHub CLI is not authenticated; run 'gh auth login'");
	}
	api(endpoint, method = "GET", data) {
		if (this.dryRun && method !== "GET") {
			if (!this.jsonOut) {
				console.error(`[DRY RUN] Would execute: ${method} ${endpoint}`);
				if (data) console.error(`[DRY RUN] With data: ${JSON.stringify(data)}`);
			}
			return [0, { dry_run: true, endpoint, method }];
		}
		const args = [
			"api",
			"-X",
			method,
			"-H",
			"Accept: application/vnd.github+json",
			"-H",
			"X-GitHub-Api-Version: 2022-11-28",
			endpoint,
		];
		if (data)
			for (const [key, value] of Object.entries(data))
				if (typeof value === "object" && value)
					for (const [subkey, subvalue] of Object.entries(value))
						args.push("-f", `${key}[${subkey}]=${subvalue}`);
				else args.push("-f", `${key}=${typeof value === "boolean" ? String(value) : value}`);
		const r = this.run("gh", args, {
			encoding: "utf8",
			timeout: 30000,
			maxBuffer: 10 * 1024 * 1024,
		});
		let body = {};
		if (r.stdout?.trim()) {
			try {
				body = JSON.parse(r.stdout);
			} catch {
				body = { raw: r.stdout.trim() };
			}
		}
		if ((r.status ?? 1) !== 0 && !body.message)
			body.message = r.error?.message ?? r.stderr?.trim() ?? "GitHub CLI command failed";
		return [r.status ?? 1, body];
	}
	emit(out, success) {
		if (this.jsonOut) console.log(json(out));
		else if (out.ok) console.log(msg(success, "success"));
		else console.error(msg(out.error, "error"));
		return out;
	}
	enable(branch = "main", path = "/", buildType = "workflow", https = true) {
		if (!this.jsonOut) {
			console.log(`Enabling GitHub Pages for ${this.repo}...`);
			console.log(`  Branch: ${branch}`);
			console.log(`  Path: ${path}`);
			console.log(`  Build type: ${buildType}`);
		}
		const endpoint = `/repos/${this.repo}/pages`;
		const [status, res] = this.api(endpoint, "POST", {
			source: { branch, path },
			build_type: buildType,
		});
		if (status !== 0)
			return this.emit(
				{ ok: false, status: "error", action: "enable", error: errorMessage(res) },
				"",
			);
		if (res.dry_run)
			return this.emit(
				{
					ok: true,
					status: "dry_run",
					action: "pages_enabled",
					repository: this.repo,
					config: { branch, path, build_type: buildType, https_enforced: https },
				},
				"GitHub Pages would be enabled (dry-run)",
			);
		if (https) {
			const [httpsStatus, httpsRes] = this.api(endpoint, "PUT", { https_enforced: true });
			if (httpsStatus !== 0)
				return this.emit(
					{
						ok: false,
						status: "error",
						action: "enforce_https",
						pages_enabled: true,
						error: errorMessage(httpsRes),
					},
					"",
				);
		}
		return this.emit(
			{
				ok: true,
				status: "success",
				action: "pages_enabled",
				repository: this.repo,
				config: { branch, path, build_type: buildType, https_enforced: https },
			},
			"GitHub Pages enabled successfully",
		);
	}
	status() {
		if (!this.jsonOut) console.log(`Checking GitHub Pages status for ${this.repo}...`);
		const [status, res] = this.api(`/repos/${this.repo}/pages`);
		if (status !== 0)
			return this.emit(
				{ ok: false, status: "error", action: "status", error: errorMessage(res) },
				"",
			);
		const out = {
			ok: true,
			status: "enabled",
			repository: this.repo,
			url: res.html_url ?? "N/A",
			pages_status: res.status ?? "N/A",
			build_type: res.build_type ?? "N/A",
			source: res.source ?? {},
			https_enforced: res.https_enforced ?? false,
			custom_domain: res.cname,
		};
		if (this.jsonOut) console.log(json(out));
		else {
			console.log(msg("GitHub Pages is enabled", "success"));
			console.log(`  URL: ${out.url}`);
			console.log(`  Status: ${out.pages_status}`);
			console.log(`  Build type: ${out.build_type}`);
			console.log(`  Source branch: ${out.source.branch ?? "N/A"}`);
			console.log(`  Source path: ${out.source.path ?? "N/A"}`);
			console.log(`  HTTPS enforced: ${out.https_enforced}`);
			if (out.custom_domain) console.log(`  Custom domain: ${out.custom_domain}`);
		}
		return out;
	}
	rebuild() {
		if (!this.jsonOut) console.log(`Triggering rebuild for ${this.repo}...`);
		const [status, res] = this.api(`/repos/${this.repo}/pages/builds`, "POST");
		if (status !== 0)
			return this.emit(
				{ ok: false, status: "error", action: "rebuild", error: errorMessage(res) },
				"",
			);
		return this.emit(
			{
				ok: true,
				status: res.dry_run ? "dry_run" : "triggered",
				repository: this.repo,
				build_url: res.url,
			},
			res.dry_run ? "Build would be triggered (dry-run)" : "Build triggered successfully",
		);
	}
	latest() {
		const [status, res] = this.api(`/repos/${this.repo}/pages/builds/latest`);
		if (status !== 0)
			return this.emit(
				{ ok: false, status: "error", action: "latest", error: errorMessage(res) },
				"",
			);
		const out = {
			ok: true,
			status: res.status ?? "N/A",
			commit: res.commit ?? "N/A",
			created_at: res.created_at,
			error: res.error ?? {},
		};
		if (this.jsonOut) console.log(json(out));
		else {
			console.log("\nLatest build:");
			console.log(`  Status: ${out.status}`);
			console.log(`  Commit: ${out.commit}`);
			if (out.created_at) console.log(`  Created: ${out.created_at}`);
			if (out.error?.message) console.log(`  Error: ${out.error.message}`);
		}
		return out;
	}
}

function validateSiteDir(siteDir) {
	if (!siteDir?.trim()) throw new Error("An explicit site directory is required via --site-dir");
	const normalized = siteDir.replaceAll("\\", "/");
	if (
		isAbsolute(siteDir) ||
		/^[A-Za-z]:/.test(siteDir) ||
		normalized.startsWith("~") ||
		normalized.split("/").some((part) => part === ".." || part === "") ||
		!/^[A-Za-z0-9._/-]+$/.test(normalized) ||
		normalized === "."
	)
		throw new Error("Site directory must be a safe repository-relative path");
	return normalized;
}
function createWorkflow(output = ".github/workflows/pages.yml", force = false, dryRun = false, siteDir) {
	const artifactPath = validateSiteDir(siteDir);
	const existing = existsSync(output);
	if (existing && !force) {
		const out = { ok: false, status: "skipped", reason: "file_exists", path: resolve(output) };
		console.log(json(out));
		return out;
	}
	if (dryRun) {
		const out = {
			ok: true,
			status: "dry_run",
			action: existing ? "replace_workflow" : "create_workflow",
			path: resolve(output),
			site_directory: artifactPath,
		};
		console.log(json(out));
		return out;
	}
	const content = `name: Deploy to GitHub Pages

on:
  push:
    branches: ["main"]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  deploy:
    environment:
      name: github-pages
      url: \${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Pages
        uses: actions/configure-pages@v5
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: '${artifactPath}'
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
`;
	mkdirSync(dirname(output), { recursive: true });
	writeFileSync(output, content);
	const out = {
		ok: true,
		status: "created",
		path: resolve(output),
		site_directory: artifactPath,
	};
	console.log(json(out));
	console.log(
		"\nReview the workflow, then commit and push it. The site directory must contain only publishable artifacts.",
	);
	return out;
}

function main() {
	const cmd = process.argv.find((value) =>
		["enable", "status", "rebuild", "create-workflow"].includes(value),
	);
	if (!cmd) {
		console.error("Usage: gh_pages_deploy.mjs <enable|status|rebuild|create-workflow>");
		return 1;
	}
	const dryRun = has("--dry-run");
	const jsonOut = has("--json");
	if (cmd === "create-workflow") {
		const result = createWorkflow(arg("--output"), has("--force"), dryRun, arg("--site-dir"));
		return result.ok ? 0 : 1;
	}
	const repo = process.argv[process.argv.indexOf(cmd) + 1];
	if (!repo) throw new Error("repo required");
	const pages = new Pages(repo, dryRun, jsonOut);
	let result;
	if (cmd === "enable") {
		result = pages.enable(
			arg("--branch", "main"),
			arg("--path", "/"),
			arg("--build-type", "workflow"),
			!has("--no-https"),
		);
		if (result.ok && arg("--build-type", "workflow") === "workflow" && !jsonOut)
			console.log(
				"\nTip: scaffold deployment with:\n   node scripts/gh_pages_deploy.mjs create-workflow --site-dir <publishable-directory>",
			);
	} else if (cmd === "status") {
		result = pages.status();
		if (result.ok && has("--build-info")) result = pages.latest();
	} else result = pages.rebuild();
	return result.ok ? 0 : 1;
}

export const __test = {
	Pages,
	createWorkflow,
	json,
	msg,
	validateSiteDir,
};

if (process.argv[1] === fileURLToPath(import.meta.url)) {
	try {
		process.exit(main());
	} catch (error) {
		console.error(json({ ok: false, status: "error", error: error.message }));
		process.exit(1);
	}
}