Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295#!/usr/bin/env node// deploy-status.mjs — a tiny deploy-status bridge for the slab menubar.//// Reports whether a repo's latest commit on each watched branch has finished// deploying, as JSON the Swift menubar renders into a `Deploy` submenu// (mirrors `asana`). The signal is Cloudflare's "Workers Builds" GitHub// check-runs — one per worker per environment (e.g. `Workers Builds:// api-staging`) — so "deployed or not" is just whether those checks have all// gone green on the branch tip. No Cloudflare API token needed; the same// GitHub read access slab already uses is enough.//// This file ships in the PUBLIC aesthetic.computer repo, so it carries NO// repo names or tokens: the watched targets and the GitHub PAT live in an// UNTRACKED config at ~/.config/slab/deploy-status.json (see// `deploy-status config`). If no token is set there, we fall back to// `gh auth token` for machines where the GitHub CLI is signed in.//// Subcommands:// deploy-status status [--target <name>] [--env <name>] JSON summary (default)// deploy-status config print/create config stub// deploy-status open [--target <name>] [--env <name>] open the deploy in a browser//// Per-repo: `targets` is a list, so this watches one repo (fuser) today and// extends to more by adding entries — `--target <name>` selects one.
import { spawnSync } from "node:child_process";import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";import { homedir } from "node:os";import { join, dirname } from "node:path";
const HOME = homedir();const CONFIG_PATH = process.env.SLAB_DEPLOY_STATUS_CONFIG || join(HOME, ".config", "slab", "deploy-status.json");const API = "https://api.github.com";
// ─── config ──────────────────────────────────────────────────────────────
const CONFIG_STUB = { _README: "slab deploy-status config — UNTRACKED, never committed. List the repos " + "to watch under `targets`; each maps an environment name to the branch " + "whose tip deploys it. The deploy signal is Cloudflare 'Workers Builds' " + "check-runs named `<buildsPrefix> <worker>-<env>`.", token: "REPLACE_ME", tokenComment: "GitHub PAT with read access to the repos' commit statuses/checks. " + "Leave as REPLACE_ME (or blank) to fall back to `gh auth token`.", targets: [ { name: "example", repo: "owner/repo", environments: { staging: "staging", production: "main" }, buildsPrefix: "Workers Builds:", }, ], default: "example",};
function loadConfig() { if (!existsSync(CONFIG_PATH)) return null; try { const cfg = JSON.parse(readFileSync(CONFIG_PATH, "utf8")); if (!cfg || !Array.isArray(cfg.targets) || cfg.targets.length === 0) return null; const real = cfg.targets.filter((t) => t.repo && !/owner\/repo/.test(t.repo)); if (real.length === 0) return null; return { ...cfg, targets: real }; } catch { return null; }}
function writeStub() { mkdirSync(dirname(CONFIG_PATH), { recursive: true }); if (!existsSync(CONFIG_PATH)) { writeFileSync(CONFIG_PATH, JSON.stringify(CONFIG_STUB, null, 2) + "\n"); }}
// Ask the GitHub CLI for a token. The menubar spawns us under launchd's// minimal PATH (no Homebrew/fnm shims), so `gh` won't be on PATH — resolve it// at the usual absolute locations, mirroring the wrapper's node lookup.function ghToken() { const candidates = [ "gh", "/opt/homebrew/bin/gh", "/usr/local/bin/gh", `${HOME}/.local/bin/gh`, ]; for (const bin of candidates) { try { const r = spawnSync(bin, ["auth", "token"], { encoding: "utf8" }); if (r.status === 0 && r.stdout.trim()) return r.stdout.trim(); } catch { // try the next candidate } } return null;}
// Token precedence: config → env → `gh auth token` (CLI sign-in).function resolveToken(cfg) { if (cfg?.token && !/REPLACE_ME/.test(cfg.token)) return cfg.token; if (process.env.GH_TOKEN) return process.env.GH_TOKEN; if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN; return ghToken();}
// ─── GitHub REST ───────────────────────────────────────────────────────────
async function gh(token, path) { const res = await fetch(`${API}${path}`, { headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", "User-Agent": "slab-deploy-status", }, }); if (!res.ok) { const err = new Error(`github ${res.status}`); err.status = res.status; throw err; } return res.json();}
// ─── deploy state ────────────────────────────────────────────────────────
// Roll the per-worker check-runs for one branch tip into a single state.// deployed → every Workers Build succeeded// building → at least one still queued/in-progress (none failing)// failing → at least one concluded unsuccessfully// none → no Workers Builds checks on this commitfunction rollUp(apps) { if (apps.length === 0) return "none"; if (apps.some((a) => a.state === "failing")) return "failing"; if (apps.some((a) => a.state === "building")) return "building"; return "deployed";}
const GLYPH = { deployed: "✓", building: "⏳", failing: "✗", none: "·" };
function appState(run) { if (run.status !== "completed") return "building"; return run.conclusion === "success" || run.conclusion === "neutral" ? "deployed" : "failing";}
async function envStatus(token, repo, env, branch, prefix) { const head = await gh(token, `/repos/${repo}/commits/${branch}`); const sha = head.sha; const checks = await gh( token, `/repos/${repo}/commits/${sha}/check-runs?per_page=100`, ); const wantSuffix = `-${env}`; const apps = (checks.check_runs || []) .filter((r) => r.name.startsWith(prefix)) .map((r) => ({ name: r.name .slice(prefix.length) .trim() .replace(new RegExp(`${wantSuffix}$`), ""), raw: r.name.slice(prefix.length).trim(), state: appState(r), conclusion: r.conclusion, url: r.details_url || r.html_url, completedAt: r.completed_at, })) // Only the workers for THIS environment (suffix match), when discernible. .filter((a) => a.raw.endsWith(wantSuffix) || a.raw === a.name); const state = rollUp(apps); const completed = apps.map((a) => a.completedAt).filter(Boolean).sort(); return { env, branch, sha: sha.slice(0, 7), message: (head.commit?.message || "").split("\n")[0], author: head.commit?.author?.name || head.author?.login || "", committedAt: head.commit?.author?.date || head.commit?.committer?.date || "", state, deployedAt: state === "deployed" ? completed[completed.length - 1] : "", apps: apps.map(({ raw, ...a }) => a), url: `https://github.com/${repo}/commit/${sha}`, };}
function pickTarget(cfg, name) { const want = name || cfg.default; return cfg.targets.find((t) => t.name === want) || cfg.targets[0];}
async function status(cfg, opts) { const token = resolveToken(cfg); if (!token) return { configured: false, label: "Deploy: no GitHub token", }; const target = pickTarget(cfg, opts.target); const prefix = target.buildsPrefix || "Workers Builds:"; const envMap = target.environments || { staging: "staging" }; const wantEnvs = opts.env && opts.env !== "all" ? { [opts.env]: envMap[opts.env] } : envMap;
const envs = []; for (const [env, branch] of Object.entries(wantEnvs)) { if (!branch) continue; try { envs.push(await envStatus(token, target.repo, env, branch, prefix)); } catch (e) { envs.push({ env, branch, state: "error", error: String(e.message || e) }); } }
// Menubar parent label: "<target> <glyph> <env> …" newest-env-first. const summary = envs .map((e) => `${GLYPH[e.state] || "?"} ${e.env}`) .join(" "); return { configured: true, target: target.name, label: `${target.name} ${summary}`.trim(), state: rollUp( envs.map((e) => ({ state: e.state === "deployed" ? "deployed" : e.state })), ), envs, url: envs[0]?.url || `https://github.com/${target.repo}`, };}
// ─── CLI ────────────────────────────────────────────────────────────────
function parseFlags(argv) { const opts = {}; for (let i = 0; i < argv.length; i++) { if (argv[i] === "--target") opts.target = argv[++i]; else if (argv[i] === "--env") opts.env = argv[++i]; } return opts;}
async function main() { const [cmd = "status", ...rest] = process.argv.slice(2); const opts = parseFlags(rest);
if (cmd === "config") { writeStub(); console.log(CONFIG_PATH); return; }
const cfg = loadConfig();
if (cmd === "open") { if (!cfg) return console.log(CONFIG_PATH); const target = pickTarget(cfg, opts.target); const url = `https://github.com/${target.repo}/commits/${ (target.environments || {})[opts.env || "staging"] || "staging" }`; spawnSync("open", [url]); return; }
// status (default) if (!cfg) { console.log( JSON.stringify({ configured: false, label: "Deploy: not set up", configPath: CONFIG_PATH, }), ); return; } try { console.log(JSON.stringify(await status(cfg, opts))); } catch (e) { console.log( JSON.stringify({ configured: true, label: `Deploy: ${e.status || ""} error`.trim(), error: String(e.message || e), }), ); }}
main();