comfyui-mcp 0.25.1 → 0.26.0

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.
@@ -1,29 +1,88 @@
1
1
  #!/usr/bin/env node
2
- // Build + push the FULL (fat) RunPod template image to Docker Hub.
2
+ // npm-publish-style release of the RunPod template image to Docker Hub.
3
3
  //
4
- // npm run runpod:release # auto-increments the 1.x tag
5
- // npm run runpod:release -- 2.0 # explicit tag
4
+ // npm run runpod:release # minor bump: 1.6 -> 1.7
5
+ // npm run runpod:release -- major # major bump: 1.6 -> 2.0
6
+ // npm run runpod:release -- 2.3 # explicit tag
6
7
  //
7
- // This is the image the RunPod template pulls (artokun/comfyui-mcp-runpod on
8
- // Docker Hub) — the GitHub Actions workflow only builds the LEAN variant to
9
- // ghcr.io (standard runners can't hold the 63 GB donor pull), so the fat
10
- // template image is released from a dev machine with Docker + the layer cache.
8
+ // Like `npm publish`, nothing reaches the registry unverified. The flow:
11
9
  //
12
- // The panel git-clone layer is cache-busted with the CURRENT panel main SHA so
13
- // every release bakes the latest panel (boot auto-update then keeps it fresh).
10
+ // 1. VERSION — resolve the next tag from what Docker Hub actually serves
11
+ // (two-part MAJOR.MINOR scheme; hub is the source of truth,
12
+ // not local docker tags).
13
+ // 2. BUILD — fat image (extras + spotcheck). The extras donor defaults to
14
+ // the PREVIOUS release (it carries the same three artifacts),
15
+ // so no 63 GB aitrepreneur pull; the panel clone layer is
16
+ // cache-busted to the panel repo's current main SHA. The build
17
+ // itself ends in the Dockerfile §9.5 integrity gate.
18
+ // 3. GATE — docker/runpod/deploy-dockerhub.sh runs test_image.sh (static
19
+ // + boot suite: fresh volume, redeploy persistence, ENOSPC
20
+ // self-heal). Any failure aborts before anything is pushed.
21
+ // 4. PUBLISH — push :<version> + :latest, then verify_image_remote.py
22
+ // checks what the registry ACTUALLY serves.
23
+ // 5. TEMPLATE — pin the RunPod template to the new VERSION tag via the
24
+ // GraphQL API (RUNPOD_API_KEY env or .env). NOT runpodctl:
25
+ // its `template update` sends a registry-credentials field
26
+ // that RunPod rejects for PUBLIC templates ("public templates
27
+ // cannot have Registry Credentials", verified 2026-07 on
28
+ // runpodctl 2.6.1). Pods must pull by version tag: RunPod
29
+ // hosts cache per-tag, so a template left on :latest can
30
+ // silently serve a stale build.
31
+ //
32
+ // Env knobs: RUNPOD_DONOR (extras donor image; set to aitrepreneur/comfyui:2.3.5
33
+ // for a from-scratch bootstrap), RUNPOD_TEMPLATE_ID (default bnqtkvcer3),
34
+ // RUNPOD_API_KEY (or .env), SKIP_TEMPLATE=1 to stop after publishing.
14
35
  import { execFileSync, execSync } from "node:child_process";
36
+ import { readFileSync, existsSync } from "node:fs";
15
37
  import { fileURLToPath } from "node:url";
16
38
  import { dirname, join } from "node:path";
17
39
 
18
40
  const REPO = "artokun/comfyui-mcp-runpod";
41
+ const TEMPLATE_ID = process.env.RUNPOD_TEMPLATE_ID || "bnqtkvcer3";
19
42
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
20
43
  const CTX = join(ROOT, "docker", "runpod");
44
+ const DONOR = process.env.RUNPOD_DONOR || `${REPO}:latest`;
21
45
 
22
46
  const run = (cmd, args, opts = {}) =>
23
47
  execFileSync(cmd, args, { stdio: "inherit", ...opts });
24
48
  const capture = (cmd) => execSync(cmd, { encoding: "utf8" }).trim();
25
49
 
26
- // current panel main SHA (no local clone needed)
50
+ // ---- 1. VERSION -------------------------------------------------------------
51
+ const arg = process.argv[2] || "minor";
52
+ if (!/^\d+\.\d+$/.test(arg) && !["major", "minor", "patch"].includes(arg)) {
53
+ console.error(`usage: npm run runpod:release [-- major|minor|<X.Y>] (got "${arg}")`);
54
+ process.exit(1);
55
+ }
56
+
57
+ const hubTags = await (async () => {
58
+ const res = await fetch(
59
+ `https://hub.docker.com/v2/repositories/${REPO}/tags?page_size=100`,
60
+ );
61
+ if (!res.ok) throw new Error(`Docker Hub tag list failed: ${res.status}`);
62
+ return (await res.json()).results.map((t) => t.name);
63
+ })();
64
+ const versions = hubTags
65
+ .map((t) => /^(\d+)\.(\d+)$/.exec(t))
66
+ .filter(Boolean)
67
+ .map((m) => [Number(m[1]), Number(m[2])])
68
+ .sort((a, b) => a[0] - b[0] || a[1] - b[1]);
69
+ const cur = versions.at(-1) ?? [1, 0];
70
+
71
+ let tag;
72
+ if (/^\d+\.\d+$/.test(arg)) {
73
+ if (hubTags.includes(arg)) {
74
+ console.error(`refusing: ${REPO}:${arg} already exists on Docker Hub`);
75
+ process.exit(1);
76
+ }
77
+ tag = arg;
78
+ } else if (arg === "major") {
79
+ tag = `${cur[0] + 1}.0`;
80
+ } else {
81
+ tag = `${cur[0]}.${cur[1] + 1}`; // minor (and patch alias — two-part scheme)
82
+ }
83
+ console.log(`current hub release: ${cur.join(".")} -> releasing: ${tag}`);
84
+
85
+ // current panel main SHA (cache-busts the panel clone layer; no local clone needed)
27
86
  const panelSha = capture(
28
87
  "git ls-remote https://github.com/artokun/comfyui-mcp-panel.git refs/heads/main",
29
88
  ).split(/\s+/)[0];
@@ -32,27 +91,58 @@ if (!/^[0-9a-f]{40}$/.test(panelSha)) {
32
91
  process.exit(1);
33
92
  }
34
93
 
35
- // tag: explicit arg, or auto-increment the highest local 1.x
36
- let tag = process.argv[2];
37
- if (!tag) {
38
- const tags = capture(`docker images ${REPO} --format "{{.Tag}}"`)
39
- .split(/\r?\n/)
40
- .map((t) => /^(\d+)\.(\d+)$/.exec(t))
41
- .filter(Boolean)
42
- .map((m) => [Number(m[1]), Number(m[2])])
43
- .sort((a, b) => a[0] - b[0] || a[1] - b[1]);
44
- const last = tags.at(-1) ?? [1, 0];
45
- tag = `${last[0]}.${last[1] + 1}`;
46
- }
47
-
48
- console.log(`building ${REPO}:${tag} (+latest) — panel @ ${panelSha.slice(0, 8)}, fat (extras + spotcheck)`);
94
+ // ---- 2. BUILD (fat: extras + spotcheck; donor = previous release) -----------
95
+ console.log(`building ${REPO}:build panel @ ${panelSha.slice(0, 8)}, extras donor: ${DONOR}`);
49
96
  run("docker", [
50
97
  "build",
51
98
  "--build-arg", `PANEL_CACHEBUST=${panelSha}`,
52
- "-t", `${REPO}:${tag}`,
53
- "-t", `${REPO}:latest`,
99
+ "--build-arg", `RUNPOD_SRC_IMAGE=${DONOR}`,
100
+ "--build-arg", "INCLUDE_RUNPOD_EXTRAS=1",
101
+ "--build-arg", "BAKE_SPOTCHECK_MODEL=1",
102
+ "-t", `${REPO}:build`,
54
103
  CTX,
55
104
  ]);
56
- run("docker", ["push", `${REPO}:${tag}`]);
57
- run("docker", ["push", `${REPO}:latest`]);
58
- console.log(`\nreleased ${REPO}:${tag} and :latest — new RunPod pods pick it up on creation.`);
105
+
106
+ // ---- 3+4. GATE, PUBLISH, VERIFY (deploy-dockerhub.sh refuses unverified pushes)
107
+ run("bash", [join(CTX, "deploy-dockerhub.sh"), tag, `${REPO}:build`]);
108
+
109
+ // ---- 5. TEMPLATE — pin the RunPod template to the immutable version tag -----
110
+ if (process.env.SKIP_TEMPLATE === "1") {
111
+ console.log(`SKIP_TEMPLATE=1 — remember to point the template at ${REPO}:${tag}`);
112
+ process.exit(0);
113
+ }
114
+ const image = `${REPO}:${tag}`;
115
+ // saveTemplate needs the full field set, so fetch-and-echo everything except
116
+ // imageName. (runpodctl deliberately not used here — see the header note.)
117
+ let key = process.env.RUNPOD_API_KEY;
118
+ if (!key && existsSync(join(ROOT, ".env"))) {
119
+ key = /^RUNPOD_API_KEY=(.+)$/m.exec(readFileSync(join(ROOT, ".env"), "utf8"))?.[1]?.trim();
120
+ }
121
+ if (!key) {
122
+ console.error(`released ${image}, but RUNPOD_API_KEY is not set — update the template manually:`);
123
+ console.error(` console.runpod.io -> Templates -> ${TEMPLATE_ID} -> Container Image -> ${image}`);
124
+ process.exit(1);
125
+ }
126
+ const gql = async (query, variables) => {
127
+ const res = await fetch(`https://api.runpod.io/graphql?api_key=${key}`, {
128
+ method: "POST",
129
+ headers: { "content-type": "application/json" },
130
+ body: JSON.stringify({ query, variables }),
131
+ });
132
+ const body = await res.json();
133
+ if (body.errors) throw new Error(JSON.stringify(body.errors));
134
+ return body.data;
135
+ };
136
+ const { podTemplates } = await gql(
137
+ "query { podTemplates { id name imageName containerDiskInGb volumeInGb volumeMountPath ports dockerArgs env { key value } readme } }",
138
+ );
139
+ const t = podTemplates.find((x) => x.id === TEMPLATE_ID);
140
+ if (!t) throw new Error(`template ${TEMPLATE_ID} not found on this account`);
141
+ await gql(
142
+ "mutation Save($input: SaveTemplateInput!) { saveTemplate(input: $input) { id imageName } }",
143
+ { input: { ...t, imageName: image } },
144
+ );
145
+ console.log(`template ${TEMPLATE_ID} -> ${image} (via GraphQL)`);
146
+
147
+ console.log(`\nreleased ${image} (+ :latest), registry-verified, template pinned.`);
148
+ console.log("New pods pull the pinned version tag — stale host caches of :latest can no longer bite.");