@valbuild/cli 0.137.0 → 0.138.1
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/CHANGELOG.md +53 -0
- package/cli/dist/valbuild-cli-cli.cjs.dev.js +578 -44
- package/cli/dist/valbuild-cli-cli.cjs.prod.js +578 -44
- package/cli/dist/valbuild-cli-cli.esm.js +578 -45
- package/package.json +29 -7
- package/src/ciReport.ts +39 -0
- package/src/cli.ts +41 -10
- package/src/publish/__fixtures__/buildPublishProbe.ts +238 -0
- package/src/publish/artifacts.ts +4 -4
- package/src/publish/buildArtifacts.ts +365 -0
- package/src/publish/buildPublish.test.ts +118 -0
- package/src/publish/client.ts +8 -0
- package/src/publish/fakeContent.ts +60 -5
- package/src/publish/reportCiRun.test.ts +110 -0
- package/src/publish/reportCiRun.ts +105 -0
- package/src/publish/runPublish.ts +105 -23
|
@@ -6,13 +6,14 @@ import { glob } from 'fast-glob';
|
|
|
6
6
|
import { Internal, DEFAULT_VAL_REMOTE_HOST, DEFAULT_CONTENT_HOST } from '@valbuild/core';
|
|
7
7
|
import { createPrettierFormatter, createService, createValModuleFileInspector, fixHandlers, createFixPatch, getModulePathRange, findJsonEntryFilePath, createJsonEntryPathMap, createModulePathMap, findAndEvalValConfigFile, getSettings, uploadRemoteFile, createDefaultValFSHost, startValLogin, awaitValLoginConfirmation, persistPersonalAccessToken, ValLoginError, loadValModules, safeReadGit, ValOpsHttp, ValOpsFS, getPersonalAccessTokenPath, parsePersonalAccessTokenFile, formatPatchSourceError } from '@valbuild/server';
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
|
-
import { resolveSchemaSourceFixes, parseDeclare, parseArtifacts, parseVerify, parsePromote, parseStatus, parsePublishToken, PublishProtocolError, parseProblems } from '@valbuild/shared/internal';
|
|
9
|
+
import { resolveSchemaSourceFixes, parseBuildTarget, parseDeclare, parseArtifacts, parseVerify, parsePromote, parseStatus, parsePublishToken, PublishProtocolError, parseProblems } from '@valbuild/shared/internal';
|
|
10
10
|
import fs$1 from 'fs';
|
|
11
11
|
import ts from 'typescript';
|
|
12
12
|
import JSZip from 'jszip';
|
|
13
13
|
import readline from 'readline';
|
|
14
14
|
import crypto from 'crypto';
|
|
15
15
|
import zlib from 'zlib';
|
|
16
|
+
import os from 'os';
|
|
16
17
|
|
|
17
18
|
function error(message) {
|
|
18
19
|
console.error(chalk.red("❌Error: ") + message);
|
|
@@ -2095,10 +2096,10 @@ function confirm(question) {
|
|
|
2095
2096
|
* imports it at, so a normalised name produces a bundle whose imports resolve
|
|
2096
2097
|
* to nothing, at runtime, in the isolate.
|
|
2097
2098
|
*
|
|
2098
|
-
* **This is the seam with the build.**
|
|
2099
|
-
*
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2099
|
+
* **This is the seam with the build.** Whatever built the project writes this
|
|
2100
|
+
* directory, and this reads it: `buildArtifacts` when `val publish` builds the
|
|
2101
|
+
* checkout itself, or anything else that lays out the same keys, handed over
|
|
2102
|
+
* with `--artifacts`. One path for bytes to leave by, whoever built them.
|
|
2102
2103
|
*/
|
|
2103
2104
|
async function collectArtifacts(dir) {
|
|
2104
2105
|
const artifacts = [];
|
|
@@ -2250,6 +2251,308 @@ function resolveArtifactsDir(options) {
|
|
|
2250
2251
|
};
|
|
2251
2252
|
}
|
|
2252
2253
|
|
|
2254
|
+
/**
|
|
2255
|
+
* `val publish`'s build: the site, built from the checkout, as the artifacts
|
|
2256
|
+
* the publish API takes (valbuild/home, docs/app-mode.md, "Briefing:
|
|
2257
|
+
* val-publish" and "Flow B").
|
|
2258
|
+
*
|
|
2259
|
+
* The same builder the Studio runs in a tab -- `@valbuild/tanstack-build` --
|
|
2260
|
+
* plus the half a tab cannot run: the project's dependency layer, built from
|
|
2261
|
+
* its own `node_modules` (`@valbuild/tanstack-build/node`). So a CI checkout
|
|
2262
|
+
* and a Studio build of the same content produce the same site, and the layer
|
|
2263
|
+
* is the one thing only this side can make.
|
|
2264
|
+
*
|
|
2265
|
+
* Against content's build target, never the loader's: content is the only
|
|
2266
|
+
* address a publisher has, and it answers what the platform expects.
|
|
2267
|
+
*
|
|
2268
|
+
* Written to a directory, and then published by the upload path `val publish
|
|
2269
|
+
* --artifacts` has always had: one path for bytes to leave by, whoever built
|
|
2270
|
+
* them.
|
|
2271
|
+
*/
|
|
2272
|
+
|
|
2273
|
+
/** What `val publish` hands on to the upload. */
|
|
2274
|
+
|
|
2275
|
+
/**
|
|
2276
|
+
* The builder, loaded only when a build is asked for: it brings rolldown's
|
|
2277
|
+
* native binary with it, and `val validate` has no use for one.
|
|
2278
|
+
*/
|
|
2279
|
+
async function loadBuilder() {
|
|
2280
|
+
try {
|
|
2281
|
+
const [build, node] = await Promise.all([import('@valbuild/tanstack-build'), import('@valbuild/tanstack-build/node')]);
|
|
2282
|
+
return {
|
|
2283
|
+
status: "ok",
|
|
2284
|
+
build,
|
|
2285
|
+
node
|
|
2286
|
+
};
|
|
2287
|
+
} catch (e) {
|
|
2288
|
+
return {
|
|
2289
|
+
status: "error",
|
|
2290
|
+
message: "`val publish` builds the site with @valbuild/tanstack-build and rolldown, and " + `they could not be loaded: ${e instanceof Error ? e.message : String(e)}\n\n` + "Install them in the project:\n\n" + " npm install --save-dev @valbuild/tanstack-build rolldown @tanstack/router-generator @tanstack/router-plugin\n\n" + "or publish a build made elsewhere with --artifacts <dir>."
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
/**
|
|
2296
|
+
* Never the project's own files, at any depth: installed packages. Dot
|
|
2297
|
+
* directories (`.git`, `.tanstack`, `.val`, `.output`) are tooling too.
|
|
2298
|
+
*/
|
|
2299
|
+
const IGNORED_DIRS = new Set(["node_modules"]);
|
|
2300
|
+
|
|
2301
|
+
/**
|
|
2302
|
+
* Build output, at the project's root only: `src/routes/build/` is a route
|
|
2303
|
+
* called "build", not a previous build.
|
|
2304
|
+
*/
|
|
2305
|
+
const ROOT_OUTPUT_DIRS = new Set(["dist", "build"]);
|
|
2306
|
+
|
|
2307
|
+
/** Lockfiles are JSON, and nothing imports them. */
|
|
2308
|
+
const ROOT_LOCKFILES = new Set(["package-lock.json"]);
|
|
2309
|
+
const CODE_FILE = /\.(tsx?|jsx?|mjs|cjs|css)$/;
|
|
2310
|
+
|
|
2311
|
+
/**
|
|
2312
|
+
* JSON is the project's too: `tsconfig.json` and `tsr.config.json` are read
|
|
2313
|
+
* by the build, a `.val.json` holds a `.jsonValues()` entry no import points
|
|
2314
|
+
* at, and a module may import one -- the starter's `val.server.ts` imports
|
|
2315
|
+
* `.prettierrc.json`, which is also why a dot FILE is read.
|
|
2316
|
+
*/
|
|
2317
|
+
const JSON_FILE = /\.json$/;
|
|
2318
|
+
|
|
2319
|
+
/**
|
|
2320
|
+
* The project's source as it is STORED, which is what the Studio builds from:
|
|
2321
|
+
* every file this build read, less the stylesheets.
|
|
2322
|
+
*
|
|
2323
|
+
* This build compiles them, because it can run what a stylesheet asks for -- a
|
|
2324
|
+
* Tailwind `@plugin` is a module resolved and executed from `node_modules`. A
|
|
2325
|
+
* build in the browser cannot, and refuses the stylesheet rather than ship a
|
|
2326
|
+
* page that is quietly unstyled; the starter's `src/styles.css` has
|
|
2327
|
+
* `@plugin "@tailwindcss/typography"`. So a stored stylesheet made every
|
|
2328
|
+
* Studio publish after the first one fail. Without it, the Studio's build has
|
|
2329
|
+
* no CSS of its own and ships the live site's (`liveStylesheet` in the
|
|
2330
|
+
* Studio), which is right: a Studio save changes content and uploaded files,
|
|
2331
|
+
* never a stylesheet. It is also what the platform's own publish has always
|
|
2332
|
+
* stored.
|
|
2333
|
+
*/
|
|
2334
|
+
function withoutStylesheets(sources) {
|
|
2335
|
+
return Object.fromEntries(Object.entries(sources).filter(([key]) => !key.endsWith(".css")));
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
/**
|
|
2339
|
+
* The project's own files: its source as text, and the binaries it imports
|
|
2340
|
+
* (an image, a font) base64, as `BuildInput.assets` takes them. `public/` is
|
|
2341
|
+
* neither: it is served as is, and read by `readPublicFiles`.
|
|
2342
|
+
*/
|
|
2343
|
+
function readProjectFiles(root, isAsset) {
|
|
2344
|
+
const sources = {};
|
|
2345
|
+
const assets = {};
|
|
2346
|
+
const walk = dir => {
|
|
2347
|
+
let entries;
|
|
2348
|
+
try {
|
|
2349
|
+
entries = fs$1.readdirSync(dir, {
|
|
2350
|
+
withFileTypes: true
|
|
2351
|
+
});
|
|
2352
|
+
} catch {
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
for (const entry of entries) {
|
|
2356
|
+
const full = path.join(dir, entry.name);
|
|
2357
|
+
if (entry.isDirectory()) {
|
|
2358
|
+
if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
|
|
2359
|
+
if (dir === root && ROOT_OUTPUT_DIRS.has(entry.name)) continue;
|
|
2360
|
+
if (dir === root && entry.name === "public") continue;
|
|
2361
|
+
walk(full);
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
const key = path.relative(root, full).split(path.sep).join("/");
|
|
2365
|
+
if (CODE_FILE.test(entry.name)) {
|
|
2366
|
+
sources[key] = fs$1.readFileSync(full, "utf8");
|
|
2367
|
+
} else if (JSON_FILE.test(entry.name)) {
|
|
2368
|
+
if (!ROOT_LOCKFILES.has(key)) {
|
|
2369
|
+
sources[key] = fs$1.readFileSync(full, "utf8");
|
|
2370
|
+
}
|
|
2371
|
+
} else if (isAsset(key)) {
|
|
2372
|
+
assets[key] = fs$1.readFileSync(full).toString("base64");
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
};
|
|
2376
|
+
walk(root);
|
|
2377
|
+
return {
|
|
2378
|
+
sources,
|
|
2379
|
+
assets
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/** Files under `public/`, base64, keyed `public/<path>` as the build takes them. */
|
|
2384
|
+
function readPublicFiles(root) {
|
|
2385
|
+
const out = {};
|
|
2386
|
+
const base = path.join(root, "public");
|
|
2387
|
+
const walk = dir => {
|
|
2388
|
+
let entries;
|
|
2389
|
+
try {
|
|
2390
|
+
entries = fs$1.readdirSync(dir, {
|
|
2391
|
+
withFileTypes: true
|
|
2392
|
+
});
|
|
2393
|
+
} catch {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
for (const entry of entries) {
|
|
2397
|
+
const full = path.join(dir, entry.name);
|
|
2398
|
+
if (entry.isDirectory()) walk(full);else {
|
|
2399
|
+
const key = path.relative(root, full).split(path.sep).join("/");
|
|
2400
|
+
out[key] = fs$1.readFileSync(full).toString("base64");
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
};
|
|
2404
|
+
walk(base);
|
|
2405
|
+
return out;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/** The project's build-time variables: `PUBLIC_*` reach the browser, by the builder's rule. */
|
|
2409
|
+
function publicEnv(env) {
|
|
2410
|
+
const out = {};
|
|
2411
|
+
for (const [key, value] of Object.entries(env)) {
|
|
2412
|
+
if (key.startsWith("PUBLIC_") && value !== undefined) out[key] = value;
|
|
2413
|
+
}
|
|
2414
|
+
return out;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
/**
|
|
2418
|
+
* The target, with this project's layer as built here rather than as content
|
|
2419
|
+
* last stored it: a build has to link against the layer it ships with.
|
|
2420
|
+
*/
|
|
2421
|
+
function targetWithLayer(target, layer) {
|
|
2422
|
+
return {
|
|
2423
|
+
...target,
|
|
2424
|
+
project: {
|
|
2425
|
+
...target.project,
|
|
2426
|
+
rev: (layer === null || layer === void 0 ? void 0 : layer.rev) ?? null,
|
|
2427
|
+
modules: (layer === null || layer === void 0 ? void 0 : layer.modules) ?? {},
|
|
2428
|
+
css: (layer === null || layer === void 0 ? void 0 : layer.css) ?? {},
|
|
2429
|
+
workerOnly: (layer === null || layer === void 0 ? void 0 : layer.workerOnly) ?? []
|
|
2430
|
+
}
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
function writeArtifact(dir, key, body) {
|
|
2434
|
+
const file = path.join(dir, ...key.split("/"));
|
|
2435
|
+
fs$1.mkdirSync(path.dirname(file), {
|
|
2436
|
+
recursive: true
|
|
2437
|
+
});
|
|
2438
|
+
fs$1.writeFileSync(file, body);
|
|
2439
|
+
}
|
|
2440
|
+
async function buildArtifacts(options) {
|
|
2441
|
+
const log = options.log ?? (() => {});
|
|
2442
|
+
const loaded = await loadBuilder();
|
|
2443
|
+
if (loaded.status === "error") return loaded;
|
|
2444
|
+
const {
|
|
2445
|
+
build: builder,
|
|
2446
|
+
node
|
|
2447
|
+
} = loaded;
|
|
2448
|
+
const {
|
|
2449
|
+
root,
|
|
2450
|
+
target
|
|
2451
|
+
} = options;
|
|
2452
|
+
const warnings = [];
|
|
2453
|
+
try {
|
|
2454
|
+
/*
|
|
2455
|
+
* Wired for the platform at publish time, as the Studio's builds are: the
|
|
2456
|
+
* wired `val.server.ts` reads its files from a module the BUILD emits, so
|
|
2457
|
+
* no checkout can carry it. `rebakeGit` would be the tab's spelling of the
|
|
2458
|
+
* same thing for a record that is already wired.
|
|
2459
|
+
*/
|
|
2460
|
+
const {
|
|
2461
|
+
sources: onDisk,
|
|
2462
|
+
assets
|
|
2463
|
+
} = readProjectFiles(root, builder.isAsset);
|
|
2464
|
+
const wiring = builder.isValProject(onDisk) && !builder.isWired(onDisk) ? builder.wireUp(onDisk, {
|
|
2465
|
+
project: options.project,
|
|
2466
|
+
loader: options.contentHost,
|
|
2467
|
+
...(options.git ? {
|
|
2468
|
+
git: options.git
|
|
2469
|
+
} : {})
|
|
2470
|
+
}) : null;
|
|
2471
|
+
// Less the one about the layer, which is the thing this goes on to build.
|
|
2472
|
+
warnings.push(...((wiring === null || wiring === void 0 ? void 0 : wiring.warnings) ?? []).filter(warning => !warning.includes("dependency layer built for it")));
|
|
2473
|
+
const sources = (wiring === null || wiring === void 0 ? void 0 : wiring.files) ?? onDisk;
|
|
2474
|
+
const projectSource = withoutStylesheets(sources);
|
|
2475
|
+
|
|
2476
|
+
// The dependency layer: from what the project imports, of what it declares.
|
|
2477
|
+
log("Building the dependency layer");
|
|
2478
|
+
const manifest = (() => {
|
|
2479
|
+
try {
|
|
2480
|
+
return JSON.parse(sources["package.json"] ?? "{}");
|
|
2481
|
+
} catch {
|
|
2482
|
+
return {};
|
|
2483
|
+
}
|
|
2484
|
+
})();
|
|
2485
|
+
const deps = node.layerDepsOf(manifest, sources);
|
|
2486
|
+
const css = await node.collectDependencyCss(root, node.cssSpecifiersOf(sources));
|
|
2487
|
+
const layer = await node.buildVendorLayer(root, deps, css, {
|
|
2488
|
+
base: target.base,
|
|
2489
|
+
// A project on the platform layers its own copies; only the platform's
|
|
2490
|
+
// own checkout carries pinned ones.
|
|
2491
|
+
resolvePlatformModule: () => null,
|
|
2492
|
+
// No built base layer here: the export audit is skipped, not failed.
|
|
2493
|
+
baseLayerDir: null
|
|
2494
|
+
});
|
|
2495
|
+
log("Building the site");
|
|
2496
|
+
const fileBased = await node.isFileBased(root);
|
|
2497
|
+
const files = fileBased ? await node.generateRouteTree(root, sources) : sources;
|
|
2498
|
+
/*
|
|
2499
|
+
* The project's copy of the splitter first, then the one beside this CLI
|
|
2500
|
+
* -- where a package manager puts an optional peer the project installed
|
|
2501
|
+
* but did not hoist. Neither is a build that does not split, which costs
|
|
2502
|
+
* size and not behaviour.
|
|
2503
|
+
*/
|
|
2504
|
+
const splitter = (await node.routeSplitter(root)) ?? (typeof __dirname === "string" ? await node.routeSplitter(__dirname) : null);
|
|
2505
|
+
if (splitter === null) {
|
|
2506
|
+
warnings.push("Routes are not code-split: @tanstack/router-plugin is not installed.");
|
|
2507
|
+
}
|
|
2508
|
+
const built = await builder.buildUserApp({
|
|
2509
|
+
files,
|
|
2510
|
+
target: targetWithLayer(target, layer),
|
|
2511
|
+
env: publicEnv(options.env ?? process.env),
|
|
2512
|
+
publicFiles: readPublicFiles(root),
|
|
2513
|
+
assets,
|
|
2514
|
+
// The project's own files, without the generated tree or the
|
|
2515
|
+
// stylesheets: what the next publish -- the Studio's included -- starts
|
|
2516
|
+
// from. See `withoutStylesheets`.
|
|
2517
|
+
projectSource,
|
|
2518
|
+
loadCssModule: node.cssModuleLoader(root),
|
|
2519
|
+
...(splitter ? {
|
|
2520
|
+
routeSplitter: splitter
|
|
2521
|
+
} : {}),
|
|
2522
|
+
rsc: target.project.rsc
|
|
2523
|
+
});
|
|
2524
|
+
const artifacts = await builder.publishArtifacts(built, {
|
|
2525
|
+
projectSource
|
|
2526
|
+
});
|
|
2527
|
+
const dir = options.outDir ?? fs$1.mkdtempSync(path.join(os.tmpdir(), "val-publish-"));
|
|
2528
|
+
for (const artifact of artifacts) {
|
|
2529
|
+
writeArtifact(dir, artifact.key, artifact.body);
|
|
2530
|
+
}
|
|
2531
|
+
/*
|
|
2532
|
+
* The layer: sent when content does not hold this one, named when it
|
|
2533
|
+
* does. The rev is a digest of the built chunks, so "content has this" is
|
|
2534
|
+
* a fact rather than a hope.
|
|
2535
|
+
*/
|
|
2536
|
+
let layerRev;
|
|
2537
|
+
if (layer === null) layerRev = null;else if (layer.rev === target.project.rev) layerRev = layer.rev;else writeArtifact(dir, "layer", zlib.gzipSync(JSON.stringify(layer)));
|
|
2538
|
+
return {
|
|
2539
|
+
status: "built",
|
|
2540
|
+
dir,
|
|
2541
|
+
buildHash: built.hash,
|
|
2542
|
+
linksOwnCss: built.linksOwnCss,
|
|
2543
|
+
...(layerRev !== undefined ? {
|
|
2544
|
+
layerRev
|
|
2545
|
+
} : {}),
|
|
2546
|
+
warnings
|
|
2547
|
+
};
|
|
2548
|
+
} catch (e) {
|
|
2549
|
+
return {
|
|
2550
|
+
status: "error",
|
|
2551
|
+
message: `The site could not be built: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2253
2556
|
/**
|
|
2254
2557
|
* The one host `val publish` talks to.
|
|
2255
2558
|
*
|
|
@@ -2396,6 +2699,11 @@ function createPublishClient(options) {
|
|
|
2396
2699
|
};
|
|
2397
2700
|
const publishUrl = (publishId, step) => `${host}/v1/publish/${encodeURIComponent(publishId)}${step ? `/${step}` : ""}`;
|
|
2398
2701
|
return {
|
|
2702
|
+
buildTarget: async () => parseBuildTarget(await getJson({
|
|
2703
|
+
url: `${host}/v1/build-target`,
|
|
2704
|
+
headers,
|
|
2705
|
+
fetchImpl
|
|
2706
|
+
})),
|
|
2399
2707
|
declare: async body => parseDeclare(await postJson({
|
|
2400
2708
|
url: `${host}/v1/publish`,
|
|
2401
2709
|
headers,
|
|
@@ -2735,40 +3043,33 @@ const DECLARE_ROUNDS = 3;
|
|
|
2735
3043
|
* that could reach either.
|
|
2736
3044
|
*/
|
|
2737
3045
|
async function runPublish(options) {
|
|
3046
|
+
var _built;
|
|
2738
3047
|
const env = options.env ?? process.env;
|
|
2739
3048
|
const log = options.log ?? (line => console.log(line));
|
|
2740
3049
|
const sleep = options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
2741
3050
|
const root = options.root ? path.resolve(options.root) : process.cwd();
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
3051
|
+
|
|
3052
|
+
/*
|
|
3053
|
+
* Upload-only when there is a build to upload: `--artifacts`, or the
|
|
3054
|
+
* directory it defaults to. Otherwise this builds the checkout -- which is
|
|
3055
|
+
* what a CI runner has, and `.val/` is never committed, so a fresh checkout
|
|
3056
|
+
* always builds.
|
|
3057
|
+
*/
|
|
3058
|
+
const prebuilt = options.artifacts !== undefined || fs$1.existsSync(path.join(root, DEFAULT_ARTIFACTS_DIR));
|
|
3059
|
+
if (!prebuilt && !fs$1.existsSync(path.join(root, "package.json"))) {
|
|
2749
3060
|
return {
|
|
2750
3061
|
status: "error",
|
|
2751
|
-
message:
|
|
3062
|
+
message: `Nothing to publish: ${root} has no package.json to build, and no --artifacts were given.\n\n` + "Run this in the project, or point at a build made elsewhere:\n\n" + " npx val publish --artifacts <directory>"
|
|
2752
3063
|
};
|
|
2753
3064
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
message: `There is nothing in ${dir.dir}. Build the project before publishing it.`
|
|
2762
|
-
};
|
|
3065
|
+
let collected = null;
|
|
3066
|
+
let layerRev = null;
|
|
3067
|
+
if (prebuilt) {
|
|
3068
|
+
const fromDir = await collectPrebuilt(root, options.artifacts, log);
|
|
3069
|
+
if (fromDir.status === "error") return fromDir;
|
|
3070
|
+
collected = fromDir.collected;
|
|
3071
|
+
layerRev = fromDir.layerRev;
|
|
2763
3072
|
}
|
|
2764
|
-
const layer = await layerRevOf(collected.artifacts);
|
|
2765
|
-
if (layer.status === "error") {
|
|
2766
|
-
return {
|
|
2767
|
-
status: "error",
|
|
2768
|
-
message: layer.message
|
|
2769
|
-
};
|
|
2770
|
-
}
|
|
2771
|
-
const layerRev = options.layerRev ?? layer.layerRev;
|
|
2772
3073
|
const git = await resolveGit({
|
|
2773
3074
|
root,
|
|
2774
3075
|
options,
|
|
@@ -2778,9 +3079,10 @@ async function runPublish(options) {
|
|
|
2778
3079
|
return git;
|
|
2779
3080
|
}
|
|
2780
3081
|
const config = await findAndEvalValConfigFile(root).catch(() => null);
|
|
3082
|
+
const project = (config === null || config === void 0 ? void 0 : config.project) ?? env.VAL_PROJECT ?? null;
|
|
2781
3083
|
const credential = await resolvePublishCredential({
|
|
2782
3084
|
root,
|
|
2783
|
-
project
|
|
3085
|
+
project,
|
|
2784
3086
|
env,
|
|
2785
3087
|
...(options.fetchImpl ? {
|
|
2786
3088
|
fetchImpl: options.fetchImpl
|
|
@@ -2792,16 +3094,52 @@ async function runPublish(options) {
|
|
|
2792
3094
|
message: credential.message
|
|
2793
3095
|
};
|
|
2794
3096
|
}
|
|
3097
|
+
const contentHost = getContentHost(env);
|
|
2795
3098
|
const client = createPublishClient({
|
|
2796
|
-
host:
|
|
3099
|
+
host: contentHost,
|
|
2797
3100
|
token: credential.credential.token,
|
|
2798
3101
|
...(options.fetchImpl ? {
|
|
2799
3102
|
fetchImpl: options.fetchImpl
|
|
2800
3103
|
} : {})
|
|
2801
3104
|
});
|
|
2802
|
-
|
|
3105
|
+
let built = null;
|
|
3106
|
+
try {
|
|
3107
|
+
if (collected === null) {
|
|
3108
|
+
log("Asking content what to build against");
|
|
3109
|
+
const target = await client.buildTarget();
|
|
3110
|
+
const build = await buildArtifacts({
|
|
3111
|
+
root,
|
|
3112
|
+
target,
|
|
3113
|
+
project: project ?? "",
|
|
3114
|
+
contentHost,
|
|
3115
|
+
git: {
|
|
3116
|
+
commit: git.commit,
|
|
3117
|
+
branch: git.branch
|
|
3118
|
+
},
|
|
3119
|
+
env,
|
|
3120
|
+
log
|
|
3121
|
+
});
|
|
3122
|
+
if (build.status === "error") {
|
|
3123
|
+
return {
|
|
3124
|
+
status: "error",
|
|
3125
|
+
message: build.message
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
built = build;
|
|
3129
|
+
for (const warning of build.warnings) log(`warn: ${warning}`);
|
|
3130
|
+
const fromBuild = await collectPrebuilt(root, build.dir, log);
|
|
3131
|
+
if (fromBuild.status === "error") return fromBuild;
|
|
3132
|
+
collected = fromBuild.collected;
|
|
3133
|
+
// Named when content already holds it; otherwise it is in the directory.
|
|
3134
|
+
layerRev = build.layerRev !== undefined ? build.layerRev : fromBuild.layerRev;
|
|
3135
|
+
}
|
|
3136
|
+
} catch (err) {
|
|
3137
|
+
return errorFrom(err);
|
|
3138
|
+
}
|
|
3139
|
+
const buildHash = options.buildHash ?? ((_built = built) === null || _built === void 0 ? void 0 : _built.buildHash) ?? buildHashOf(collected.artifacts);
|
|
2803
3140
|
log(`${count(collected.artifacts.length, "artifact")}, ${formatBytes(collected.totalBytes)}` + `, at ${git.commit.slice(0, 7)} on ${git.branch}`);
|
|
2804
3141
|
try {
|
|
3142
|
+
var _built2;
|
|
2805
3143
|
return await publishDeclaredBuild({
|
|
2806
3144
|
client,
|
|
2807
3145
|
log,
|
|
@@ -2812,8 +3150,8 @@ async function runPublish(options) {
|
|
|
2812
3150
|
buildHash,
|
|
2813
3151
|
commit: git.commit,
|
|
2814
3152
|
branch: git.branch,
|
|
2815
|
-
layerRev,
|
|
2816
|
-
linksOwnCss: options.linksOwnCss ?? null,
|
|
3153
|
+
layerRev: options.layerRev ?? layerRev,
|
|
3154
|
+
linksOwnCss: options.linksOwnCss ?? ((_built2 = built) === null || _built2 === void 0 ? void 0 : _built2.linksOwnCss) ?? null,
|
|
2817
3155
|
artifacts: collected.artifacts.map(({
|
|
2818
3156
|
key,
|
|
2819
3157
|
sha256,
|
|
@@ -2828,7 +3166,53 @@ async function runPublish(options) {
|
|
|
2828
3166
|
});
|
|
2829
3167
|
} catch (err) {
|
|
2830
3168
|
return errorFrom(err);
|
|
3169
|
+
} finally {
|
|
3170
|
+
// Ours to remove: a build is written to a fresh temporary directory.
|
|
3171
|
+
if (built !== null) {
|
|
3172
|
+
fs$1.rmSync(built.dir, {
|
|
3173
|
+
recursive: true,
|
|
3174
|
+
force: true
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
/** A build on disk: its artifacts, and the layer it sends or names. */
|
|
3181
|
+
async function collectPrebuilt(root, artifacts, log) {
|
|
3182
|
+
const dir = resolveArtifactsDir({
|
|
3183
|
+
root,
|
|
3184
|
+
...(artifacts ? {
|
|
3185
|
+
dir: artifacts
|
|
3186
|
+
} : {})
|
|
3187
|
+
});
|
|
3188
|
+
if (dir.status === "error") {
|
|
3189
|
+
return {
|
|
3190
|
+
status: "error",
|
|
3191
|
+
message: dir.message
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
const collected = await collectArtifacts(dir.dir);
|
|
3195
|
+
for (const skipped of collected.skipped) {
|
|
3196
|
+
log(`Skipped ${skipped}`);
|
|
3197
|
+
}
|
|
3198
|
+
if (collected.artifacts.length === 0) {
|
|
3199
|
+
return {
|
|
3200
|
+
status: "error",
|
|
3201
|
+
message: `There is nothing in ${dir.dir}. Build the project before publishing it.`
|
|
3202
|
+
};
|
|
2831
3203
|
}
|
|
3204
|
+
const layer = await layerRevOf(collected.artifacts);
|
|
3205
|
+
if (layer.status === "error") {
|
|
3206
|
+
return {
|
|
3207
|
+
status: "error",
|
|
3208
|
+
message: layer.message
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
return {
|
|
3212
|
+
status: "ok",
|
|
3213
|
+
collected,
|
|
3214
|
+
layerRev: layer.layerRev
|
|
3215
|
+
};
|
|
2832
3216
|
}
|
|
2833
3217
|
async function publishDeclaredBuild(args) {
|
|
2834
3218
|
const {
|
|
@@ -3322,6 +3706,125 @@ function printProblem(problem) {
|
|
|
3322
3706
|
}
|
|
3323
3707
|
}
|
|
3324
3708
|
|
|
3709
|
+
/**
|
|
3710
|
+
* `val ci-report` -- CI saying how its build of a commit went.
|
|
3711
|
+
*
|
|
3712
|
+
* The last step of a connected project's workflow, run whether the build
|
|
3713
|
+
* passed or not. Content learns a build failed from this and nothing else,
|
|
3714
|
+
* so it needs no GitHub permission to tell an editor "Published, not on the
|
|
3715
|
+
* site yet: the build failed" (valbuild/home, docs/app-mode.md, "Failures":
|
|
3716
|
+
* after the seal). The run's address goes with it, for "View run".
|
|
3717
|
+
*
|
|
3718
|
+
* Everything has a default from GitHub Actions' own environment -- the
|
|
3719
|
+
* commit it checked out, the branch it ran for, the run's URL -- so the step
|
|
3720
|
+
* is one line. Never fails the job over a report it could not send: the
|
|
3721
|
+
* build's own result is what the job's status is, and a report is advice.
|
|
3722
|
+
*/
|
|
3723
|
+
|
|
3724
|
+
function ciReportFromEnv(options, env = process.env) {
|
|
3725
|
+
const status = options.status;
|
|
3726
|
+
if (status !== "failed" && status !== "succeeded") {
|
|
3727
|
+
return {
|
|
3728
|
+
status: "error",
|
|
3729
|
+
message: `--status is "failed" or "succeeded"${status === undefined ? "" : `, not "${status}"`}.`
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
const commit = options.commit ?? env.GITHUB_SHA ?? env.VAL_GIT_COMMIT;
|
|
3733
|
+
if (!commit) {
|
|
3734
|
+
return {
|
|
3735
|
+
status: "error",
|
|
3736
|
+
message: "No commit to report on: pass --commit, or run this in GitHub Actions (GITHUB_SHA)."
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
const branch = options.branch ?? env.GITHUB_REF_NAME ?? env.VAL_GIT_BRANCH;
|
|
3740
|
+
const url = options.url ?? (env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : undefined);
|
|
3741
|
+
return {
|
|
3742
|
+
status: "ok",
|
|
3743
|
+
report: {
|
|
3744
|
+
status,
|
|
3745
|
+
commit,
|
|
3746
|
+
...(branch ? {
|
|
3747
|
+
branch
|
|
3748
|
+
} : {}),
|
|
3749
|
+
...(url ? {
|
|
3750
|
+
url
|
|
3751
|
+
} : {})
|
|
3752
|
+
}
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
async function reportCiRun(options) {
|
|
3756
|
+
const env = options.env ?? process.env;
|
|
3757
|
+
const config = await findAndEvalValConfigFile(options.root).catch(() => null);
|
|
3758
|
+
const credential = await resolvePublishCredential({
|
|
3759
|
+
root: options.root,
|
|
3760
|
+
project: (config === null || config === void 0 ? void 0 : config.project) ?? env.VAL_PROJECT ?? null,
|
|
3761
|
+
env,
|
|
3762
|
+
...(options.fetchImpl ? {
|
|
3763
|
+
fetchImpl: options.fetchImpl
|
|
3764
|
+
} : {})
|
|
3765
|
+
});
|
|
3766
|
+
if (credential.status === "error") {
|
|
3767
|
+
return {
|
|
3768
|
+
status: "error",
|
|
3769
|
+
message: credential.message
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
const body = options.report;
|
|
3773
|
+
try {
|
|
3774
|
+
await postJson({
|
|
3775
|
+
url: `${getContentHost(env)}/v1/ci-runs`,
|
|
3776
|
+
headers: {
|
|
3777
|
+
Authorization: `Bearer ${credential.credential.token}`
|
|
3778
|
+
},
|
|
3779
|
+
body,
|
|
3780
|
+
...(options.fetchImpl ? {
|
|
3781
|
+
fetchImpl: options.fetchImpl
|
|
3782
|
+
} : {})
|
|
3783
|
+
});
|
|
3784
|
+
return {
|
|
3785
|
+
status: "reported"
|
|
3786
|
+
};
|
|
3787
|
+
} catch (e) {
|
|
3788
|
+
return {
|
|
3789
|
+
status: "error",
|
|
3790
|
+
message: e instanceof ContentHostError ? `Content did not take the report (${e.statusCode}): ${e.message}` : e instanceof Error ? e.message : String(e)
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
|
|
3795
|
+
/**
|
|
3796
|
+
* `val ci-report` -- a thin shell around `reportCiRun`. It never fails the
|
|
3797
|
+
* job: the build's own result is what the job's status is, and a report that
|
|
3798
|
+
* could not be sent is said and then left.
|
|
3799
|
+
*/
|
|
3800
|
+
async function ciReport(options) {
|
|
3801
|
+
const read = ciReportFromEnv({
|
|
3802
|
+
status: options.status,
|
|
3803
|
+
...(options.commit ? {
|
|
3804
|
+
commit: options.commit
|
|
3805
|
+
} : {}),
|
|
3806
|
+
...(options.branch ? {
|
|
3807
|
+
branch: options.branch
|
|
3808
|
+
} : {}),
|
|
3809
|
+
...(options.url ? {
|
|
3810
|
+
url: options.url
|
|
3811
|
+
} : {})
|
|
3812
|
+
});
|
|
3813
|
+
if (read.status === "error") {
|
|
3814
|
+
console.error(pc.yellow(`val ci-report: ${read.message}`));
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
const sent = await reportCiRun({
|
|
3818
|
+
root: options.root ?? process.cwd(),
|
|
3819
|
+
report: read.report
|
|
3820
|
+
});
|
|
3821
|
+
if (sent.status === "error") {
|
|
3822
|
+
console.error(pc.yellow(`val ci-report: not reported. ${sent.message}`));
|
|
3823
|
+
return;
|
|
3824
|
+
}
|
|
3825
|
+
console.log(pc.dim(`Reported the build of ${read.report.commit.slice(0, 7)} as ${read.report.status}.`));
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3325
3828
|
async function main() {
|
|
3326
3829
|
const {
|
|
3327
3830
|
input,
|
|
@@ -3364,19 +3867,35 @@ async function main() {
|
|
|
3364
3867
|
Options:
|
|
3365
3868
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
3366
3869
|
|
|
3870
|
+
Command: ci-report
|
|
3871
|
+
Description: tell content how CI's build of a commit went, for a connected
|
|
3872
|
+
project's workflow. Run as its last step, whatever the build did:
|
|
3873
|
+
a failed build then reads "Published, not on the site yet" in the
|
|
3874
|
+
Studio, with a link to the run. Never fails the job.
|
|
3875
|
+
Options:
|
|
3876
|
+
--status [failed|succeeded] How the build went
|
|
3877
|
+
--commit [sha] The commit built (default GITHUB_SHA)
|
|
3878
|
+
--branch [name] Its branch (default GITHUB_REF_NAME, else the project's)
|
|
3879
|
+
--url [url] The run (default this GitHub Actions run)
|
|
3880
|
+
|
|
3367
3881
|
Command: publish
|
|
3368
|
-
Description:
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3882
|
+
Description: build this project and publish it through content.val.build.
|
|
3883
|
+
Asks content what to build against, builds the site and its dependency layer from
|
|
3884
|
+
the checkout (needs @valbuild/tanstack-build, rolldown, @tanstack/router-generator
|
|
3885
|
+
and @tanstack/router-plugin installed), declares the artifacts, uploads the ones
|
|
3886
|
+
content does not already hold, and has content verify it by building and rendering
|
|
3887
|
+
a canary before the site changes. With --artifacts, or a .val/publish directory,
|
|
3888
|
+
it publishes that build instead of making one. Authenticates with VAL_PROJECT_TOKEN,
|
|
3889
|
+
or with the "val login" token in .val/pat.json (which needs the project, as
|
|
3890
|
+
"<org>/<project>", in val.config or VAL_PROJECT). Never as a flag: an argument is
|
|
3891
|
+
visible to anyone who can list processes, and it is kept in shell history and in
|
|
3892
|
+
every CI log.
|
|
3375
3893
|
Options:
|
|
3376
3894
|
--root [root], -r [root] Set project root directory (default process.cwd())
|
|
3377
|
-
--artifacts [dir]
|
|
3378
|
-
|
|
3379
|
-
|
|
3895
|
+
--artifacts [dir] Publish a build made elsewhere (default: build the checkout,
|
|
3896
|
+
unless <root>/.val/publish exists). The path of each file
|
|
3897
|
+
under it is its artifact key: server, client, css, rsc,
|
|
3898
|
+
layer, source, or a path under chunk/server, chunk/client,
|
|
3380
3899
|
chunk/rsc, asset, public
|
|
3381
3900
|
--commit [sha] The commit this build is of (default VAL_GIT_COMMIT,
|
|
3382
3901
|
else GITHUB_SHA, else git HEAD)
|
|
@@ -3485,6 +4004,12 @@ async function main() {
|
|
|
3485
4004
|
},
|
|
3486
4005
|
verbose: {
|
|
3487
4006
|
type: "boolean"
|
|
4007
|
+
},
|
|
4008
|
+
status: {
|
|
4009
|
+
type: "string"
|
|
4010
|
+
},
|
|
4011
|
+
url: {
|
|
4012
|
+
type: "string"
|
|
3488
4013
|
}
|
|
3489
4014
|
},
|
|
3490
4015
|
hardRejection: false
|
|
@@ -3528,6 +4053,14 @@ async function main() {
|
|
|
3528
4053
|
yes: flags.yes,
|
|
3529
4054
|
verbose: flags.verbose
|
|
3530
4055
|
});
|
|
4056
|
+
case "ci-report":
|
|
4057
|
+
return ciReport({
|
|
4058
|
+
root: flags.root,
|
|
4059
|
+
status: flags.status,
|
|
4060
|
+
commit: flags.commit,
|
|
4061
|
+
branch: flags.branch,
|
|
4062
|
+
url: flags.url
|
|
4063
|
+
});
|
|
3531
4064
|
case "publish":
|
|
3532
4065
|
return publish({
|
|
3533
4066
|
root: flags.root,
|