Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions bootstrap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## 0.1.0

- WP-22: `assembleManifest` writes `SELF_CID_PLACEHOLDER` on the current
`versions[]` entry and stamps the previous tail with the on-chain CID
(issue #30).
- WP-22: `createProductionDeps` uses `gatewaysFromEnv` so `ENSPACK_IPFS_GATEWAYS`
is prepended (then Pinata, then SPEC defaults).
- WP-22: `planBootstrap` records per-entry `failed` on resolve/publish throws
and continues; totals count only `ok` rows. Already-published models plan
`1.1.0` via `bumpMinor` / previous-manifest (same as `run`).
- WP-22: Hugging Bay lock HTTP 409/404 is treated as no lock; files step logs
"no Hugging Bay lock; cross-check skipped" and continues.
- WP-21: `ENSPACK_BOOTSTRAP_STATE` overrides the `state.json` path so the one-box
seed volume can persist resume state. `bootstrap/Dockerfile` is the infra
compose `bootstrap` profile image (aria2c + `pnpm deploy --filter @enspack/bootstrap`).
Expand Down
6 changes: 3 additions & 3 deletions bootstrap/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { LICENSE_ALLOWLIST, MIRROR_NAMESPACE } from "@enspack/core";
import { LICENSE_ALLOWLIST, MIRROR_NAMESPACE, SELF_CID_PLACEHOLDER } from "@enspack/core";

/** SPEC §3: MAGNET_RE-valid 40-hex zeros used only to drive the downloader. */
export const PLACEHOLDER_INFOHASH = "0".repeat(40);

/** SPEC §4: placeholder magnet matching `MAGNET_RE`. */
export const PLACEHOLDER_MAGNET = `magnet:?xt=urn:btih:${PLACEHOLDER_INFOHASH}`;

/** Schema-valid CID used as a versions[] placeholder before the real pin. */
export const PLACEHOLDER_CID = "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
/** Schema-valid CID used as a torrent-cid / draft placeholder (same bytes as issue #30). */
export const PLACEHOLDER_CID = SELF_CID_PLACEHOLDER;

export const DEFAULT_PUBLISHER = MIRROR_NAMESPACE;

Expand Down
2 changes: 1 addition & 1 deletion bootstrap/src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface BootstrapHf {

export interface BootstrapHb {
resolve(repo: string): Promise<HbArtifact | null>;
lock(artifactId: string): Promise<HbLock>;
lock(artifactId: string): Promise<HbLock | null>;
submitFallback(
artifactId: string,
input: HbFallbackInput & { sourceUrl?: string; filePath?: string },
Expand Down
25 changes: 19 additions & 6 deletions bootstrap/src/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { MIRROR_NAMESPACE, type Manifest, type ManifestFile, SPEC_STRING } from "@enspack/core";
import {
MIRROR_NAMESPACE,
type Manifest,
type ManifestFile,
SELF_CID_PLACEHOLDER,
SPEC_STRING,
} from "@enspack/core";
import { magnetFor } from "@enspack/torrent";
import { PLACEHOLDER_CID, PLACEHOLDER_INFOHASH, PLACEHOLDER_MAGNET } from "./constants.js";
import { PLACEHOLDER_INFOHASH, PLACEHOLDER_MAGNET } from "./constants.js";
import { canonicalNameFor, modelNameFor, versionNameFor } from "./names.js";

function hasLicenseFile(files: ManifestFile[]): boolean {
Expand Down Expand Up @@ -39,11 +45,18 @@ export function assembleManifest(input: AssembleInput): Manifest {
const thisVersion = {
version: input.version,
name,
cid: PLACEHOLDER_CID,
cid: SELF_CID_PLACEHOLDER,
createdAt: input.createdAt,
};
const versions =
input.previousVersions !== undefined ? [...input.previousVersions, thisVersion] : [thisVersion];
const prior =
input.previousVersions === undefined
? []
: input.previousVersions.map((v, i, arr) =>
i === arr.length - 1 && input.previous !== undefined
? { ...v, cid: input.previous }
: { ...v },
);
const versions = [...prior, thisVersion];

const distribution: Manifest["distribution"] = {
infohash: input.infohash,
Expand Down Expand Up @@ -107,7 +120,7 @@ export function draftManifestForDownload(
},
files: files as Manifest["files"],
totalSize,
versions: [{ version: "0.0.0", name, cid: PLACEHOLDER_CID, createdAt }],
versions: [{ version: "0.0.0", name, cid: SELF_CID_PLACEHOLDER, createdAt }],
};
}

Expand Down
85 changes: 55 additions & 30 deletions bootstrap/src/plan.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
type Manifest,
type PublishCall,
type PublishResult,
canonicalJson,
formatPublishPlan,
isEnspackError,
manifestCid,
validateManifest,
} from "@enspack/core";
Expand Down Expand Up @@ -105,14 +107,22 @@ export async function planBootstrap(

const modelName = modelNameFor(gate.data.org, gate.data.repoName ?? "");
let version = "1.0.0";
let previousVersions: Manifest["versions"] | undefined;
let previous: string | undefined;
try {
const resolved = await deps.resolver.resolve(modelName, { chain });
if (resolved.manifest !== null) {
if (resolved.manifest !== null && resolved.cid !== null) {
previousVersions = resolved.manifest.versions;
previous = resolved.cid;
const last = resolved.manifest.versions[resolved.manifest.versions.length - 1];
version = bumpMinor(last?.version ?? resolved.manifest.version);
}
} catch {
/* first publish */
} catch (err) {
if (!isEnspackError(err) || err.code !== "RESOLVE") {
const message = err instanceof Error ? err.message : String(err);
out.push({ repo: entry.repo, status: "failed", snapshotSize, license, reason: message });
continue;
}
}

const webseeds = [deps.hfWebseed(entry.repo, revision)];
Expand Down Expand Up @@ -140,34 +150,49 @@ export async function planBootstrap(
version,
createdAt: deps.now(),
};
const manifest = assembleManifest(assemble);
validateManifest(manifest);
const cid = await manifestCid(canonicalJson(manifest));
const published = await deps.publisher.publish({
manifest,
manifestCid: cid,
chain,
dryRun: true,
});
const calls = allPublishCalls(published);
const gas = sumGas(calls);
totalBytes += snapshotSize;
totalGas += gas;
const row: PlanEntryJson = {
repo: entry.repo,
status: "ok",
snapshotSize,
fileCount: files.length,
license,
gasEstimate: gas.toString(),
version,
name: manifest.name,
plan: formatPublishPlan(calls),
};
if (filesResult.data.hbCrossCheck !== undefined) {
row.hbCrossCheck = filesResult.data.hbCrossCheck;
if (previousVersions !== undefined) assemble.previousVersions = previousVersions;
if (previous !== undefined) assemble.previous = previous;

try {
const manifest = assembleManifest(assemble);
validateManifest(manifest);
const cid = await manifestCid(canonicalJson(manifest));
const published = await deps.publisher.publish({
manifest,
manifestCid: cid,
chain,
dryRun: true,
});
const calls = allPublishCalls(published);
const gas = sumGas(calls);
totalBytes += snapshotSize;
totalGas += gas;
const row: PlanEntryJson = {
repo: entry.repo,
status: "ok",
snapshotSize,
fileCount: files.length,
license,
gasEstimate: gas.toString(),
version,
name: manifest.name,
plan: formatPublishPlan(calls),
};
if (filesResult.data.hbCrossCheck !== undefined) {
row.hbCrossCheck = filesResult.data.hbCrossCheck;
}
out.push(row);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
out.push({
repo: entry.repo,
status: "failed",
snapshotSize,
license,
reason: message,
version,
});
}
out.push(row);
}

return { entries: out, totals: { bytes: totalBytes, gasEstimate: totalGas.toString() } };
Expand Down
3 changes: 2 additions & 1 deletion bootstrap/src/production.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createManifestStore,
createPublisher,
createResolver,
gatewaysFromEnv,
kuboPinner,
publicClientFor,
seedNodePinner,
Expand Down Expand Up @@ -90,7 +91,7 @@ export function createProductionDeps(opts: ProductionOpts): BootstrapDeps {
pinner = seedNodePinner({ baseUrl: seedUrl });
}

const store = createManifestStore({ pinner });
const store = createManifestStore({ gateways: gatewaysFromEnv(env), pinner });
const resolver = createResolver({
client,
store,
Expand Down
11 changes: 10 additions & 1 deletion bootstrap/src/steps/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { EntrySnapshot, ModelEntry, StepOutcome } from "../types.js";
export async function stepFiles(
entry: ModelEntry,
revision: string,
deps: Pick<BootstrapDeps, "hf" | "hb">,
deps: Pick<BootstrapDeps, "hf" | "hb" | "log">,
): Promise<StepOutcome> {
const files = await deps.hf.buildFiles(entry.repo, revision);
const artifact = await deps.hb.resolve(entry.repo);
Expand All @@ -24,6 +24,15 @@ export async function stepFiles(
}
try {
const lock = await deps.hb.lock(artifact.id);
if (lock === null) {
deps.log.info("no Hugging Bay lock; cross-check skipped");
const data: Partial<EntrySnapshot> = {
files,
fileCount: files.length,
hbCrossCheck: "skipped",
};
return { outcome: "ok", data };
}
crossCheck(files, lock);
} catch (err) {
const message = err instanceof EnspackError ? err.message : String(err);
Expand Down
34 changes: 33 additions & 1 deletion bootstrap/test/cross-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@ import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import type { BootstrapDeps } from "../src/deps.js";
import { runBootstrap } from "../src/run.js";
import { disagreeingHb, fakeHf, loadModels, resolverThatThrows, silentLog } from "./helpers.js";
import { stepFiles } from "../src/steps/files.js";
import {
TINY_REPO,
TINY_SHA,
disagreeingHb,
fakeHf,
loadModels,
noLockHb,
resolverThatThrows,
silentLog,
} from "./helpers.js";

describe("cross-check (BOOTSTRAP.md §2 rule 3)", () => {
const dirs: string[] = [];
Expand Down Expand Up @@ -80,4 +90,26 @@ describe("cross-check (BOOTSTRAP.md §2 rule 3)", () => {
expect(publishCalls).toEqual([]);
expect(records[0]?.completedSteps).toEqual(["gate"]);
});

it("treats a missing HB lock as ok and skips the cross-check", async () => {
const logs: string[] = [];
const result = await stepFiles(
{ repo: TINY_REPO, tier: 1, expected_license: "apache-2.0", revision: "abcdef12" },
TINY_SHA,
{
hf: fakeHf(),
hb: noLockHb(),
log: {
info(message) {
logs.push(message);
},
warn() {},
},
},
);
expect(result.outcome).toBe("ok");
if (result.outcome !== "ok") return;
expect(result.data.hbCrossCheck).toBe("skipped");
expect(logs).toContain("no Hugging Bay lock; cross-check skipped");
});
});
19 changes: 19 additions & 0 deletions bootstrap/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ export function emptyHb(): BootstrapDeps["hb"] {
};
}

export function noLockHb(): BootstrapDeps["hb"] {
return {
async resolve() {
return {
id: "hf-model-enspack-tiny-model",
repo: TINY_REPO,
digest: "aa".repeat(32),
raw: {},
};
},
async lock() {
return null;
},
async submitFallback() {
return {};
},
};
}

export function disagreeingHb(): BootstrapDeps["hb"] {
return {
async resolve() {
Expand Down
62 changes: 62 additions & 0 deletions bootstrap/test/manifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { SELF_CID_PLACEHOLDER } from "@enspack/core";
import { describe, expect, it } from "vitest";
import { assembleManifest } from "../src/manifest.js";
import { tinyModelFiles } from "./helpers.js";

const REAL_CID = `bafkrei${"b".repeat(52)}`;
const PREV_CID = `bafkrei${"c".repeat(52)}`;
const WEBSEED =
"https://huggingface.co/enspack/tiny-model/resolve/abcdef1200000000000000000000000000000000/";

describe("assembleManifest versions[] cid (issue #30)", () => {
it("uses SELF_CID_PLACEHOLDER for the current entry", () => {
const manifest = assembleManifest({
org: "enspack",
repoName: "tiny-model",
repo: "enspack/tiny-model",
revision: "abcdef1200000000000000000000000000000000",
license: "apache-2.0",
displayName: "tiny-model",
files: tinyModelFiles(),
infohash: "0".repeat(40),
magnet: `magnet:?xt=urn:btih:${"0".repeat(40)}`,
torrentCid: SELF_CID_PLACEHOLDER,
webseeds: [WEBSEED],
version: "1.0.0",
createdAt: "2026-09-14T00:00:00Z",
});
expect(manifest.versions).toHaveLength(1);
expect(manifest.versions[0]?.cid).toBe(SELF_CID_PLACEHOLDER);
});

it("stamps the previous tail with previous CID and placeholders the new last entry", () => {
const manifest = assembleManifest({
org: "enspack",
repoName: "tiny-model",
repo: "enspack/tiny-model",
revision: "abcdef1200000000000000000000000000000000",
license: "apache-2.0",
displayName: "tiny-model",
files: tinyModelFiles(),
infohash: "0".repeat(40),
magnet: `magnet:?xt=urn:btih:${"0".repeat(40)}`,
torrentCid: SELF_CID_PLACEHOLDER,
webseeds: [WEBSEED],
version: "1.1.0",
createdAt: "2026-09-15T00:00:00Z",
previous: PREV_CID,
previousVersions: [
{
version: "1.0.0",
name: "v1-0-0.enspack--tiny-model.mirrors.enspack.eth",
cid: REAL_CID,
createdAt: "2026-09-14T00:00:00Z",
},
],
});
expect(manifest.versions).toHaveLength(2);
expect(manifest.versions[0]?.cid).toBe(PREV_CID);
expect(manifest.versions[1]?.cid).toBe(SELF_CID_PLACEHOLDER);
expect(manifest.previous).toBe(PREV_CID);
});
});
Loading
Loading