@valbuild/cli 0.136.10 → 0.138.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.
@@ -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.** `val publish` uploads artifacts; it does
2099
- * not produce them. Whatever builds the project - today the platform's own
2100
- * builder, which is where the wire, rolldown and dependency-layer steps live -
2101
- * writes this directory, and this reads it.
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,287 @@ 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 own files: its source as text, and the binaries it imports
2321
+ * (an image, a font) base64, as `BuildInput.assets` takes them. `public/` is
2322
+ * neither: it is served as is, and read by `readPublicFiles`.
2323
+ */
2324
+ function readProjectFiles(root, isAsset) {
2325
+ const sources = {};
2326
+ const assets = {};
2327
+ const walk = dir => {
2328
+ let entries;
2329
+ try {
2330
+ entries = fs$1.readdirSync(dir, {
2331
+ withFileTypes: true
2332
+ });
2333
+ } catch {
2334
+ return;
2335
+ }
2336
+ for (const entry of entries) {
2337
+ const full = path.join(dir, entry.name);
2338
+ if (entry.isDirectory()) {
2339
+ if (entry.name.startsWith(".") || IGNORED_DIRS.has(entry.name)) continue;
2340
+ if (dir === root && ROOT_OUTPUT_DIRS.has(entry.name)) continue;
2341
+ if (dir === root && entry.name === "public") continue;
2342
+ walk(full);
2343
+ continue;
2344
+ }
2345
+ const key = path.relative(root, full).split(path.sep).join("/");
2346
+ if (CODE_FILE.test(entry.name)) {
2347
+ sources[key] = fs$1.readFileSync(full, "utf8");
2348
+ } else if (JSON_FILE.test(entry.name)) {
2349
+ if (!ROOT_LOCKFILES.has(key)) {
2350
+ sources[key] = fs$1.readFileSync(full, "utf8");
2351
+ }
2352
+ } else if (isAsset(key)) {
2353
+ assets[key] = fs$1.readFileSync(full).toString("base64");
2354
+ }
2355
+ }
2356
+ };
2357
+ walk(root);
2358
+ return {
2359
+ sources,
2360
+ assets
2361
+ };
2362
+ }
2363
+
2364
+ /** Files under `public/`, base64, keyed `public/<path>` as the build takes them. */
2365
+ function readPublicFiles(root) {
2366
+ const out = {};
2367
+ const base = path.join(root, "public");
2368
+ const walk = dir => {
2369
+ let entries;
2370
+ try {
2371
+ entries = fs$1.readdirSync(dir, {
2372
+ withFileTypes: true
2373
+ });
2374
+ } catch {
2375
+ return;
2376
+ }
2377
+ for (const entry of entries) {
2378
+ const full = path.join(dir, entry.name);
2379
+ if (entry.isDirectory()) walk(full);else {
2380
+ const key = path.relative(root, full).split(path.sep).join("/");
2381
+ out[key] = fs$1.readFileSync(full).toString("base64");
2382
+ }
2383
+ }
2384
+ };
2385
+ walk(base);
2386
+ return out;
2387
+ }
2388
+
2389
+ /** The project's build-time variables: `PUBLIC_*` reach the browser, by the builder's rule. */
2390
+ function publicEnv(env) {
2391
+ const out = {};
2392
+ for (const [key, value] of Object.entries(env)) {
2393
+ if (key.startsWith("PUBLIC_") && value !== undefined) out[key] = value;
2394
+ }
2395
+ return out;
2396
+ }
2397
+
2398
+ /**
2399
+ * The target, with this project's layer as built here rather than as content
2400
+ * last stored it: a build has to link against the layer it ships with.
2401
+ */
2402
+ function targetWithLayer(target, layer) {
2403
+ return {
2404
+ ...target,
2405
+ project: {
2406
+ ...target.project,
2407
+ rev: (layer === null || layer === void 0 ? void 0 : layer.rev) ?? null,
2408
+ modules: (layer === null || layer === void 0 ? void 0 : layer.modules) ?? {},
2409
+ css: (layer === null || layer === void 0 ? void 0 : layer.css) ?? {},
2410
+ workerOnly: (layer === null || layer === void 0 ? void 0 : layer.workerOnly) ?? []
2411
+ }
2412
+ };
2413
+ }
2414
+ function writeArtifact(dir, key, body) {
2415
+ const file = path.join(dir, ...key.split("/"));
2416
+ fs$1.mkdirSync(path.dirname(file), {
2417
+ recursive: true
2418
+ });
2419
+ fs$1.writeFileSync(file, body);
2420
+ }
2421
+ async function buildArtifacts(options) {
2422
+ const log = options.log ?? (() => {});
2423
+ const loaded = await loadBuilder();
2424
+ if (loaded.status === "error") return loaded;
2425
+ const {
2426
+ build: builder,
2427
+ node
2428
+ } = loaded;
2429
+ const {
2430
+ root,
2431
+ target
2432
+ } = options;
2433
+ const warnings = [];
2434
+ try {
2435
+ /*
2436
+ * Wired for the platform at publish time, as the Studio's builds are: the
2437
+ * wired `val.server.ts` reads its files from a module the BUILD emits, so
2438
+ * no checkout can carry it. `rebakeGit` would be the tab's spelling of the
2439
+ * same thing for a record that is already wired.
2440
+ */
2441
+ const {
2442
+ sources: onDisk,
2443
+ assets
2444
+ } = readProjectFiles(root, builder.isAsset);
2445
+ const wiring = builder.isValProject(onDisk) && !builder.isWired(onDisk) ? builder.wireUp(onDisk, {
2446
+ project: options.project,
2447
+ loader: options.contentHost,
2448
+ ...(options.git ? {
2449
+ git: options.git
2450
+ } : {})
2451
+ }) : null;
2452
+ // Less the one about the layer, which is the thing this goes on to build.
2453
+ warnings.push(...((wiring === null || wiring === void 0 ? void 0 : wiring.warnings) ?? []).filter(warning => !warning.includes("dependency layer built for it")));
2454
+ const sources = (wiring === null || wiring === void 0 ? void 0 : wiring.files) ?? onDisk;
2455
+
2456
+ // The dependency layer: from what the project imports, of what it declares.
2457
+ log("Building the dependency layer");
2458
+ const manifest = (() => {
2459
+ try {
2460
+ return JSON.parse(sources["package.json"] ?? "{}");
2461
+ } catch {
2462
+ return {};
2463
+ }
2464
+ })();
2465
+ const deps = node.layerDepsOf(manifest, sources);
2466
+ const css = await node.collectDependencyCss(root, node.cssSpecifiersOf(sources));
2467
+ const layer = await node.buildVendorLayer(root, deps, css, {
2468
+ base: target.base,
2469
+ // A project on the platform layers its own copies; only the platform's
2470
+ // own checkout carries pinned ones.
2471
+ resolvePlatformModule: () => null,
2472
+ // No built base layer here: the export audit is skipped, not failed.
2473
+ baseLayerDir: null
2474
+ });
2475
+ log("Building the site");
2476
+ const fileBased = await node.isFileBased(root);
2477
+ const files = fileBased ? await node.generateRouteTree(root, sources) : sources;
2478
+ /*
2479
+ * The project's copy of the splitter first, then the one beside this CLI
2480
+ * -- where a package manager puts an optional peer the project installed
2481
+ * but did not hoist. Neither is a build that does not split, which costs
2482
+ * size and not behaviour.
2483
+ */
2484
+ const splitter = (await node.routeSplitter(root)) ?? (typeof __dirname === "string" ? await node.routeSplitter(__dirname) : null);
2485
+ if (splitter === null) {
2486
+ warnings.push("Routes are not code-split: @tanstack/router-plugin is not installed.");
2487
+ }
2488
+ const built = await builder.buildUserApp({
2489
+ files,
2490
+ target: targetWithLayer(target, layer),
2491
+ env: publicEnv(options.env ?? process.env),
2492
+ publicFiles: readPublicFiles(root),
2493
+ assets,
2494
+ // The project's own files, without the generated tree: what the next
2495
+ // publish -- the Studio's included -- starts from.
2496
+ projectSource: sources,
2497
+ loadCssModule: node.cssModuleLoader(root),
2498
+ ...(splitter ? {
2499
+ routeSplitter: splitter
2500
+ } : {}),
2501
+ rsc: target.project.rsc
2502
+ });
2503
+ const artifacts = await builder.publishArtifacts(built, {
2504
+ projectSource: sources
2505
+ });
2506
+ const dir = options.outDir ?? fs$1.mkdtempSync(path.join(os.tmpdir(), "val-publish-"));
2507
+ for (const artifact of artifacts) {
2508
+ writeArtifact(dir, artifact.key, artifact.body);
2509
+ }
2510
+ /*
2511
+ * The layer: sent when content does not hold this one, named when it
2512
+ * does. The rev is a digest of the built chunks, so "content has this" is
2513
+ * a fact rather than a hope.
2514
+ */
2515
+ let layerRev;
2516
+ if (layer === null) layerRev = null;else if (layer.rev === target.project.rev) layerRev = layer.rev;else writeArtifact(dir, "layer", zlib.gzipSync(JSON.stringify(layer)));
2517
+ return {
2518
+ status: "built",
2519
+ dir,
2520
+ buildHash: built.hash,
2521
+ linksOwnCss: built.linksOwnCss,
2522
+ ...(layerRev !== undefined ? {
2523
+ layerRev
2524
+ } : {}),
2525
+ warnings
2526
+ };
2527
+ } catch (e) {
2528
+ return {
2529
+ status: "error",
2530
+ message: `The site could not be built: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`
2531
+ };
2532
+ }
2533
+ }
2534
+
2253
2535
  /**
2254
2536
  * The one host `val publish` talks to.
2255
2537
  *
@@ -2396,6 +2678,11 @@ function createPublishClient(options) {
2396
2678
  };
2397
2679
  const publishUrl = (publishId, step) => `${host}/v1/publish/${encodeURIComponent(publishId)}${step ? `/${step}` : ""}`;
2398
2680
  return {
2681
+ buildTarget: async () => parseBuildTarget(await getJson({
2682
+ url: `${host}/v1/build-target`,
2683
+ headers,
2684
+ fetchImpl
2685
+ })),
2399
2686
  declare: async body => parseDeclare(await postJson({
2400
2687
  url: `${host}/v1/publish`,
2401
2688
  headers,
@@ -2735,40 +3022,33 @@ const DECLARE_ROUNDS = 3;
2735
3022
  * that could reach either.
2736
3023
  */
2737
3024
  async function runPublish(options) {
3025
+ var _built;
2738
3026
  const env = options.env ?? process.env;
2739
3027
  const log = options.log ?? (line => console.log(line));
2740
3028
  const sleep = options.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
2741
3029
  const root = options.root ? path.resolve(options.root) : process.cwd();
2742
- const dir = resolveArtifactsDir({
2743
- root,
2744
- ...(options.artifacts ? {
2745
- dir: options.artifacts
2746
- } : {})
2747
- });
2748
- if (dir.status === "error") {
2749
- return {
2750
- status: "error",
2751
- message: dir.message
2752
- };
2753
- }
2754
- const collected = await collectArtifacts(dir.dir);
2755
- for (const skipped of collected.skipped) {
2756
- log(`Skipped ${skipped}`);
2757
- }
2758
- if (collected.artifacts.length === 0) {
3030
+
3031
+ /*
3032
+ * Upload-only when there is a build to upload: `--artifacts`, or the
3033
+ * directory it defaults to. Otherwise this builds the checkout -- which is
3034
+ * what a CI runner has, and `.val/` is never committed, so a fresh checkout
3035
+ * always builds.
3036
+ */
3037
+ const prebuilt = options.artifacts !== undefined || fs$1.existsSync(path.join(root, DEFAULT_ARTIFACTS_DIR));
3038
+ if (!prebuilt && !fs$1.existsSync(path.join(root, "package.json"))) {
2759
3039
  return {
2760
3040
  status: "error",
2761
- message: `There is nothing in ${dir.dir}. Build the project before publishing it.`
3041
+ 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>"
2762
3042
  };
2763
3043
  }
2764
- const layer = await layerRevOf(collected.artifacts);
2765
- if (layer.status === "error") {
2766
- return {
2767
- status: "error",
2768
- message: layer.message
2769
- };
3044
+ let collected = null;
3045
+ let layerRev = null;
3046
+ if (prebuilt) {
3047
+ const fromDir = await collectPrebuilt(root, options.artifacts, log);
3048
+ if (fromDir.status === "error") return fromDir;
3049
+ collected = fromDir.collected;
3050
+ layerRev = fromDir.layerRev;
2770
3051
  }
2771
- const layerRev = options.layerRev ?? layer.layerRev;
2772
3052
  const git = await resolveGit({
2773
3053
  root,
2774
3054
  options,
@@ -2778,9 +3058,10 @@ async function runPublish(options) {
2778
3058
  return git;
2779
3059
  }
2780
3060
  const config = await findAndEvalValConfigFile(root).catch(() => null);
3061
+ const project = (config === null || config === void 0 ? void 0 : config.project) ?? env.VAL_PROJECT ?? null;
2781
3062
  const credential = await resolvePublishCredential({
2782
3063
  root,
2783
- project: (config === null || config === void 0 ? void 0 : config.project) ?? env.VAL_PROJECT ?? null,
3064
+ project,
2784
3065
  env,
2785
3066
  ...(options.fetchImpl ? {
2786
3067
  fetchImpl: options.fetchImpl
@@ -2792,16 +3073,52 @@ async function runPublish(options) {
2792
3073
  message: credential.message
2793
3074
  };
2794
3075
  }
3076
+ const contentHost = getContentHost(env);
2795
3077
  const client = createPublishClient({
2796
- host: getContentHost(env),
3078
+ host: contentHost,
2797
3079
  token: credential.credential.token,
2798
3080
  ...(options.fetchImpl ? {
2799
3081
  fetchImpl: options.fetchImpl
2800
3082
  } : {})
2801
3083
  });
2802
- const buildHash = options.buildHash ?? buildHashOf(collected.artifacts);
3084
+ let built = null;
3085
+ try {
3086
+ if (collected === null) {
3087
+ log("Asking content what to build against");
3088
+ const target = await client.buildTarget();
3089
+ const build = await buildArtifacts({
3090
+ root,
3091
+ target,
3092
+ project: project ?? "",
3093
+ contentHost,
3094
+ git: {
3095
+ commit: git.commit,
3096
+ branch: git.branch
3097
+ },
3098
+ env,
3099
+ log
3100
+ });
3101
+ if (build.status === "error") {
3102
+ return {
3103
+ status: "error",
3104
+ message: build.message
3105
+ };
3106
+ }
3107
+ built = build;
3108
+ for (const warning of build.warnings) log(`warn: ${warning}`);
3109
+ const fromBuild = await collectPrebuilt(root, build.dir, log);
3110
+ if (fromBuild.status === "error") return fromBuild;
3111
+ collected = fromBuild.collected;
3112
+ // Named when content already holds it; otherwise it is in the directory.
3113
+ layerRev = build.layerRev !== undefined ? build.layerRev : fromBuild.layerRev;
3114
+ }
3115
+ } catch (err) {
3116
+ return errorFrom(err);
3117
+ }
3118
+ const buildHash = options.buildHash ?? ((_built = built) === null || _built === void 0 ? void 0 : _built.buildHash) ?? buildHashOf(collected.artifacts);
2803
3119
  log(`${count(collected.artifacts.length, "artifact")}, ${formatBytes(collected.totalBytes)}` + `, at ${git.commit.slice(0, 7)} on ${git.branch}`);
2804
3120
  try {
3121
+ var _built2;
2805
3122
  return await publishDeclaredBuild({
2806
3123
  client,
2807
3124
  log,
@@ -2812,8 +3129,8 @@ async function runPublish(options) {
2812
3129
  buildHash,
2813
3130
  commit: git.commit,
2814
3131
  branch: git.branch,
2815
- layerRev,
2816
- linksOwnCss: options.linksOwnCss ?? null,
3132
+ layerRev: options.layerRev ?? layerRev,
3133
+ linksOwnCss: options.linksOwnCss ?? ((_built2 = built) === null || _built2 === void 0 ? void 0 : _built2.linksOwnCss) ?? null,
2817
3134
  artifacts: collected.artifacts.map(({
2818
3135
  key,
2819
3136
  sha256,
@@ -2828,8 +3145,54 @@ async function runPublish(options) {
2828
3145
  });
2829
3146
  } catch (err) {
2830
3147
  return errorFrom(err);
3148
+ } finally {
3149
+ // Ours to remove: a build is written to a fresh temporary directory.
3150
+ if (built !== null) {
3151
+ fs$1.rmSync(built.dir, {
3152
+ recursive: true,
3153
+ force: true
3154
+ });
3155
+ }
2831
3156
  }
2832
3157
  }
3158
+
3159
+ /** A build on disk: its artifacts, and the layer it sends or names. */
3160
+ async function collectPrebuilt(root, artifacts, log) {
3161
+ const dir = resolveArtifactsDir({
3162
+ root,
3163
+ ...(artifacts ? {
3164
+ dir: artifacts
3165
+ } : {})
3166
+ });
3167
+ if (dir.status === "error") {
3168
+ return {
3169
+ status: "error",
3170
+ message: dir.message
3171
+ };
3172
+ }
3173
+ const collected = await collectArtifacts(dir.dir);
3174
+ for (const skipped of collected.skipped) {
3175
+ log(`Skipped ${skipped}`);
3176
+ }
3177
+ if (collected.artifacts.length === 0) {
3178
+ return {
3179
+ status: "error",
3180
+ message: `There is nothing in ${dir.dir}. Build the project before publishing it.`
3181
+ };
3182
+ }
3183
+ const layer = await layerRevOf(collected.artifacts);
3184
+ if (layer.status === "error") {
3185
+ return {
3186
+ status: "error",
3187
+ message: layer.message
3188
+ };
3189
+ }
3190
+ return {
3191
+ status: "ok",
3192
+ collected,
3193
+ layerRev: layer.layerRev
3194
+ };
3195
+ }
2833
3196
  async function publishDeclaredBuild(args) {
2834
3197
  const {
2835
3198
  client,
@@ -3322,6 +3685,125 @@ function printProblem(problem) {
3322
3685
  }
3323
3686
  }
3324
3687
 
3688
+ /**
3689
+ * `val ci-report` -- CI saying how its build of a commit went.
3690
+ *
3691
+ * The last step of a connected project's workflow, run whether the build
3692
+ * passed or not. Content learns a build failed from this and nothing else,
3693
+ * so it needs no GitHub permission to tell an editor "Published, not on the
3694
+ * site yet: the build failed" (valbuild/home, docs/app-mode.md, "Failures":
3695
+ * after the seal). The run's address goes with it, for "View run".
3696
+ *
3697
+ * Everything has a default from GitHub Actions' own environment -- the
3698
+ * commit it checked out, the branch it ran for, the run's URL -- so the step
3699
+ * is one line. Never fails the job over a report it could not send: the
3700
+ * build's own result is what the job's status is, and a report is advice.
3701
+ */
3702
+
3703
+ function ciReportFromEnv(options, env = process.env) {
3704
+ const status = options.status;
3705
+ if (status !== "failed" && status !== "succeeded") {
3706
+ return {
3707
+ status: "error",
3708
+ message: `--status is "failed" or "succeeded"${status === undefined ? "" : `, not "${status}"`}.`
3709
+ };
3710
+ }
3711
+ const commit = options.commit ?? env.GITHUB_SHA ?? env.VAL_GIT_COMMIT;
3712
+ if (!commit) {
3713
+ return {
3714
+ status: "error",
3715
+ message: "No commit to report on: pass --commit, or run this in GitHub Actions (GITHUB_SHA)."
3716
+ };
3717
+ }
3718
+ const branch = options.branch ?? env.GITHUB_REF_NAME ?? env.VAL_GIT_BRANCH;
3719
+ 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);
3720
+ return {
3721
+ status: "ok",
3722
+ report: {
3723
+ status,
3724
+ commit,
3725
+ ...(branch ? {
3726
+ branch
3727
+ } : {}),
3728
+ ...(url ? {
3729
+ url
3730
+ } : {})
3731
+ }
3732
+ };
3733
+ }
3734
+ async function reportCiRun(options) {
3735
+ const env = options.env ?? process.env;
3736
+ const config = await findAndEvalValConfigFile(options.root).catch(() => null);
3737
+ const credential = await resolvePublishCredential({
3738
+ root: options.root,
3739
+ project: (config === null || config === void 0 ? void 0 : config.project) ?? env.VAL_PROJECT ?? null,
3740
+ env,
3741
+ ...(options.fetchImpl ? {
3742
+ fetchImpl: options.fetchImpl
3743
+ } : {})
3744
+ });
3745
+ if (credential.status === "error") {
3746
+ return {
3747
+ status: "error",
3748
+ message: credential.message
3749
+ };
3750
+ }
3751
+ const body = options.report;
3752
+ try {
3753
+ await postJson({
3754
+ url: `${getContentHost(env)}/v1/ci-runs`,
3755
+ headers: {
3756
+ Authorization: `Bearer ${credential.credential.token}`
3757
+ },
3758
+ body,
3759
+ ...(options.fetchImpl ? {
3760
+ fetchImpl: options.fetchImpl
3761
+ } : {})
3762
+ });
3763
+ return {
3764
+ status: "reported"
3765
+ };
3766
+ } catch (e) {
3767
+ return {
3768
+ status: "error",
3769
+ message: e instanceof ContentHostError ? `Content did not take the report (${e.statusCode}): ${e.message}` : e instanceof Error ? e.message : String(e)
3770
+ };
3771
+ }
3772
+ }
3773
+
3774
+ /**
3775
+ * `val ci-report` -- a thin shell around `reportCiRun`. It never fails the
3776
+ * job: the build's own result is what the job's status is, and a report that
3777
+ * could not be sent is said and then left.
3778
+ */
3779
+ async function ciReport(options) {
3780
+ const read = ciReportFromEnv({
3781
+ status: options.status,
3782
+ ...(options.commit ? {
3783
+ commit: options.commit
3784
+ } : {}),
3785
+ ...(options.branch ? {
3786
+ branch: options.branch
3787
+ } : {}),
3788
+ ...(options.url ? {
3789
+ url: options.url
3790
+ } : {})
3791
+ });
3792
+ if (read.status === "error") {
3793
+ console.error(pc.yellow(`val ci-report: ${read.message}`));
3794
+ return;
3795
+ }
3796
+ const sent = await reportCiRun({
3797
+ root: options.root ?? process.cwd(),
3798
+ report: read.report
3799
+ });
3800
+ if (sent.status === "error") {
3801
+ console.error(pc.yellow(`val ci-report: not reported. ${sent.message}`));
3802
+ return;
3803
+ }
3804
+ console.log(pc.dim(`Reported the build of ${read.report.commit.slice(0, 7)} as ${read.report.status}.`));
3805
+ }
3806
+
3325
3807
  async function main() {
3326
3808
  const {
3327
3809
  input,
@@ -3364,19 +3846,35 @@ async function main() {
3364
3846
  Options:
3365
3847
  --root [root], -r [root] Set project root directory (default process.cwd())
3366
3848
 
3849
+ Command: ci-report
3850
+ Description: tell content how CI's build of a commit went, for a connected
3851
+ project's workflow. Run as its last step, whatever the build did:
3852
+ a failed build then reads "Published, not on the site yet" in the
3853
+ Studio, with a link to the run. Never fails the job.
3854
+ Options:
3855
+ --status [failed|succeeded] How the build went
3856
+ --commit [sha] The commit built (default GITHUB_SHA)
3857
+ --branch [name] Its branch (default GITHUB_REF_NAME, else the project's)
3858
+ --url [url] The run (default this GitHub Actions run)
3859
+
3367
3860
  Command: publish
3368
- Description: publish this project's build through content.val.build.
3369
- Declares the build's artifacts, uploads the ones content does not already hold,
3370
- and has content verify it by building and rendering a canary before the site
3371
- changes. Authenticates with VAL_PROJECT_TOKEN, or with the "val login" token in
3372
- .val/pat.json (which needs the project, as "<org>/<project>", in val.config or
3373
- VAL_PROJECT). Never as a flag: an argument is visible to anyone who can list
3374
- processes, and it is kept in shell history and in every CI log.
3861
+ Description: build this project and publish it through content.val.build.
3862
+ Asks content what to build against, builds the site and its dependency layer from
3863
+ the checkout (needs @valbuild/tanstack-build, rolldown, @tanstack/router-generator
3864
+ and @tanstack/router-plugin installed), declares the artifacts, uploads the ones
3865
+ content does not already hold, and has content verify it by building and rendering
3866
+ a canary before the site changes. With --artifacts, or a .val/publish directory,
3867
+ it publishes that build instead of making one. Authenticates with VAL_PROJECT_TOKEN,
3868
+ or with the "val login" token in .val/pat.json (which needs the project, as
3869
+ "<org>/<project>", in val.config or VAL_PROJECT). Never as a flag: an argument is
3870
+ visible to anyone who can list processes, and it is kept in shell history and in
3871
+ every CI log.
3375
3872
  Options:
3376
3873
  --root [root], -r [root] Set project root directory (default process.cwd())
3377
- --artifacts [dir] The built artifacts (default <root>/.val/publish). The path
3378
- of each file under it is its artifact key: server, client,
3379
- css, rsc, layer, or a path under chunk/server, chunk/client,
3874
+ --artifacts [dir] Publish a build made elsewhere (default: build the checkout,
3875
+ unless <root>/.val/publish exists). The path of each file
3876
+ under it is its artifact key: server, client, css, rsc,
3877
+ layer, source, or a path under chunk/server, chunk/client,
3380
3878
  chunk/rsc, asset, public
3381
3879
  --commit [sha] The commit this build is of (default VAL_GIT_COMMIT,
3382
3880
  else GITHUB_SHA, else git HEAD)
@@ -3485,6 +3983,12 @@ async function main() {
3485
3983
  },
3486
3984
  verbose: {
3487
3985
  type: "boolean"
3986
+ },
3987
+ status: {
3988
+ type: "string"
3989
+ },
3990
+ url: {
3991
+ type: "string"
3488
3992
  }
3489
3993
  },
3490
3994
  hardRejection: false
@@ -3528,6 +4032,14 @@ async function main() {
3528
4032
  yes: flags.yes,
3529
4033
  verbose: flags.verbose
3530
4034
  });
4035
+ case "ci-report":
4036
+ return ciReport({
4037
+ root: flags.root,
4038
+ status: flags.status,
4039
+ commit: flags.commit,
4040
+ branch: flags.branch,
4041
+ url: flags.url
4042
+ });
3531
4043
  case "publish":
3532
4044
  return publish({
3533
4045
  root: flags.root,