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