@malloydata/malloyyo 0.2.23 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +136 -35
- package/dist/templates/skills/malloyyo-auto-update/SKILL.md +117 -0
- package/dist/templates/skills/malloyyo-data-site/SKILL.md +174 -0
- package/dist/templates/skills/malloyyo-data-site/reference/data-to-parquet.md +109 -0
- package/dist/templates/skills/malloyyo-data-site/reference/publish-to-github-pages.md +61 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -99,8 +99,8 @@ function resolveTarget(dir, name) {
|
|
|
99
99
|
}
|
|
100
100
|
function resolveInstance(dir, arg) {
|
|
101
101
|
if (arg && /^https?:\/\//i.test(arg)) {
|
|
102
|
-
const
|
|
103
|
-
return { name:
|
|
102
|
+
const url5 = normalizeUrl(arg);
|
|
103
|
+
return { name: url5, url: url5 };
|
|
104
104
|
}
|
|
105
105
|
const targets = readTargetMap(dir);
|
|
106
106
|
const entries = Object.entries(targets);
|
|
@@ -2374,24 +2374,24 @@ function readAll() {
|
|
|
2374
2374
|
return {};
|
|
2375
2375
|
}
|
|
2376
2376
|
}
|
|
2377
|
-
function loadCreds(
|
|
2378
|
-
return readAll()[
|
|
2377
|
+
function loadCreds(url5) {
|
|
2378
|
+
return readAll()[url5];
|
|
2379
2379
|
}
|
|
2380
|
-
function saveCreds(
|
|
2380
|
+
function saveCreds(url5, creds) {
|
|
2381
2381
|
const p = credsPath();
|
|
2382
2382
|
mkdirSync(dirname(p), { recursive: true });
|
|
2383
2383
|
const all = readAll();
|
|
2384
|
-
all[
|
|
2384
|
+
all[url5] = creds;
|
|
2385
2385
|
writeFileSync(p, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
2386
2386
|
try {
|
|
2387
2387
|
chmodSync(p, 384);
|
|
2388
2388
|
} catch {
|
|
2389
2389
|
}
|
|
2390
2390
|
}
|
|
2391
|
-
function clearCreds(
|
|
2391
|
+
function clearCreds(url5) {
|
|
2392
2392
|
const all = readAll();
|
|
2393
|
-
if (!(
|
|
2394
|
-
delete all[
|
|
2393
|
+
if (!(url5 in all)) return false;
|
|
2394
|
+
delete all[url5];
|
|
2395
2395
|
writeFileSync(credsPath(), JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
2396
2396
|
return true;
|
|
2397
2397
|
}
|
|
@@ -2424,8 +2424,8 @@ async function registerClient(registrationEndpoint, redirectUri) {
|
|
|
2424
2424
|
if (!res.ok) throw new Error(`client registration failed: ${res.status} ${await res.text()}`);
|
|
2425
2425
|
return (await res.json()).client_id;
|
|
2426
2426
|
}
|
|
2427
|
-
function openBrowser(
|
|
2428
|
-
const [cmd, args] = process.platform === "darwin" ? ["open", [
|
|
2427
|
+
function openBrowser(url5) {
|
|
2428
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url5]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url5]] : ["xdg-open", [url5]];
|
|
2429
2429
|
try {
|
|
2430
2430
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
2431
2431
|
} catch {
|
|
@@ -3015,8 +3015,8 @@ window.addEventListener('message',async(e)=>{
|
|
|
3015
3015
|
dash.title
|
|
3016
3016
|
);
|
|
3017
3017
|
}
|
|
3018
|
-
function givensFromUrl(
|
|
3019
|
-
return givensFromSearch(
|
|
3018
|
+
function givensFromUrl(url5) {
|
|
3019
|
+
return givensFromSearch(url5.search);
|
|
3020
3020
|
}
|
|
3021
3021
|
function frameDoc(dash, givenSpecs, initialGivens, tileSpecs) {
|
|
3022
3022
|
const info = {
|
|
@@ -3061,7 +3061,7 @@ async function serveDashboard(opts) {
|
|
|
3061
3061
|
let byName = new Map(dashboards.map((d) => [d.name, d]));
|
|
3062
3062
|
const bundle = makeBundler();
|
|
3063
3063
|
const inPageBundle = makeInPageBundler();
|
|
3064
|
-
const pick = (
|
|
3064
|
+
const pick = (url5) => byName.get(url5.searchParams.get("d") ?? dashboards[0].name) ?? dashboards[0];
|
|
3065
3065
|
async function resolveGivens(dash) {
|
|
3066
3066
|
if (dash.tiles && dash.entryFile) {
|
|
3067
3067
|
const t = await runner.dashboardTiles(dash.entryFile, dash.tiles);
|
|
@@ -3095,15 +3095,15 @@ async function serveDashboard(opts) {
|
|
|
3095
3095
|
}
|
|
3096
3096
|
const handler = async (req, res) => {
|
|
3097
3097
|
const onFramePort = (req.socket.localPort ?? port) === framePort;
|
|
3098
|
-
const
|
|
3098
|
+
const url5 = new URL(req.url ?? "/", `http://localhost:${onFramePort ? framePort : port}`);
|
|
3099
3099
|
const send = (code, type, body, extra = {}) => {
|
|
3100
3100
|
res.writeHead(code, { "content-type": type, ...extra });
|
|
3101
3101
|
res.end(body);
|
|
3102
3102
|
};
|
|
3103
3103
|
try {
|
|
3104
3104
|
if (onFramePort) {
|
|
3105
|
-
if (
|
|
3106
|
-
const dash = pick(
|
|
3105
|
+
if (url5.pathname === "/frame") {
|
|
3106
|
+
const dash = pick(url5);
|
|
3107
3107
|
const g = await resolveGivens(dash);
|
|
3108
3108
|
if (!g.ok) {
|
|
3109
3109
|
return send(
|
|
@@ -3112,22 +3112,22 @@ async function serveDashboard(opts) {
|
|
|
3112
3112
|
html(`<pre style="color:crimson;padding:16px">model error: ${esc2(g.error)}</pre>`, dash.title)
|
|
3113
3113
|
);
|
|
3114
3114
|
}
|
|
3115
|
-
return send(200, "text/html; charset=utf-8", frameDoc(dash, g.union, givensFromUrl(
|
|
3115
|
+
return send(200, "text/html; charset=utf-8", frameDoc(dash, g.union, givensFromUrl(url5), g.tiles));
|
|
3116
3116
|
}
|
|
3117
|
-
if (
|
|
3118
|
-
return send(200, "application/javascript; charset=utf-8", await bundle(pick(
|
|
3117
|
+
if (url5.pathname === "/bundle.js") {
|
|
3118
|
+
return send(200, "application/javascript; charset=utf-8", await bundle(pick(url5)));
|
|
3119
3119
|
}
|
|
3120
3120
|
return send(404, "text/plain", "not found");
|
|
3121
3121
|
}
|
|
3122
|
-
if (
|
|
3122
|
+
if (url5.pathname === "/events") {
|
|
3123
3123
|
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
|
|
3124
3124
|
res.write("retry: 1000\n\n");
|
|
3125
3125
|
sseClients.add(res);
|
|
3126
3126
|
req.on("close", () => sseClients.delete(res));
|
|
3127
3127
|
return;
|
|
3128
3128
|
}
|
|
3129
|
-
if (
|
|
3130
|
-
const dash = pick(
|
|
3129
|
+
if (url5.pathname === "/") {
|
|
3130
|
+
const dash = pick(url5);
|
|
3131
3131
|
if (!dash.tsxPath) {
|
|
3132
3132
|
const g = await resolveGivens(dash);
|
|
3133
3133
|
if (!g.ok) {
|
|
@@ -3137,14 +3137,14 @@ async function serveDashboard(opts) {
|
|
|
3137
3137
|
html(`<pre style="color:crimson;padding:16px">model error: ${esc2(g.error)}</pre>`, dash.title)
|
|
3138
3138
|
);
|
|
3139
3139
|
}
|
|
3140
|
-
return send(200, "text/html; charset=utf-8", inPageShell(dash, dashboards, g.union, givensFromUrl(
|
|
3140
|
+
return send(200, "text/html; charset=utf-8", inPageShell(dash, dashboards, g.union, givensFromUrl(url5), g.tiles));
|
|
3141
3141
|
}
|
|
3142
|
-
return send(200, "text/html; charset=utf-8", parentShell(dash, frameBase, dashboards, givensFromUrl(
|
|
3142
|
+
return send(200, "text/html; charset=utf-8", parentShell(dash, frameBase, dashboards, givensFromUrl(url5)));
|
|
3143
3143
|
}
|
|
3144
|
-
if (
|
|
3144
|
+
if (url5.pathname === "/inpage.js") {
|
|
3145
3145
|
return send(200, "application/javascript; charset=utf-8", await inPageBundle());
|
|
3146
3146
|
}
|
|
3147
|
-
if (
|
|
3147
|
+
if (url5.pathname === "/api/run" && req.method === "POST") {
|
|
3148
3148
|
const { d, query, malloy, givens } = JSON.parse(await readBody(req));
|
|
3149
3149
|
const dash = byName.get(d);
|
|
3150
3150
|
if (!dash) return send(404, "application/json", JSON.stringify({ ok: false, problems: [{ message: `no dashboard '${d}'` }] }));
|
|
@@ -3624,6 +3624,7 @@ Publish: deploy ${path7.basename(outDir)}/ to Vercel (vercel.json written: clean
|
|
|
3624
3624
|
// src/init.ts
|
|
3625
3625
|
import fs7 from "node:fs";
|
|
3626
3626
|
import path8 from "node:path";
|
|
3627
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
3627
3628
|
var AUTHOR_MCP = {
|
|
3628
3629
|
mcpServers: {
|
|
3629
3630
|
// No -C: the server roots at the launch cwd (the project dir), so this file
|
|
@@ -3677,6 +3678,31 @@ function scaffoldIndex(root) {
|
|
|
3677
3678
|
note: anyNames ? `wrote index.malloy re-exporting ${models.length} model file(s) \u2014 REVIEW it` : "wrote index.malloy skeleton \u2014 no exports detected, fill them in by hand"
|
|
3678
3679
|
};
|
|
3679
3680
|
}
|
|
3681
|
+
function installSkills(root) {
|
|
3682
|
+
const distDir = path8.dirname(fileURLToPath2(import.meta.url));
|
|
3683
|
+
const candidates = [
|
|
3684
|
+
path8.join(distDir, "templates", "skills"),
|
|
3685
|
+
path8.join(distDir, "..", "src", "templates", "skills")
|
|
3686
|
+
];
|
|
3687
|
+
const srcSkills = candidates.find((p) => fs7.existsSync(p));
|
|
3688
|
+
if (!srcSkills) return { wrote: [], skipped: [], note: "no skill templates found \u2014 skipped" };
|
|
3689
|
+
const destSkills = path8.join(root, ".claude", "skills");
|
|
3690
|
+
fs7.mkdirSync(destSkills, { recursive: true });
|
|
3691
|
+
const wrote = [];
|
|
3692
|
+
const skipped = [];
|
|
3693
|
+
for (const name of fs7.readdirSync(srcSkills)) {
|
|
3694
|
+
const from = path8.join(srcSkills, name);
|
|
3695
|
+
if (!fs7.statSync(from).isDirectory()) continue;
|
|
3696
|
+
const to = path8.join(destSkills, name);
|
|
3697
|
+
if (fs7.existsSync(to)) {
|
|
3698
|
+
skipped.push(name);
|
|
3699
|
+
continue;
|
|
3700
|
+
}
|
|
3701
|
+
fs7.cpSync(from, to, { recursive: true });
|
|
3702
|
+
wrote.push(name);
|
|
3703
|
+
}
|
|
3704
|
+
return { wrote, skipped };
|
|
3705
|
+
}
|
|
3680
3706
|
async function initCmd(dir) {
|
|
3681
3707
|
const root = path8.resolve(dir);
|
|
3682
3708
|
if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) {
|
|
@@ -3693,6 +3719,17 @@ async function initCmd(dir) {
|
|
|
3693
3719
|
}
|
|
3694
3720
|
const idx = scaffoldIndex(root);
|
|
3695
3721
|
console.log(`${idx.wrote ? "\u2713" : "\u2022"} ${idx.note}`);
|
|
3722
|
+
const sk = installSkills(root);
|
|
3723
|
+
if (sk.note) {
|
|
3724
|
+
console.log(`\u2022 ${sk.note}`);
|
|
3725
|
+
} else {
|
|
3726
|
+
if (sk.wrote.length) {
|
|
3727
|
+
console.log(`\u2713 installed skill(s) into .claude/skills/: ${sk.wrote.join(", ")}`);
|
|
3728
|
+
}
|
|
3729
|
+
if (sk.skipped.length) {
|
|
3730
|
+
console.log(`\u2022 skill(s) already present \u2014 left as-is: ${sk.skipped.join(", ")}`);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3696
3733
|
console.log("");
|
|
3697
3734
|
console.log("Next:");
|
|
3698
3735
|
console.log(" claude # author mode (mcp__malloyyo_author__* tools)");
|
|
@@ -3700,24 +3737,81 @@ async function initCmd(dir) {
|
|
|
3700
3737
|
console.log(" malloyyo dashboard dev # see dashboards render in a browser");
|
|
3701
3738
|
}
|
|
3702
3739
|
|
|
3740
|
+
// src/sql.ts
|
|
3741
|
+
import fs8 from "node:fs";
|
|
3742
|
+
import path9 from "node:path";
|
|
3743
|
+
import url4 from "node:url";
|
|
3744
|
+
import { MalloyConfig as MalloyConfig3, discoverConfig as discoverConfig3 } from "@malloydata/malloy";
|
|
3745
|
+
function fileReader() {
|
|
3746
|
+
return {
|
|
3747
|
+
readURL: async (u) => {
|
|
3748
|
+
if (u.protocol !== "file:") {
|
|
3749
|
+
throw new Error(`unsupported URL scheme for import: ${u.href}`);
|
|
3750
|
+
}
|
|
3751
|
+
return fs8.promises.readFile(u, "utf8");
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
async function loadConfig3(rootDir) {
|
|
3756
|
+
const rootUrl = url4.pathToFileURL(rootDir.endsWith(path9.sep) ? rootDir : rootDir + path9.sep);
|
|
3757
|
+
const discovered = await discoverConfig3(rootUrl, rootUrl, fileReader()).catch(() => null);
|
|
3758
|
+
return discovered ?? new MalloyConfig3({ includeDefaultConnections: true }, {
|
|
3759
|
+
rootDirectory: rootUrl.toString()
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
async function readStdin() {
|
|
3763
|
+
const chunks = [];
|
|
3764
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
3765
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
3766
|
+
}
|
|
3767
|
+
async function resolveSql(opts) {
|
|
3768
|
+
if (opts.execute != null) return opts.execute;
|
|
3769
|
+
if (opts.file) return fs8.promises.readFile(opts.file, "utf8");
|
|
3770
|
+
return readStdin();
|
|
3771
|
+
}
|
|
3772
|
+
async function sqlCmd(connection, opts) {
|
|
3773
|
+
const name = connection ?? "duckdb";
|
|
3774
|
+
const rootDir = path9.resolve(opts.root ?? ".");
|
|
3775
|
+
const sql = (await resolveSql(opts)).trim();
|
|
3776
|
+
if (!sql) {
|
|
3777
|
+
throw new Error("no SQL provided \u2014 pass -e <sql>, -f <file>, or pipe it via stdin");
|
|
3778
|
+
}
|
|
3779
|
+
await import("@malloydata/malloy-connections");
|
|
3780
|
+
const cfg = await loadConfig3(rootDir);
|
|
3781
|
+
try {
|
|
3782
|
+
const conn = await cfg.connections.lookupConnection(name);
|
|
3783
|
+
const result = await conn.runSQL(sql);
|
|
3784
|
+
const rows = result?.rows ?? [];
|
|
3785
|
+
if (opts.json) {
|
|
3786
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
|
|
3787
|
+
} else if (rows.length === 0) {
|
|
3788
|
+
console.log(`ok \u2014 statement ran on connection "${name}" (no result rows)`);
|
|
3789
|
+
} else {
|
|
3790
|
+
console.table(rows);
|
|
3791
|
+
}
|
|
3792
|
+
} finally {
|
|
3793
|
+
await cfg.shutdown?.();
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3703
3797
|
// src/launch.ts
|
|
3704
3798
|
import { spawn as spawn2 } from "node:child_process";
|
|
3705
|
-
import
|
|
3799
|
+
import fs9 from "node:fs";
|
|
3706
3800
|
import os from "node:os";
|
|
3707
|
-
import
|
|
3801
|
+
import path10 from "node:path";
|
|
3708
3802
|
var SURFACE_FLAG = { author: "--develop", test: "--explore" };
|
|
3709
3803
|
var SERVER_KEY = { author: "malloyyo_author", test: "malloyyo_test" };
|
|
3710
3804
|
async function launchCmd(mode, opts) {
|
|
3711
|
-
const root =
|
|
3712
|
-
const tmpDir =
|
|
3713
|
-
const cfgPath =
|
|
3805
|
+
const root = path10.resolve(opts.root ?? process.cwd());
|
|
3806
|
+
const tmpDir = fs9.mkdtempSync(path10.join(os.tmpdir(), "malloyyo-launch-"));
|
|
3807
|
+
const cfgPath = path10.join(tmpDir, "mcp.json");
|
|
3714
3808
|
const cfg = {
|
|
3715
3809
|
mcpServers: {
|
|
3716
3810
|
// Absolute -C: an ephemeral config, so pinning the root is robust.
|
|
3717
3811
|
[SERVER_KEY[mode]]: { command: "malloyyo", args: ["mcp", SURFACE_FLAG[mode], "-C", root] }
|
|
3718
3812
|
}
|
|
3719
3813
|
};
|
|
3720
|
-
|
|
3814
|
+
fs9.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
|
|
3721
3815
|
const label = mode === "author" ? "AUTHOR (compile/edit)" : "TEST (claude.ai web preview)";
|
|
3722
3816
|
process.stderr.write(`\u25B6 launching Claude in ${label} mode over ${root}
|
|
3723
3817
|
`);
|
|
@@ -3736,11 +3830,11 @@ async function launchCmd(mode, opts) {
|
|
|
3736
3830
|
});
|
|
3737
3831
|
child.on("exit", () => resolve3());
|
|
3738
3832
|
});
|
|
3739
|
-
|
|
3833
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
3740
3834
|
}
|
|
3741
3835
|
|
|
3742
3836
|
// package.json
|
|
3743
|
-
var version = "0.2.
|
|
3837
|
+
var version = "0.2.24";
|
|
3744
3838
|
|
|
3745
3839
|
// src/index.ts
|
|
3746
3840
|
function shortSha(sha) {
|
|
@@ -3841,6 +3935,13 @@ program.command("mcp").option("-C, --root <dir>", "project root (default: curren
|
|
|
3841
3935
|
program.command("init").argument("[dir]", "model repo to set up", ".").description(
|
|
3842
3936
|
"set up a model repo: write .mcp.json so `cd <repo> && claude` opens in author mode, and scaffold index.malloy if missing"
|
|
3843
3937
|
).action(initCmd);
|
|
3938
|
+
program.command("sql").argument("[connection]", "connection name from malloy-config.json", "duckdb").option("-e, --execute <sql>", "SQL to run (else read from -f <file> or stdin)").option("-f, --file <path>", "read SQL from a file").option("-C, --root <dir>", "project root for malloy-config.json discovery (default: current directory)").option("-j, --json", "print result rows as JSON").description(
|
|
3939
|
+
"run raw SQL against a configured connection using the embedded DuckDB \u2014 e.g. COPY a web CSV into docs/*.parquet, no standalone duckdb needed"
|
|
3940
|
+
).action(
|
|
3941
|
+
async (connection, opts) => {
|
|
3942
|
+
await sqlCmd(connection, opts);
|
|
3943
|
+
}
|
|
3944
|
+
);
|
|
3844
3945
|
program.command("author").option("-C, --root <dir>", "project root (default: current directory)").description("launch Claude wired ONLY to the author surface (compile/edit the model)").action(async (opts) => {
|
|
3845
3946
|
await launchCmd("author", opts);
|
|
3846
3947
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: malloyyo-auto-update
|
|
3
|
+
description: Keep a malloyyo GitHub-Pages data site current automatically with a weekly GitHub Actions job — download from the source URL, transform, export parquet, commit. Use after a site is built (see malloyyo-data-site) when the data comes from a public URL that refreshes over time and you want the published site to track it with no manual steps. Worked example: malloydata/malloyyo-imdb.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auto-update a data site weekly
|
|
7
|
+
|
|
8
|
+
Add a scheduled GitHub Actions job that rebuilds the data and commits it, so the
|
|
9
|
+
published Pages site stays current on its own. Use this **after** the site works
|
|
10
|
+
(built with `malloyyo-data-site`). If the data never changes, skip this.
|
|
11
|
+
|
|
12
|
+
The worked example is `malloydata/malloyyo-imdb` — fetch and read its
|
|
13
|
+
`scripts/build_data.sh` and `.github/workflows/refresh-data.yml`; adapt, don't
|
|
14
|
+
copy blindly.
|
|
15
|
+
|
|
16
|
+
## Precondition: one command that rebuilds the data
|
|
17
|
+
|
|
18
|
+
The whole thing rests on a single script that turns the source URL(s) into the
|
|
19
|
+
committed parquet — the same script you'd run by hand. Keep it linear so it
|
|
20
|
+
reads as a recipe:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# scripts/build_data.sh
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
cd "$(dirname "$0")/.."
|
|
26
|
+
bash data/get.sh # 1. download source URLs -> data/
|
|
27
|
+
rm -f data/build.duckdb
|
|
28
|
+
malloy-cli -c malloy-build.json build transform.malloy # 2. transform
|
|
29
|
+
echo "ATTACH 'data/build.duckdb' AS b (READ_ONLY); -- 3. export -> docs/*.parquet
|
|
30
|
+
COPY b.thing TO 'docs/thing.parquet' (FORMAT parquet)" | malloyyo sql
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
(If you used Path A — direct — the whole script is just the one
|
|
34
|
+
`echo "COPY (FROM read_..('https://…')) TO 'docs/…'" | malloyyo sql` command.
|
|
35
|
+
No `data/get.sh`, `malloy-build.json`, `malloy-cli`, or transform needed — and
|
|
36
|
+
no standalone `duckdb` either, since `malloyyo sql` uses the embedded one.)
|
|
37
|
+
|
|
38
|
+
Gitignore the working files: `data/*.gz`, `data/*.duckdb`, `MANIFESTS/`.
|
|
39
|
+
|
|
40
|
+
## The workflow
|
|
41
|
+
|
|
42
|
+
`.github/workflows/refresh-data.yml`:
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
name: refresh-data
|
|
46
|
+
on:
|
|
47
|
+
schedule:
|
|
48
|
+
- cron: "0 6 * * 0" # weekly, Sunday 06:00 UTC
|
|
49
|
+
workflow_dispatch: # ...and on demand from the Actions tab
|
|
50
|
+
jobs:
|
|
51
|
+
refresh:
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
permissions:
|
|
54
|
+
contents: write # so the job can commit + push
|
|
55
|
+
steps:
|
|
56
|
+
- uses: actions/checkout@v4
|
|
57
|
+
- uses: actions/setup-node@v4
|
|
58
|
+
with: { node-version: "20" }
|
|
59
|
+
# malloyyo carries its own DuckDB (`malloyyo sql`), so no duckdb CLI to install.
|
|
60
|
+
# Add `@malloydata/cli` only for a `#@ persist` transform (malloy-cli build).
|
|
61
|
+
- run: npm install -g @malloydata/malloyyo @malloydata/cli
|
|
62
|
+
- uses: actions/setup-python@v5 # only if an enrichment step needs it
|
|
63
|
+
with: { python-version: "3.12" }
|
|
64
|
+
- name: Build data
|
|
65
|
+
run: bash scripts/build_data.sh
|
|
66
|
+
- name: Commit refreshed data
|
|
67
|
+
run: |
|
|
68
|
+
git config user.name github-actions
|
|
69
|
+
git config user.email github-actions@github.com
|
|
70
|
+
git add docs/*.parquet
|
|
71
|
+
git commit -m "refresh data $(date -u +%F)" || exit 0 # unchanged = clean no-op
|
|
72
|
+
git push
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Hard-won details (these came from a real first run)
|
|
76
|
+
|
|
77
|
+
- **`git commit … || exit 0`** — an unchanged week means nothing new upstream.
|
|
78
|
+
That is success, not a failed job.
|
|
79
|
+
- **Secondary/enrichment steps must be best-effort.** If the build has a step
|
|
80
|
+
that can fail independently (an external API lookup, an optional metadata
|
|
81
|
+
enrichment) and it isn't essential to the data, mark it
|
|
82
|
+
`continue-on-error: true`. Otherwise one flaky API call throws away an
|
|
83
|
+
otherwise-good data refresh — the commit step never runs and the fresh parquet
|
|
84
|
+
is lost. (In `malloyyo-imdb` this bit us: a poster-image lookup with a missing
|
|
85
|
+
secret failed the whole job until it was made non-blocking.)
|
|
86
|
+
- **Secrets** go in repo settings (Settings → Secrets and variables → Actions,
|
|
87
|
+
or `gh secret set NAME`), referenced as `${{ secrets.NAME }}`. A step reading
|
|
88
|
+
a secret that isn't set gets an empty string — pair that with best-effort.
|
|
89
|
+
- **No re-bundle needed for data changes.** The dashboards fetch parquet at
|
|
90
|
+
runtime, so committing fresh parquet updates the live site. Only install
|
|
91
|
+
`@malloydata/malloyyo` and run `malloyyo dashboard bundle` in CI if the job
|
|
92
|
+
also needs to regenerate the HTML (i.e. dashboard code changed — rare for a
|
|
93
|
+
data refresh).
|
|
94
|
+
- **Test before trusting the schedule.** Push, then Actions tab → the workflow →
|
|
95
|
+
**Run workflow** (that's `workflow_dispatch`). Or `gh workflow run
|
|
96
|
+
refresh-data.yml`, then `gh run watch <id> --exit-status`.
|
|
97
|
+
|
|
98
|
+
## Git growth, and the flatten
|
|
99
|
+
|
|
100
|
+
Each committed refresh adds the full parquet to history (~its file size per
|
|
101
|
+
run). That is intentional simplicity, not a leak. When history gets too big,
|
|
102
|
+
flatten it to a single commit by hand — **not** as part of the weekly job:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
git checkout --orphan flat && git add -A && git commit -m "flatten history"
|
|
106
|
+
git branch -M flat main && git push -f origin main
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Only do this on a repo you solely own (it force-pushes; other clones must
|
|
110
|
+
`git reset --hard origin/main`). Run it as often or as rarely as you like.
|
|
111
|
+
Document this in the repo's README so future-you remembers it's available.
|
|
112
|
+
|
|
113
|
+
## Done when
|
|
114
|
+
|
|
115
|
+
- The workflow file is on the default branch (so `workflow_dispatch` shows up)
|
|
116
|
+
- A manual run goes green end-to-end and commits fresh `docs/*.parquet`
|
|
117
|
+
- The Pages site shows the new data
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: malloyyo-data-site
|
|
3
|
+
description: Turn a public data URL into a browsable, interactive dashboard site hosted on GitHub Pages, using Malloy (malloyyo). Use when someone points at data on the web (CSV/TSV/Parquet/JSON at an https URL) and wants a public web interface to explore it — scaffold the repo with `malloyyo init`, transform the data into parquet under docs/, write the Malloy model + dashboards, preview with `malloyyo dashboard dev`, build with `malloyyo dashboard bundle`, and publish on GitHub Pages. Worked examples: malloydata/malloyyo-babynames (base) and malloydata/malloyyo-imdb (adds a transform + weekly auto-update).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a public data site from a web data pointer
|
|
7
|
+
|
|
8
|
+
Goal: given one or more **public data URLs**, produce a **GitHub Pages site** of
|
|
9
|
+
interactive Malloy dashboards that anyone can open in a browser. The data ends
|
|
10
|
+
up as parquet in `docs/`; the dashboards are static HTML that query it
|
|
11
|
+
client-side with DuckDB-WASM — so there is no server, no database to run.
|
|
12
|
+
|
|
13
|
+
Two reference repos — read them, they are the ground truth for structure:
|
|
14
|
+
|
|
15
|
+
- **`malloydata/malloyyo-babynames`** — the clean base pattern (one model file,
|
|
16
|
+
a few dashboards, `docs/`). Data is static.
|
|
17
|
+
- **`malloydata/malloyyo-imdb`** — the same, plus a `transform.malloy` that
|
|
18
|
+
cleans raw source files into parquet, plus a **weekly auto-refresh** (that
|
|
19
|
+
part is the separate `malloyyo-auto-update` skill).
|
|
20
|
+
|
|
21
|
+
Fetch either with `gh api repos/<repo>/git/trees/HEAD?recursive=1` and read the
|
|
22
|
+
files you need. Match their layout rather than inventing your own.
|
|
23
|
+
|
|
24
|
+
## The shape of a finished repo
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
malloy-config.json connection(s): duckdb (local), optionally md/gs mirrors
|
|
28
|
+
index.malloy the EXPORT SURFACE — only what this file exports is live
|
|
29
|
+
<model>.malloy sources, measures, joins, givens (parameters)
|
|
30
|
+
storage.malloy sources point at the parquet — docs-local OR an https URL
|
|
31
|
+
(or gs.malloy / md.malloy) same source names, swappable hosting (step 3)
|
|
32
|
+
dashboards/*.malloy the query behind each dashboard
|
|
33
|
+
dashboards/*.jsx the dashboard layout (grid of charts/tables)
|
|
34
|
+
docs/ PUBLISHED SITE — bundled HTML (+ the *.parquet if docs-local)
|
|
35
|
+
*.parquet the data, when hosted docs-local (committed; served by Pages)
|
|
36
|
+
*.html .nojekyll written by `malloyyo dashboard bundle`
|
|
37
|
+
.mcp.json written by `malloyyo init` (author-mode Claude)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Prerequisites (install once)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g @malloydata/malloyyo # the `malloyyo` command
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That's the whole toolchain for the common case. `malloyyo` has **DuckDB built
|
|
47
|
+
in** — `malloyyo sql` runs SQL (read a URL, `COPY` to parquet) through it, so you
|
|
48
|
+
do **not** need a standalone `duckdb` CLI.
|
|
49
|
+
|
|
50
|
+
Add `@malloydata/cli` (`malloy-cli`) **only** if you do a heavier transform with
|
|
51
|
+
Malloy `#@ persist` (see Path B in `reference/data-to-parquet.md`):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g @malloydata/cli # only for #@ persist transforms
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Recipe
|
|
58
|
+
|
|
59
|
+
Work top to bottom. After each step, prove it before moving on.
|
|
60
|
+
|
|
61
|
+
### 1. Scaffold
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
mkdir my-site && cd my-site && git init
|
|
65
|
+
malloyyo init # writes .mcp.json (author-mode Claude) + index.malloy stub
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Create `malloy-config.json` with a local DuckDB connection (this is what reads
|
|
69
|
+
the parquet):
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{ "connections": { "duckdb": { "is": "duckdb" } } }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Get the web data into `docs/*.parquet`
|
|
76
|
+
|
|
77
|
+
This is the one dataset-specific step. Two paths — pick the simpler one that
|
|
78
|
+
works. Full detail and copy-paste commands: **`reference/data-to-parquet.md`**.
|
|
79
|
+
|
|
80
|
+
- **Direct** (default): `malloyyo sql` reads the URL and writes parquet in one
|
|
81
|
+
shot, through the embedded DuckDB:
|
|
82
|
+
```bash
|
|
83
|
+
echo "COPY (FROM read_csv_auto('https://…/thing.csv')) TO 'docs/thing.parquet'" | malloyyo sql
|
|
84
|
+
```
|
|
85
|
+
Use when the web data is already close to what you want to show.
|
|
86
|
+
- **Transform** (when you need to clean / join / rank / reshape): write a
|
|
87
|
+
`transform.malloy` with `#@ persist` sources and build it with `malloy-cli`,
|
|
88
|
+
then export the tables to `docs/*.parquet`. This is the `malloyyo-imdb`
|
|
89
|
+
pattern.
|
|
90
|
+
|
|
91
|
+
Verify: `malloyyo sql -e "DESCRIBE SELECT * FROM 'docs/thing.parquet'"` and a
|
|
92
|
+
`SELECT count(*)`.
|
|
93
|
+
|
|
94
|
+
### 3. Write the model
|
|
95
|
+
|
|
96
|
+
A storage file — one source per parquet file — that the rest of the model builds
|
|
97
|
+
on. **Where the parquet lives is a choice** (the two examples differ here):
|
|
98
|
+
|
|
99
|
+
- **docs-local** (the `malloyyo-imdb` way) — parquet committed in `docs/`,
|
|
100
|
+
served same-origin by Pages. Self-contained; git carries the data. Address it
|
|
101
|
+
by project-relative path so the same spelling works locally and published:
|
|
102
|
+
```malloy
|
|
103
|
+
// storage.malloy
|
|
104
|
+
source: thing_table is duckdb.table('docs/thing.parquet') extend {}
|
|
105
|
+
```
|
|
106
|
+
- **External URL** (the `malloyyo-babynames` way — it uses `import "gs.malloy"`)
|
|
107
|
+
— parquet hosted on GCS / a CDN / any https URL; `docs/` holds only the HTML,
|
|
108
|
+
so git stays small. DuckDB-WASM fetches the URL at runtime:
|
|
109
|
+
```malloy
|
|
110
|
+
// gs.malloy
|
|
111
|
+
source: thing_table is duckdb.table('https://storage.googleapis.com/…/thing.parquet') extend {}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Keep the **source names identical** across storage files (as babynames does with
|
|
115
|
+
`gs.malloy` / `md.malloy`) so switching hosting is a one-line import change in
|
|
116
|
+
the model. Start docs-local (simplest); move to a URL if git growth bites.
|
|
117
|
+
|
|
118
|
+
`<model>.malloy` — build real sources on top: primary keys, measures, joins,
|
|
119
|
+
and **givens** (the parameters that become the dashboard's filter controls).
|
|
120
|
+
`index.malloy` — re-export exactly the sources/queries/givens the dashboards
|
|
121
|
+
use. **Only what `index.malloy` exports is visible** to dashboards, `dashboard
|
|
122
|
+
dev`, and the hosted app.
|
|
123
|
+
|
|
124
|
+
For Malloy modeling and givens specifics, lean on the author MCP rather than
|
|
125
|
+
guessing: the repo's `.mcp.json` wires `mcp__malloyyo_author__*`. Call
|
|
126
|
+
`mcp__malloyyo_author__compile` to check files and
|
|
127
|
+
`mcp__malloyyo_author__yo_help` for topics (`develop/working-with-models`).
|
|
128
|
+
|
|
129
|
+
### 4. Author dashboards and preview live
|
|
130
|
+
|
|
131
|
+
Each dashboard is a `.malloy` (the query/view) + a `.jsx` (the layout) under
|
|
132
|
+
`dashboards/`. Author them with the `malloyyo_author` MCP and its `yo_help`
|
|
133
|
+
topics — **read these, don't guess the JSX/grid API**:
|
|
134
|
+
`dashboards/authoring`, `dashboards/grid-layout`, `dashboards/vega-charts`.
|
|
135
|
+
|
|
136
|
+
Preview in a browser with live reload:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
malloyyo dashboard dev # serves at http://localhost:4173
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Iterate here until the dashboards look right. `malloyyo lint` validates the
|
|
143
|
+
dashboards against the model; `malloyyo test` previews what the hosted claude.ai
|
|
144
|
+
app would see.
|
|
145
|
+
|
|
146
|
+
### 5. Build the static site
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
malloyyo dashboard bundle --out docs
|
|
150
|
+
# optional: add analytics + a title
|
|
151
|
+
malloyyo dashboard bundle --out docs --title "My Site" --analytics G-XXXXXXXXXX
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`bundle` writes the HTML, `.nojekyll`, and assets into `docs/`, next to the
|
|
155
|
+
parquet from step 2. The pages fetch `./*.parquet` relative to the site root, so
|
|
156
|
+
the data must already be in `docs/` — always run the data step before bundling.
|
|
157
|
+
|
|
158
|
+
### 6. Publish on GitHub Pages
|
|
159
|
+
|
|
160
|
+
Commit `docs/` and turn on Pages (Settings → Pages → Deploy from a branch →
|
|
161
|
+
`main` / `docs`). Full steps + gotchas: **`reference/publish-to-github-pages.md`**.
|
|
162
|
+
|
|
163
|
+
### 7. (Optional) Keep it fresh automatically
|
|
164
|
+
|
|
165
|
+
If the data comes from a URL that updates over time and you want the site to
|
|
166
|
+
track it, add a weekly GitHub Actions refresh — see the **`malloyyo-auto-update`**
|
|
167
|
+
skill (worked example: `malloydata/malloyyo-imdb`).
|
|
168
|
+
|
|
169
|
+
## Done when
|
|
170
|
+
|
|
171
|
+
- `malloyyo sql -e "SELECT count(*) FROM 'docs/<file>.parquet'"` returns real rows
|
|
172
|
+
- `malloyyo dashboard dev` renders every dashboard with no errors
|
|
173
|
+
- `docs/` has the bundled `*.html` + `.nojekyll` alongside the parquet
|
|
174
|
+
- the Pages URL loads and the dashboards populate (data fetch succeeds)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Getting web data into `docs/*.parquet`
|
|
2
|
+
|
|
3
|
+
The site queries parquet files (in `docs/`, or at an https URL — see the model
|
|
4
|
+
step). This is how you turn a public data URL into that parquet. Pick the
|
|
5
|
+
simplest path that produces clean data.
|
|
6
|
+
|
|
7
|
+
`malloyyo sql` runs SQL through malloyyo's **embedded DuckDB** — no standalone
|
|
8
|
+
`duckdb` binary. It takes SQL from `-e`, `-f <file>`, or stdin, and runs against
|
|
9
|
+
a connection from `malloy-config.json` (default `duckdb`).
|
|
10
|
+
|
|
11
|
+
## Path A — Direct (default)
|
|
12
|
+
|
|
13
|
+
One command reads the URL and writes parquet. Best when the web data is already
|
|
14
|
+
close to what you want to display.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# CSV / TSV
|
|
18
|
+
echo "COPY (SELECT * FROM read_csv_auto('https://example.com/data.csv'))
|
|
19
|
+
TO 'docs/data.parquet' (FORMAT parquet)" | malloyyo sql
|
|
20
|
+
|
|
21
|
+
# TSV, gzipped, tab-delimited, header row (IMDb-style)
|
|
22
|
+
echo "COPY (SELECT * FROM read_csv_auto('https://example.com/data.tsv.gz',
|
|
23
|
+
delim='\t', header=true, all_varchar=true))
|
|
24
|
+
TO 'docs/data.parquet' (FORMAT parquet)" | malloyyo sql
|
|
25
|
+
|
|
26
|
+
# JSON / NDJSON
|
|
27
|
+
echo "COPY (SELECT * FROM read_json_auto('https://example.com/data.json'))
|
|
28
|
+
TO 'docs/data.parquet' (FORMAT parquet)" | malloyyo sql
|
|
29
|
+
|
|
30
|
+
# Already parquet on the web — just fetch it
|
|
31
|
+
curl -fsSL -o docs/data.parquet 'https://example.com/data.parquet'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For anything longer than a line or two, put the SQL in a file and run it with
|
|
35
|
+
`-f`, so it's re-runnable and diffable:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
malloyyo sql -f scripts/build.sql # a file of ;-separated statements
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Notes:
|
|
42
|
+
- DuckDB autoloads `httpfs` for `https://` reads.
|
|
43
|
+
- Keep only the columns/rows the dashboards need — `SELECT` the columns and add a
|
|
44
|
+
`WHERE` to drop noise. Smaller parquet = faster page loads.
|
|
45
|
+
- Cast types here if the source is all-strings: `col::INT`, `col::DOUBLE`, etc.
|
|
46
|
+
|
|
47
|
+
## Path B — Transform with Malloy (when you need to reshape)
|
|
48
|
+
|
|
49
|
+
Use when the data needs cleaning, joining across files, ranking, or nesting —
|
|
50
|
+
the `malloydata/malloyyo-imdb` case. You write the transform once in Malloy;
|
|
51
|
+
`malloy-cli build` materializes it; you export the tables to parquet. This path
|
|
52
|
+
also needs `@malloydata/cli` (`npm install -g @malloydata/cli`).
|
|
53
|
+
|
|
54
|
+
**1. A build connection** — `malloy-build.json` (a DuckDB db just for building):
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{ "connections": { "build": { "is": "duckdb", "databasePath": "data/build.duckdb" } } }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**2. `transform.malloy`** — read the raw source(s) through the `build`
|
|
61
|
+
connection and mark each output source with `#@ persist name="…"`:
|
|
62
|
+
|
|
63
|
+
```malloy
|
|
64
|
+
##! experimental.persistence experimental.virtual_source
|
|
65
|
+
|
|
66
|
+
source: raw is build.sql("""
|
|
67
|
+
SELECT * FROM read_csv_auto('data/raw.csv.gz', delim='\t', all_varchar=true, header=true)
|
|
68
|
+
""")
|
|
69
|
+
|
|
70
|
+
#@ persist name="thing"
|
|
71
|
+
source: thing_base is raw -> {
|
|
72
|
+
where: some_count::number > 100
|
|
73
|
+
select: id, name, value is value::number
|
|
74
|
+
calculate: rank is rank() { order_by: value::number desc }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Download the raw files first (a `data/get.sh` that `wget`s the URLs into `data/`,
|
|
79
|
+
gitignored). Gitignore `data/`, `data/*.duckdb`, and `MANIFESTS/`.
|
|
80
|
+
|
|
81
|
+
**3. Build, then export the persisted tables to `docs/`** — the export still
|
|
82
|
+
goes through `malloyyo sql` (no standalone duckdb needed):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
rm -f data/build.duckdb
|
|
86
|
+
malloy-cli -c malloy-build.json build transform.malloy # -> tables in build.duckdb
|
|
87
|
+
echo "ATTACH 'data/build.duckdb' AS b (READ_ONLY);
|
|
88
|
+
COPY b.thing TO 'docs/thing.parquet' (FORMAT parquet)" | malloyyo sql
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The persist `name=` is the table name inside `build.duckdb`; the `COPY` names the
|
|
92
|
+
served file (they can differ — e.g. `thing` → `docs/mysite_thing.parquet`).
|
|
93
|
+
|
|
94
|
+
Wrap steps 1–3 in a single `scripts/build_data.sh` so it is one command, by hand
|
|
95
|
+
and in CI. That's exactly what `malloyyo-auto-update` automates weekly.
|
|
96
|
+
|
|
97
|
+
## Either way, verify
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
malloyyo sql -e "DESCRIBE SELECT * FROM 'docs/thing.parquet'"
|
|
101
|
+
malloyyo sql -e "SELECT count(*) FROM 'docs/thing.parquet'"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Then point a storage source at it (docs-local shown; an https URL works too —
|
|
105
|
+
see the model step in SKILL.md):
|
|
106
|
+
|
|
107
|
+
```malloy
|
|
108
|
+
source: thing_table is duckdb.table('docs/thing.parquet') extend {}
|
|
109
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Publishing the site on GitHub Pages
|
|
2
|
+
|
|
3
|
+
The published site is just the `docs/` directory: bundled HTML plus the parquet
|
|
4
|
+
it queries. GitHub Pages serves it directly — no build step on GitHub's side.
|
|
5
|
+
|
|
6
|
+
## One-time setup
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
# from the repo root, after `malloyyo dashboard bundle --out docs`
|
|
10
|
+
gh repo create <owner>/<name> --public --source=. --remote=origin # or an existing repo
|
|
11
|
+
git add -A
|
|
12
|
+
git commit -m "initial data site"
|
|
13
|
+
git push -u origin main
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Turn on Pages, pointing at the `docs/` folder on the default branch:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gh api -X POST repos/<owner>/<name>/pages \
|
|
20
|
+
-f 'source[branch]=main' -f 'source[path]=/docs'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
(Or in the UI: Settings → Pages → Source: **Deploy from a branch** → Branch
|
|
24
|
+
`main`, folder `/docs`.)
|
|
25
|
+
|
|
26
|
+
The site appears at `https://<owner>.github.io/<name>/` within a minute or two.
|
|
27
|
+
|
|
28
|
+
## Why `docs/` and why `.nojekyll`
|
|
29
|
+
|
|
30
|
+
- Serving from `/docs` on the main branch keeps the data committed **once**
|
|
31
|
+
(it's the same directory you build into), not duplicated on a separate branch.
|
|
32
|
+
- `malloyyo dashboard bundle` writes a `.nojekyll` file so Pages serves the
|
|
33
|
+
bundled assets as-is instead of running them through Jekyll (which would drop
|
|
34
|
+
files beginning with `_`). Keep it committed.
|
|
35
|
+
|
|
36
|
+
## Updating the site
|
|
37
|
+
|
|
38
|
+
Re-run the data step and the bundle, then commit `docs/`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bash scripts/build_data.sh # or your Path-A duckdb command → docs/*.parquet
|
|
42
|
+
malloyyo dashboard bundle --out docs
|
|
43
|
+
git add docs && git commit -m "update" && git push
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Because the dashboards fetch the parquet client-side, **committing fresh parquet
|
|
47
|
+
is enough to update the live data** — you only need to re-`bundle` when you
|
|
48
|
+
change dashboard code. To automate the data refresh weekly, see the
|
|
49
|
+
`malloyyo-auto-update` skill.
|
|
50
|
+
|
|
51
|
+
## Gotchas
|
|
52
|
+
|
|
53
|
+
- **Data must be in `docs/` before you bundle** — the pages fetch `./*.parquet`
|
|
54
|
+
relative to the site root; `bundle` also reads the parquet to get schemas.
|
|
55
|
+
- **Big parquet grows git history.** Each committed refresh adds the full file.
|
|
56
|
+
Fine for occasional updates; for frequent auto-refresh, see the flatten note
|
|
57
|
+
in `malloyyo-auto-update`.
|
|
58
|
+
- **Git LFS does not work with Pages "deploy from a branch"** — Pages serves the
|
|
59
|
+
LFS pointer text, not the file. Commit parquet as normal git objects.
|
|
60
|
+
- **Private repos**: Pages on private repos needs a paid plan. Use a public repo
|
|
61
|
+
for a public site.
|