@vibesdotdev/infra-deploy 1.0.0-rc.6 → 1.0.0-rc.8
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/cli/dns/infra-dns-list.cli-command.impl.d.ts +1 -1
- package/dist/cli/dns/infra-dns-list.cli-command.impl.js +1 -1
- package/dist/cli/npm/mirror-registry-artifacts.d.ts +3 -2
- package/dist/cli/npm/mirror-registry-artifacts.d.ts.map +1 -1
- package/dist/cli/npm/mirror-registry-artifacts.js +6 -4
- package/dist/cli/npm/mirror-registry-artifacts.js.map +1 -1
- package/dist/cli/npm/put-attested-packument.d.ts +15 -0
- package/dist/cli/npm/put-attested-packument.d.ts.map +1 -1
- package/dist/cli/npm/put-attested-packument.js +31 -5
- package/dist/cli/npm/put-attested-packument.js.map +1 -1
- package/dist/cli/run/build-closure.d.ts +34 -0
- package/dist/cli/run/build-closure.d.ts.map +1 -0
- package/dist/cli/run/build-closure.js +122 -0
- package/dist/cli/run/build-closure.js.map +1 -0
- package/dist/cli/run/build-only.d.ts +45 -0
- package/dist/cli/run/build-only.d.ts.map +1 -0
- package/dist/cli/run/build-only.js +35 -0
- package/dist/cli/run/build-only.js.map +1 -0
- package/dist/cli/run/deploy-smoke.d.ts +6 -0
- package/dist/cli/run/deploy-smoke.d.ts.map +1 -1
- package/dist/cli/run/deploy-smoke.js +43 -3
- package/dist/cli/run/deploy-smoke.js.map +1 -1
- package/dist/cli/run/deployment-budget-gate.d.ts +41 -0
- package/dist/cli/run/deployment-budget-gate.d.ts.map +1 -0
- package/dist/cli/run/deployment-budget-gate.js +85 -0
- package/dist/cli/run/deployment-budget-gate.js.map +1 -0
- package/dist/cli/run/deployment-provider.d.ts +24 -0
- package/dist/cli/run/deployment-provider.d.ts.map +1 -1
- package/dist/cli/run/deployment-provider.js +57 -17
- package/dist/cli/run/deployment-provider.js.map +1 -1
- package/dist/cli/run/deployment-run-ledger.d.ts +2 -0
- package/dist/cli/run/deployment-run-ledger.d.ts.map +1 -1
- package/dist/cli/run/deployment-run-ledger.js +1 -0
- package/dist/cli/run/deployment-run-ledger.js.map +1 -1
- package/dist/cli/run/edge-hostname-reconcile.d.ts +52 -0
- package/dist/cli/run/edge-hostname-reconcile.d.ts.map +1 -0
- package/dist/cli/run/edge-hostname-reconcile.js +105 -0
- package/dist/cli/run/edge-hostname-reconcile.js.map +1 -0
- package/dist/cli/run/immutable-deployment-release.program.d.ts +9 -0
- package/dist/cli/run/immutable-deployment-release.program.d.ts.map +1 -1
- package/dist/cli/run/immutable-deployment-release.program.js +2 -1
- package/dist/cli/run/immutable-deployment-release.program.js.map +1 -1
- package/dist/cli/run/infra-deploy.run.cli-command.descriptor.d.ts +6 -0
- package/dist/cli/run/infra-deploy.run.cli-command.descriptor.d.ts.map +1 -1
- package/dist/cli/run/infra-deploy.run.cli-command.descriptor.js +8 -0
- package/dist/cli/run/infra-deploy.run.cli-command.descriptor.js.map +1 -1
- package/dist/cli/run/infra-deploy.run.cli-command.impl.d.ts.map +1 -1
- package/dist/cli/run/infra-deploy.run.cli-command.impl.js +118 -7
- package/dist/cli/run/infra-deploy.run.cli-command.impl.js.map +1 -1
- package/dist/cli/shared/build-workspace-artifacts.d.ts +44 -7
- package/dist/cli/shared/build-workspace-artifacts.d.ts.map +1 -1
- package/dist/cli/shared/build-workspace-artifacts.js +313 -202
- package/dist/cli/shared/build-workspace-artifacts.js.map +1 -1
- package/dist/cli/shared/workspace-artifact-store.d.ts +5 -0
- package/dist/cli/shared/workspace-artifact-store.d.ts.map +1 -1
- package/dist/cli/shared/workspace-artifact-store.js +22 -3
- package/dist/cli/shared/workspace-artifact-store.js.map +1 -1
- package/dist/storage/deployment-runs.schema.d.ts +2 -0
- package/dist/storage/deployment-runs.schema.d.ts.map +1 -1
- package/dist/storage/deployment-runs.schema.js +2 -0
- package/dist/storage/deployment-runs.schema.js.map +1 -1
- package/package.json +21 -21
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { satisfies, validRange } from 'semver';
|
|
2
2
|
import * as z from 'zod/v4';
|
|
3
|
-
import { CapabilityError, settle } from '@vibesdotdev/kernel';
|
|
3
|
+
import { CapabilityError, settle, settleNow } from '@vibesdotdev/kernel';
|
|
4
4
|
import { JsonValueSchema } from '@vibesdotdev/kernel/schemas/json';
|
|
5
5
|
import { planDag } from '@vibesdotdev/shared/schemas';
|
|
6
6
|
import { resolveHash } from '@vibesdotdev/crypto';
|
|
@@ -274,7 +274,7 @@ async function readAppManifest(host, path) {
|
|
|
274
274
|
}
|
|
275
275
|
/** The errno facts a raw host absence carries. */
|
|
276
276
|
const ErrnoCodeSchema = z.object({ code: z.string().optional() }).loose();
|
|
277
|
-
function isMissingPathError(error) {
|
|
277
|
+
export function isMissingPathError(error) {
|
|
278
278
|
if (error instanceof CapabilityError && error.code === 'NOT_FOUND')
|
|
279
279
|
return true;
|
|
280
280
|
const facts = ErrnoCodeSchema.safeParse(error);
|
|
@@ -502,7 +502,7 @@ async function collectTsconfigInputs(host, packageRoot, artifactsDir, configPath
|
|
|
502
502
|
* every `tsconfig*.json` (and their `extends`/`include` targets), tool config
|
|
503
503
|
* files (`vite.config.*`, `svelte.config.*`, ...), `scripts/`, every file the
|
|
504
504
|
* selected build script names, and every non-test, non-hidden file under
|
|
505
|
-
* `src/`. Workspace dependencies
|
|
505
|
+
* `src/`. Workspace dependencies enter through the stamp's traces ({@link currentTraces}).
|
|
506
506
|
*/
|
|
507
507
|
async function packageBuildInputs(host, pkg) {
|
|
508
508
|
const packageRoot = pkg.dir;
|
|
@@ -548,155 +548,108 @@ async function computePackageSourceHash(host, packageRoot) {
|
|
|
548
548
|
}
|
|
549
549
|
return hash.digestHex();
|
|
550
550
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
function dependencyHashMemo(ownHashes) {
|
|
554
|
-
let memo = effectiveHashMemos.get(ownHashes);
|
|
555
|
-
if (!memo) {
|
|
556
|
-
memo = { effective: new Map(), edges: new Map() };
|
|
557
|
-
effectiveHashMemos.set(ownHashes, memo);
|
|
558
|
-
}
|
|
559
|
-
return memo;
|
|
560
|
-
}
|
|
561
|
-
function buildDependencyNames(host, pkg, packages, memo) {
|
|
562
|
-
let pending = memo.edges.get(pkg.name);
|
|
551
|
+
function buildDependencyNames(host, pkg, packages, context) {
|
|
552
|
+
let pending = context.edges.get(pkg.name);
|
|
563
553
|
if (!pending) {
|
|
564
554
|
pending = workspaceDependencyEdges(host, pkg, packages)
|
|
565
555
|
.then(({ inputs }) => inputs.filter((name) => packages.has(name)));
|
|
566
|
-
|
|
556
|
+
context.edges.set(pkg.name, pending);
|
|
567
557
|
}
|
|
568
558
|
return pending;
|
|
569
559
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
const
|
|
560
|
+
function createTraceContext() {
|
|
561
|
+
return { ownHashes: new Map(), edges: new Map(), outputs: new Map(), verdicts: new Map() };
|
|
562
|
+
}
|
|
563
|
+
/** `name`'s artifacts directory was replaced: its output and every verdict that read it are stale. */
|
|
564
|
+
function forgetArtifacts(context, name) {
|
|
565
|
+
context.outputs.delete(name);
|
|
566
|
+
context.verdicts.clear();
|
|
567
|
+
}
|
|
568
|
+
async function ownSourceHash(host, pkg, context) {
|
|
569
|
+
let own = context.ownHashes.get(pkg.name);
|
|
570
|
+
if (own === undefined) {
|
|
571
|
+
own = await computePackageSourceHash(host, pkg.dir);
|
|
572
|
+
context.ownHashes.set(pkg.name, own);
|
|
573
|
+
}
|
|
574
|
+
return own;
|
|
575
|
+
}
|
|
576
|
+
/** Every workspace package `pkg` reaches through build-input edges, itself excluded, sorted. */
|
|
577
|
+
async function buildInputClosure(host, pkg, packages, context) {
|
|
578
|
+
if (!packages)
|
|
579
|
+
return [];
|
|
580
|
+
const reached = new Set();
|
|
581
|
+
const pending = [pkg];
|
|
592
582
|
for (let cursor = 0; cursor < pending.length; cursor += 1) {
|
|
593
|
-
const name
|
|
594
|
-
|
|
595
|
-
continue;
|
|
596
|
-
const dependencies = await buildDependencyNames(host, packages.get(name), packages, scope);
|
|
597
|
-
graph.set(name, dependencies);
|
|
598
|
-
for (const dependency of dependencies) {
|
|
599
|
-
if (memo.has(dependency) || queued.has(dependency))
|
|
583
|
+
for (const name of await buildDependencyNames(host, pending[cursor], packages, context)) {
|
|
584
|
+
if (reached.has(name))
|
|
600
585
|
continue;
|
|
601
|
-
|
|
602
|
-
pending.push(
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const connect = (name) => {
|
|
619
|
-
index.set(name, counter);
|
|
620
|
-
low.set(name, counter);
|
|
621
|
-
counter += 1;
|
|
622
|
-
stack.push(name);
|
|
623
|
-
onStack.add(name);
|
|
624
|
-
for (const dep of edges(name)) {
|
|
625
|
-
if (memo.has(dep))
|
|
586
|
+
reached.add(name);
|
|
587
|
+
pending.push(packages.get(name));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
reached.delete(pkg.name);
|
|
591
|
+
return [...reached].sort();
|
|
592
|
+
}
|
|
593
|
+
/** The hash of every file under `artifactsDirectory` except the stamp and incremental build state. */
|
|
594
|
+
async function computeOutputHash(host, artifactsDirectory) {
|
|
595
|
+
if (!(await host.files.exists(artifactsDirectory)))
|
|
596
|
+
return 'absent';
|
|
597
|
+
const files = [];
|
|
598
|
+
const visit = async (directory) => {
|
|
599
|
+
for (const entry of await host.files.list(directory)) {
|
|
600
|
+
const absolute = host.paths.join(directory, entry.name);
|
|
601
|
+
if (entry.kind === 'directory') {
|
|
602
|
+
await visit(absolute);
|
|
626
603
|
continue;
|
|
627
|
-
if (!index.has(dep)) {
|
|
628
|
-
connect(dep);
|
|
629
|
-
low.set(name, Math.min(low.get(name), low.get(dep)));
|
|
630
|
-
}
|
|
631
|
-
else if (onStack.has(dep)) {
|
|
632
|
-
low.set(name, Math.min(low.get(name), index.get(dep)));
|
|
633
604
|
}
|
|
605
|
+
const path = toPosixPath(host, host.paths.relative(artifactsDirectory, absolute));
|
|
606
|
+
if (path === BUILD_STAMP_FILENAME || path.endsWith('.tsbuildinfo'))
|
|
607
|
+
continue;
|
|
608
|
+
files.push({ path, absolute });
|
|
634
609
|
}
|
|
635
|
-
if (low.get(name) === index.get(name)) {
|
|
636
|
-
const component = [];
|
|
637
|
-
let member;
|
|
638
|
-
do {
|
|
639
|
-
member = stack.pop();
|
|
640
|
-
onStack.delete(member);
|
|
641
|
-
component.push(member);
|
|
642
|
-
} while (member !== name);
|
|
643
|
-
components.push(component);
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
connect(pkg.name);
|
|
647
|
-
const ownOf = async (name) => {
|
|
648
|
-
let own = ownHashes.get(name);
|
|
649
|
-
if (own === undefined) {
|
|
650
|
-
own = await computePackageSourceHash(host, packages.get(name).dir);
|
|
651
|
-
ownHashes.set(name, own);
|
|
652
|
-
}
|
|
653
|
-
return own;
|
|
654
610
|
};
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
/** What a stamp records about a package's workspace dependencies: name -> effective source hash. */
|
|
688
|
-
const RecordedDependencyHashesSchema = z.record(z.string(), z.string());
|
|
689
|
-
async function workspaceDependencyHashes(host, pkg, packages, ownHashes) {
|
|
690
|
-
const recorded = {};
|
|
611
|
+
await visit(artifactsDirectory);
|
|
612
|
+
if (files.length === 0)
|
|
613
|
+
return 'absent';
|
|
614
|
+
files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
615
|
+
const hash = host.sha256().update('outputs');
|
|
616
|
+
for (const file of files) {
|
|
617
|
+
const fileHash = host.sha256().update(await readBytes(host.files, file.absolute)).digestHex();
|
|
618
|
+
hash.update(`\0${file.path}\0${fileHash}`);
|
|
619
|
+
}
|
|
620
|
+
return hash.digestHex();
|
|
621
|
+
}
|
|
622
|
+
function outputHashOf(host, pkg, context) {
|
|
623
|
+
let pending = context.outputs.get(pkg.name);
|
|
624
|
+
if (!pending) {
|
|
625
|
+
pending = computeOutputHash(host, host.paths.join(pkg.dir, resolveArtifactsDir(pkg)));
|
|
626
|
+
context.outputs.set(pkg.name, pending);
|
|
627
|
+
}
|
|
628
|
+
return pending;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* What a dependent's build consumed from `pkg`: the bytes it emitted, or for a
|
|
632
|
+
* package that builds nothing, its own sources.
|
|
633
|
+
*/
|
|
634
|
+
async function traceOf(host, pkg, context) {
|
|
635
|
+
return hasBuildArtifacts(pkg)
|
|
636
|
+
? `output:${await outputHashOf(host, pkg, context)}`
|
|
637
|
+
: `source:${await ownSourceHash(host, pkg, context)}`;
|
|
638
|
+
}
|
|
639
|
+
/** The current trace of every package in `pkg`'s build-input closure. */
|
|
640
|
+
async function currentTraces(host, pkg, packages, context) {
|
|
641
|
+
const traces = {};
|
|
691
642
|
if (!packages)
|
|
692
|
-
return
|
|
693
|
-
for (const name of await
|
|
694
|
-
|
|
695
|
-
if (!dependency)
|
|
696
|
-
continue;
|
|
697
|
-
recorded[name] = await effectiveSourceHash(host, dependency, packages, ownHashes);
|
|
643
|
+
return traces;
|
|
644
|
+
for (const name of await buildInputClosure(host, pkg, packages, context)) {
|
|
645
|
+
traces[name] = await traceOf(host, packages.get(name), context);
|
|
698
646
|
}
|
|
699
|
-
return
|
|
647
|
+
return traces;
|
|
648
|
+
}
|
|
649
|
+
/** The first closure member whose recorded trace differs from `current`, if any. */
|
|
650
|
+
function changedTrace(recorded, current) {
|
|
651
|
+
const names = new Set([...Object.keys(recorded), ...Object.keys(current)]);
|
|
652
|
+
return [...names].sort().find((name) => recorded[name] !== current[name]);
|
|
700
653
|
}
|
|
701
654
|
/** Every file the build emitted under the artifacts directory (the stamp itself excluded). */
|
|
702
655
|
async function listEmittedFiles(host, artifactsDirectory) {
|
|
@@ -744,18 +697,22 @@ async function removeBuildStamp(host, pkg) {
|
|
|
744
697
|
await removeIfPresent(host.files, host.paths.join(pkg.dir, resolveArtifactsDir(pkg), BUILD_STAMP_FILENAME));
|
|
745
698
|
}
|
|
746
699
|
/**
|
|
747
|
-
* Records a successful build: the source content hash it was built from
|
|
748
|
-
*
|
|
700
|
+
* Records a successful build: the source content hash it was built from, the
|
|
701
|
+
* traces of its build-input closure as they stand now, and the exact files it
|
|
702
|
+
* emitted. Callers write it only after the build exited 0 and, inside a
|
|
703
|
+
* cycle, only once the whole component settled.
|
|
749
704
|
*/
|
|
750
705
|
export async function writeBuildOkStamp(packageDir,
|
|
751
706
|
/** The runtime whose logger reports a build input the hash could only read as bytes. */
|
|
752
|
-
runtime, packages,
|
|
707
|
+
runtime, packages, context = createTraceContext()) {
|
|
753
708
|
const host = await resolveArtifactHost(runtime);
|
|
754
709
|
const pkg = await readPackageManifest(host, packageDir);
|
|
755
710
|
const artifactsDirectory = host.paths.join(pkg.dir, resolveArtifactsDir(pkg));
|
|
711
|
+
forgetArtifacts(context, pkg.name);
|
|
756
712
|
const stamp = {
|
|
757
|
-
sourceHash: await
|
|
758
|
-
|
|
713
|
+
sourceHash: await ownSourceHash(host, pkg, context),
|
|
714
|
+
traces: await currentTraces(host, pkg, packages, context),
|
|
715
|
+
outputHash: await outputHashOf(host, pkg, context),
|
|
759
716
|
toolVersions: declaredToolVersions(pkg.packageJson),
|
|
760
717
|
files: await listEmittedFiles(host, artifactsDirectory)
|
|
761
718
|
};
|
|
@@ -769,7 +726,8 @@ runtime, packages, dependencySourceHashes = new Map()) {
|
|
|
769
726
|
/** The admitted shape of a build stamp, parsed once at the boundary. */
|
|
770
727
|
const BuildStampSchema = z.object({
|
|
771
728
|
sourceHash: z.string(),
|
|
772
|
-
|
|
729
|
+
traces: z.record(z.string(), z.string()),
|
|
730
|
+
outputHash: z.string(),
|
|
773
731
|
toolVersions: z.record(z.string(), z.string()),
|
|
774
732
|
files: z.array(z.object({ path: z.string(), size: z.number() }).loose())
|
|
775
733
|
}).loose();
|
|
@@ -777,11 +735,12 @@ function parseBuildStamp(text) {
|
|
|
777
735
|
const parsed = BuildStampSchema.safeParse(JSON.parse(text));
|
|
778
736
|
if (parsed.success) {
|
|
779
737
|
// The schema admits extra keys from older and newer writers; the stamp
|
|
780
|
-
// is the
|
|
781
|
-
const { sourceHash,
|
|
738
|
+
// is the five typed fields, constructed explicitly instead of cast.
|
|
739
|
+
const { sourceHash, traces, outputHash, toolVersions, files } = parsed.data;
|
|
782
740
|
return {
|
|
783
741
|
sourceHash,
|
|
784
|
-
|
|
742
|
+
traces,
|
|
743
|
+
outputHash,
|
|
785
744
|
toolVersions,
|
|
786
745
|
files: files.map((file) => ({ path: file.path, size: file.size }))
|
|
787
746
|
};
|
|
@@ -793,13 +752,47 @@ function parseBuildStamp(text) {
|
|
|
793
752
|
/**
|
|
794
753
|
* The single freshness decision every caller shares: `assertDeployWorkspacePreflight`
|
|
795
754
|
* uses it to refuse a deploy over stale inputs, and the build orchestration
|
|
796
|
-
* below uses the same predicate to skip a closure member
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
* no stamp (never built, failed build,
|
|
755
|
+
* below uses the same predicate to skip or restore a closure member. Fresh
|
|
756
|
+
* means the package's own build is valid ({@link checkOwnBuild}) and so is
|
|
757
|
+
* every package in its build-input closure: a trace only vouches for a
|
|
758
|
+
* dependency whose own build still matches its sources. There is no
|
|
759
|
+
* timestamp heuristic: a package with no stamp (never built, failed build,
|
|
760
|
+
* interrupted build) is not fresh.
|
|
801
761
|
*/
|
|
802
|
-
async function checkWorkspacePackageFreshness(host, pkg, packages,
|
|
762
|
+
async function checkWorkspacePackageFreshness(host, pkg, packages, context = createTraceContext()) {
|
|
763
|
+
const own = await checkOwnBuild(host, pkg, packages, context);
|
|
764
|
+
if (!own.fresh || !packages)
|
|
765
|
+
return own;
|
|
766
|
+
for (const name of await buildInputClosure(host, pkg, packages, context)) {
|
|
767
|
+
const dependency = packages.get(name);
|
|
768
|
+
if (!hasBuildArtifacts(dependency))
|
|
769
|
+
continue;
|
|
770
|
+
const verdict = await checkOwnBuild(host, dependency, packages, context);
|
|
771
|
+
if (!verdict.fresh) {
|
|
772
|
+
return {
|
|
773
|
+
...own,
|
|
774
|
+
fresh: false,
|
|
775
|
+
reason: `workspace dependency ${name} changed since the last successful build (${verdict.reason})`
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return own;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* One package's own build, judged alone: a build-success stamp exists, its
|
|
783
|
+
* source hash equals the hash of the package's current build inputs, every
|
|
784
|
+
* closure trace it recorded equals that package's trace now, and the emitted
|
|
785
|
+
* files are still the recorded bytes.
|
|
786
|
+
*/
|
|
787
|
+
function checkOwnBuild(host, pkg, packages, context) {
|
|
788
|
+
let verdict = context.verdicts.get(pkg.name);
|
|
789
|
+
if (!verdict) {
|
|
790
|
+
verdict = judgeOwnBuild(host, pkg, packages, context);
|
|
791
|
+
context.verdicts.set(pkg.name, verdict);
|
|
792
|
+
}
|
|
793
|
+
return verdict;
|
|
794
|
+
}
|
|
795
|
+
async function judgeOwnBuild(host, pkg, packages, context) {
|
|
803
796
|
const resolvedPackageRoot = await realpathChecked(host.files, pkg.dir);
|
|
804
797
|
const artifactsDir = resolveArtifactsDir(pkg);
|
|
805
798
|
const artifactsDirectory = host.paths.join(resolvedPackageRoot, artifactsDir);
|
|
@@ -825,16 +818,12 @@ async function checkWorkspacePackageFreshness(host, pkg, packages, ownHashes = n
|
|
|
825
818
|
return result(`${artifactsDir}/${BUILD_STAMP_FILENAME} is not a valid build stamp`);
|
|
826
819
|
if (stamp.files.length === 0)
|
|
827
820
|
return result(`${artifactsDir}/${BUILD_STAMP_FILENAME} lists no emitted files`);
|
|
828
|
-
if (stamp.sourceHash !== (await
|
|
821
|
+
if (stamp.sourceHash !== (await ownSourceHash(host, pkg, context))) {
|
|
829
822
|
return result(`package sources changed since the last successful build`);
|
|
830
823
|
}
|
|
831
|
-
const
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
if (stamp.dependencies[name] !== currentDependencies[name]) {
|
|
835
|
-
return result(`workspace dependency ${name} changed since the last successful build`);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
824
|
+
const changed = changedTrace(stamp.traces, await currentTraces(host, pkg, packages, context));
|
|
825
|
+
if (changed !== undefined)
|
|
826
|
+
return result(`workspace dependency ${changed} changed since the last successful build`);
|
|
838
827
|
for (const file of stamp.files) {
|
|
839
828
|
const path = host.paths.join(artifactsDirectory, file.path);
|
|
840
829
|
if (!isPathWithin(host, artifactsDirectory, path)) {
|
|
@@ -854,6 +843,9 @@ async function checkWorkspacePackageFreshness(host, pkg, packages, ownHashes = n
|
|
|
854
843
|
return result(`emitted file changed size (${file.size} -> ${size}): ${artifactsDir}/${file.path}`);
|
|
855
844
|
}
|
|
856
845
|
}
|
|
846
|
+
if (stamp.outputHash !== (await outputHashOf(host, pkg, context))) {
|
|
847
|
+
return result(`${artifactsDir} no longer holds the bytes its build emitted`);
|
|
848
|
+
}
|
|
857
849
|
const buildInfoReason = await typeScriptBuildInfoRefusal(host, host.paths.join(artifactsDirectory, TYPESCRIPT_BUILD_INFO_FILENAME));
|
|
858
850
|
if (buildInfoReason)
|
|
859
851
|
return result(buildInfoReason);
|
|
@@ -888,7 +880,7 @@ async function typeScriptBuildInfoRefusal(host, tsBuildInfoPath) {
|
|
|
888
880
|
}
|
|
889
881
|
return undefined;
|
|
890
882
|
}
|
|
891
|
-
async function buildPackage(host, workspaceRoot, pkg, execution,
|
|
883
|
+
async function buildPackage(host, workspaceRoot, pkg, execution, onLine) {
|
|
892
884
|
const scriptName = resolveBuildScriptName(pkg);
|
|
893
885
|
onLine?.('stdout', `[workspace-artifacts] building ${pkg.name}`);
|
|
894
886
|
const diagnostics = createWorkspaceDiagnosticTail();
|
|
@@ -920,7 +912,6 @@ async function buildPackage(host, workspaceRoot, pkg, execution, packages, depen
|
|
|
920
912
|
throw new Error(`Workspace artifact build failed for ${pkg.name} (exit=${result.code ?? 'unknown'})\n` +
|
|
921
913
|
`Compiler diagnostics (redacted tail):\n${diagnostics.render()}`);
|
|
922
914
|
}
|
|
923
|
-
await writeBuildOkStamp(pkg.dir, host.runtime, packages, dependencySourceHashes);
|
|
924
915
|
}
|
|
925
916
|
/** A failed build leaves neither its stamp nor its incremental state, so the next run rebuilds from scratch. */
|
|
926
917
|
async function invalidateFailedBuild(host, pkg) {
|
|
@@ -933,11 +924,10 @@ async function invalidateFailedBuild(host, pkg) {
|
|
|
933
924
|
* Independent branches of the dependency graph (declared or source-discovered
|
|
934
925
|
* edges, the same edge set the planner used) run concurrently; a mutual-import
|
|
935
926
|
* cycle — the class `orderCycleMembersByWorkspaceDependencies` already
|
|
936
|
-
* resolves into one contiguous
|
|
937
|
-
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
* scheduling.
|
|
927
|
+
* resolves into one contiguous run — is handed to `run` whole, in the plan's
|
|
928
|
+
* existing order, once every component it depends on has settled. `run`
|
|
929
|
+
* decides which members actually spawn a build; this function only owns the
|
|
930
|
+
* safe scheduling.
|
|
941
931
|
*/
|
|
942
932
|
async function runWorkspaceBuildPlan(host, plan, packages, concurrency, run) {
|
|
943
933
|
if (plan.length === 0)
|
|
@@ -977,8 +967,7 @@ async function runWorkspaceBuildPlan(host, plan, packages, concurrency, run) {
|
|
|
977
967
|
// combined rejection and completion always advances the scheduler.
|
|
978
968
|
const runComponent = async (index) => {
|
|
979
969
|
const outcome = await settle(async () => {
|
|
980
|
-
|
|
981
|
-
await run(pkg);
|
|
970
|
+
await run(components[index].members);
|
|
982
971
|
});
|
|
983
972
|
if (!outcome.ok)
|
|
984
973
|
fail(outcome.cause);
|
|
@@ -1060,37 +1049,164 @@ function groupWorkspaceBuildPlanIntoComponents(plan, dependenciesOf) {
|
|
|
1060
1049
|
dependsOn: dependsOnByComponent.get(component) ?? new Set()
|
|
1061
1050
|
}));
|
|
1062
1051
|
}
|
|
1063
|
-
/** Independent workspace package builds run this many at a time
|
|
1052
|
+
/** Independent workspace package builds run this many at a time when the host reports no resources. */
|
|
1064
1053
|
const DEFAULT_WORKSPACE_BUILD_CONCURRENCY = 4;
|
|
1054
|
+
/** One package build (tsc over the dependency graph's declarations) holds about this much memory. */
|
|
1055
|
+
const WORKSPACE_BUILD_MEMORY_BYTES = 4 * 1024 ** 3;
|
|
1056
|
+
/**
|
|
1057
|
+
* Size the build to the host it runs on: half its cores, no more builds than its memory
|
|
1058
|
+
* holds, never below the default.
|
|
1059
|
+
*/
|
|
1060
|
+
function workspaceBuildConcurrency(host) {
|
|
1061
|
+
const resources = host.runtime.environment?.systemResources?.();
|
|
1062
|
+
if (!resources)
|
|
1063
|
+
return DEFAULT_WORKSPACE_BUILD_CONCURRENCY;
|
|
1064
|
+
const byCpu = Math.floor(resources.cpuCount / 2);
|
|
1065
|
+
const byMemory = Math.floor(resources.totalMemoryBytes / WORKSPACE_BUILD_MEMORY_BYTES);
|
|
1066
|
+
return Math.max(DEFAULT_WORKSPACE_BUILD_CONCURRENCY, Math.min(byCpu, byMemory));
|
|
1067
|
+
}
|
|
1065
1068
|
async function buildWorkspacePlanWithFreshnessSkip(host, workspaceRoot, plan, packages, execution, onLine, artifactStore) {
|
|
1066
|
-
|
|
1067
|
-
const ownHashes = new Map();
|
|
1069
|
+
const context = createTraceContext();
|
|
1068
1070
|
const store = artifactStore ?? resolveWorkspaceArtifactStore(host.runtime, host.files, host.paths);
|
|
1069
1071
|
const tools = await installedToolVersions(host, workspaceRoot);
|
|
1070
|
-
await runWorkspaceBuildPlan(host, plan, packages,
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1072
|
+
await runWorkspaceBuildPlan(host, plan, packages, workspaceBuildConcurrency(host), (members) => settleWorkspaceComponent(host, workspaceRoot, members, packages, execution, context, store, tools, onLine));
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Settles one component (a lone package, or every member of an import cycle)
|
|
1076
|
+
* once everything it depends on has settled.
|
|
1077
|
+
*
|
|
1078
|
+
* Each member reuses a build of its current sources (the one on disk, else a
|
|
1079
|
+
* stored one) whose recorded traces agree with the settled packages below and
|
|
1080
|
+
* with the builds already chosen for earlier members; a member with none
|
|
1081
|
+
* builds. Builds read workspace dependencies from source, so a member built in
|
|
1082
|
+
* this run is right whatever its siblings' artifacts hold; a reused member is
|
|
1083
|
+
* right only if its traces still agree once the component's artifacts are in
|
|
1084
|
+
* place, so any that disagree rebuild, until none do. Each pass turns at least
|
|
1085
|
+
* one reuse into a build, so this ends. Stamps for the members built here are
|
|
1086
|
+
* written last, recording the settled traces.
|
|
1087
|
+
*/
|
|
1088
|
+
async function settleWorkspaceComponent(host, workspaceRoot, members, packages, execution, context, store, tools, onLine) {
|
|
1089
|
+
const memberNames = new Set(members.map((pkg) => pkg.name));
|
|
1090
|
+
const ownKeys = new Map();
|
|
1091
|
+
const options = new Map();
|
|
1092
|
+
for (const pkg of members) {
|
|
1093
|
+
const own = await ownSourceHash(host, pkg, context);
|
|
1094
|
+
const ownKey = host.sha256().update(`workspace-artifacts\0v2\0${own}\0${tools}`).digestHex();
|
|
1095
|
+
ownKeys.set(pkg.name, ownKey);
|
|
1096
|
+
const current = await currentTraces(host, pkg, packages, context);
|
|
1097
|
+
// Outside the component everything has settled, so a reusable build agrees with it exactly.
|
|
1098
|
+
const agreesBelow = (stamp) => {
|
|
1099
|
+
if (stamp.sourceHash !== own)
|
|
1100
|
+
return false;
|
|
1101
|
+
const names = new Set([...Object.keys(stamp.traces), ...Object.keys(current)]);
|
|
1102
|
+
return [...names].every((name) => memberNames.has(name) || stamp.traces[name] === current[name]);
|
|
1103
|
+
};
|
|
1104
|
+
const reusable = [];
|
|
1105
|
+
const onDisk = await readStampOnDisk(host, pkg);
|
|
1106
|
+
if (onDisk && onDisk.outputHash === (await outputHashOf(host, pkg, context)) && agreesBelow(onDisk)) {
|
|
1107
|
+
reusable.push({ stamp: onDisk });
|
|
1108
|
+
}
|
|
1109
|
+
for (const entry of await store.entries(pkg.name, ownKey, BUILD_STAMP_FILENAME)) {
|
|
1110
|
+
const stamp = parseStoredStamp(entry.text);
|
|
1111
|
+
if (stamp && agreesBelow(stamp))
|
|
1112
|
+
reusable.push({ stamp, key: entry.key });
|
|
1113
|
+
}
|
|
1114
|
+
options.set(pkg.name, reusable);
|
|
1115
|
+
}
|
|
1116
|
+
// Inside the component, drop a build that traced a sibling output no reusable
|
|
1117
|
+
// build of that sibling has. A sibling with none builds; its output is checked
|
|
1118
|
+
// after it builds.
|
|
1119
|
+
const satisfiable = (build) => Object.entries(build.stamp.traces).every(([name, trace]) => {
|
|
1120
|
+
const sibling = memberNames.has(name) ? options.get(name) : [];
|
|
1121
|
+
return sibling.length === 0 || sibling.some((other) => trace === `output:${other.stamp.outputHash}`);
|
|
1122
|
+
});
|
|
1123
|
+
for (let pruned = true; pruned;) {
|
|
1124
|
+
pruned = false;
|
|
1125
|
+
for (const pkg of members) {
|
|
1126
|
+
const reusable = options.get(pkg.name);
|
|
1127
|
+
const kept = reusable.filter(satisfiable);
|
|
1128
|
+
if (kept.length === reusable.length)
|
|
1129
|
+
continue;
|
|
1130
|
+
options.set(pkg.name, kept);
|
|
1131
|
+
pruned = true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
// Choose in plan order, each agreeing both ways with the builds chosen before it.
|
|
1135
|
+
const chosen = new Map();
|
|
1136
|
+
for (const pkg of members) {
|
|
1137
|
+
const pick = options.get(pkg.name).find((build) => [...chosen].every(([name, other]) => (build.stamp.traces[name] === undefined || build.stamp.traces[name] === `output:${other.stamp.outputHash}`) &&
|
|
1138
|
+
(other.stamp.traces[pkg.name] === undefined || other.stamp.traces[pkg.name] === `output:${build.stamp.outputHash}`)));
|
|
1139
|
+
if (pick)
|
|
1140
|
+
chosen.set(pkg.name, pick);
|
|
1141
|
+
}
|
|
1142
|
+
const built = new Set();
|
|
1143
|
+
const build = async (pkg) => {
|
|
1144
|
+
await buildPackage(host, workspaceRoot, pkg, execution, onLine);
|
|
1145
|
+
forgetArtifacts(context, pkg.name);
|
|
1146
|
+
built.add(pkg.name);
|
|
1147
|
+
};
|
|
1148
|
+
for (const pkg of members) {
|
|
1149
|
+
const reuse = chosen.get(pkg.name);
|
|
1150
|
+
if (!reuse)
|
|
1151
|
+
continue;
|
|
1152
|
+
if (reuse.key === undefined) {
|
|
1153
|
+
onLine?.('stdout', `[workspace-artifacts] skip ${pkg.name}: ${resolveArtifactsDir(pkg)} is already fresh`);
|
|
1154
|
+
continue;
|
|
1082
1155
|
}
|
|
1083
|
-
if (await store.restore(pkg.name, key,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
onLine?.('stdout', `[workspace-artifacts] restore ${pkg.name}: built before at this source hash`);
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
onLine?.('stdout', `[workspace-artifacts] rebuild ${pkg.name}: the stored build is not fresh (${restored.reason})`);
|
|
1156
|
+
if (await store.restore(pkg.name, reuse.key, host.paths.join(pkg.dir, resolveArtifactsDir(pkg)))) {
|
|
1157
|
+
forgetArtifacts(context, pkg.name);
|
|
1158
|
+
onLine?.('stdout', `[workspace-artifacts] restore ${pkg.name}: built before from these sources and inputs`);
|
|
1090
1159
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1160
|
+
else {
|
|
1161
|
+
chosen.delete(pkg.name);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
for (const pkg of members)
|
|
1165
|
+
if (!chosen.has(pkg.name))
|
|
1166
|
+
await build(pkg);
|
|
1167
|
+
for (;;) {
|
|
1168
|
+
const disagreeing = [];
|
|
1169
|
+
for (const pkg of members) {
|
|
1170
|
+
if (built.has(pkg.name))
|
|
1171
|
+
continue;
|
|
1172
|
+
const verdict = await checkOwnBuild(host, pkg, packages, context);
|
|
1173
|
+
if (verdict.fresh)
|
|
1174
|
+
continue;
|
|
1175
|
+
onLine?.('stdout', `[workspace-artifacts] rebuild ${pkg.name}: ${verdict.reason}`);
|
|
1176
|
+
disagreeing.push(pkg);
|
|
1177
|
+
}
|
|
1178
|
+
if (disagreeing.length === 0)
|
|
1179
|
+
break;
|
|
1180
|
+
for (const pkg of disagreeing)
|
|
1181
|
+
await build(pkg);
|
|
1182
|
+
}
|
|
1183
|
+
for (const pkg of members) {
|
|
1184
|
+
const artifactsDirectory = host.paths.join(pkg.dir, resolveArtifactsDir(pkg));
|
|
1185
|
+
const stamp = built.has(pkg.name)
|
|
1186
|
+
? await writeBuildOkStamp(pkg.dir, host.runtime, packages, context)
|
|
1187
|
+
: chosen.get(pkg.name).stamp;
|
|
1188
|
+
// A stored entry is the build for its key; a fresh build or a reused local one is stored for other checkouts.
|
|
1189
|
+
if (!built.has(pkg.name) && chosen.get(pkg.name).key !== undefined)
|
|
1190
|
+
continue;
|
|
1191
|
+
await store.save(pkg.name, `${ownKeys.get(pkg.name)}/${tracesKey(host, stamp.traces)}`, artifactsDirectory);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function tracesKey(host, traces) {
|
|
1195
|
+
const hash = host.sha256().update('traces');
|
|
1196
|
+
for (const name of Object.keys(traces).sort())
|
|
1197
|
+
hash.update(`\0${name}\0${traces[name]}`);
|
|
1198
|
+
return hash.digestHex();
|
|
1199
|
+
}
|
|
1200
|
+
async function readStampOnDisk(host, pkg) {
|
|
1201
|
+
const path = host.paths.join(pkg.dir, resolveArtifactsDir(pkg), BUILD_STAMP_FILENAME);
|
|
1202
|
+
if (!(await host.files.exists(path)))
|
|
1203
|
+
return undefined;
|
|
1204
|
+
return parseStoredStamp(await readText(host.files, path));
|
|
1205
|
+
}
|
|
1206
|
+
/** A stamp read from disk or the store; one that does not parse is no candidate. */
|
|
1207
|
+
function parseStoredStamp(text) {
|
|
1208
|
+
const parsed = settleNow(() => parseBuildStamp(text));
|
|
1209
|
+
return parsed.ok ? parsed.value : undefined;
|
|
1094
1210
|
}
|
|
1095
1211
|
/**
|
|
1096
1212
|
* The resolved versions of the toolchain a build runs (from the workspace install),
|
|
@@ -1104,11 +1220,6 @@ async function installedToolVersions(host, workspaceRoot) {
|
|
|
1104
1220
|
}
|
|
1105
1221
|
return JSON.stringify(versions);
|
|
1106
1222
|
}
|
|
1107
|
-
/** A package build's store identity: its effective source hash and the installed toolchain. */
|
|
1108
|
-
async function artifactStoreKey(host, pkg, packages, ownHashes, tools) {
|
|
1109
|
-
const effective = await effectiveSourceHash(host, pkg, packages, ownHashes);
|
|
1110
|
-
return host.sha256().update(`workspace-artifacts\0v1\0${effective}\0${tools}`).digestHex();
|
|
1111
|
-
}
|
|
1112
1223
|
async function discoverSvelteKitApps(host, workspaceRoot) {
|
|
1113
1224
|
const appsRoot = host.paths.join(workspaceRoot, 'apps');
|
|
1114
1225
|
if (!(await host.files.exists(appsRoot)))
|
|
@@ -1743,13 +1854,13 @@ export async function assertDeployWorkspacePreflight({ workspaceRoot, runtime, a
|
|
|
1743
1854
|
if (artifactPolicy === 'planned-build') {
|
|
1744
1855
|
return { warnings };
|
|
1745
1856
|
}
|
|
1746
|
-
const
|
|
1857
|
+
const context = createTraceContext();
|
|
1747
1858
|
for (const pkg of packageClosure.filter(hasBuildArtifacts)) {
|
|
1748
1859
|
// Single source of truth: the same predicate that decides whether the
|
|
1749
1860
|
// build orchestration below may skip this package's build (see
|
|
1750
1861
|
// `checkWorkspacePackageFreshness`) decides whether this postflight
|
|
1751
1862
|
// assertion accepts it as a valid deploy input.
|
|
1752
|
-
const freshness = await checkWorkspacePackageFreshness(host, pkg, workspacePackages,
|
|
1863
|
+
const freshness = await checkWorkspacePackageFreshness(host, pkg, workspacePackages, context);
|
|
1753
1864
|
if (freshness.fresh)
|
|
1754
1865
|
continue;
|
|
1755
1866
|
if (freshness.artifactsMissing) {
|