@fungi.computer/oyster 0.0.1 → 0.2.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.
- package/README.md +228 -176
- package/dist/agent-avatars.d.ts +103 -0
- package/dist/agent-avatars.js +66 -0
- package/dist/agents.d.ts +12 -2
- package/dist/agents.js +39 -2
- package/dist/app-builds.d.ts +76 -17
- package/dist/app-builds.js +207 -63
- package/dist/app-sources.d.ts +7 -22
- package/dist/app-sources.js +49 -14
- package/dist/apps/gate.d.ts +7 -7
- package/dist/apps/gate.js +6 -6
- package/dist/apps/installation.d.ts +35 -35
- package/dist/apps/installation.js +20 -19
- package/dist/build/artifact-writer.d.ts +40 -0
- package/dist/build/artifact-writer.js +181 -0
- package/dist/build/build-jobs.js +7 -1
- package/dist/build/build-policy.d.ts +10 -0
- package/dist/build/build-policy.js +10 -0
- package/dist/build/build-step.d.ts +12 -1
- package/dist/build/build-step.js +9 -1
- package/dist/build/candidate-builder.d.ts +68 -0
- package/dist/build/candidate-builder.js +91 -0
- package/dist/build/candidate-cli.d.ts +1 -0
- package/dist/build/candidate-cli.js +119 -0
- package/dist/build/candidate-cli.mjs +28220 -0
- package/dist/build/content-objects.d.ts +37 -0
- package/dist/build/content-objects.js +53 -0
- package/dist/build/effect.d.ts +18 -0
- package/dist/build/effect.js +52 -12
- package/dist/build/git-environment.d.ts +3 -0
- package/dist/build/git-environment.js +21 -9
- package/dist/build/local-build.d.ts +13 -3
- package/dist/build/local-build.js +149 -99
- package/dist/build/snapshot-package-store.d.ts +10 -0
- package/dist/build/snapshot-package-store.js +15 -0
- package/dist/build/snapshot-program-scripts.js +1 -1
- package/dist/build/snapshot-programs/snapshot-path.d.ts +2 -0
- package/dist/build/snapshot-programs/snapshot-path.js +6 -0
- package/dist/build/submission-contract.d.ts +461 -17
- package/dist/build/submission-contract.js +136 -26
- package/dist/build/workspace-build.d.ts +7 -2
- package/dist/build/workspace-build.js +55 -11
- package/dist/build-contract.d.ts +3 -0
- package/dist/build-contract.js +3 -0
- package/dist/build-egress.d.ts +62 -0
- package/dist/build-egress.js +148 -0
- package/dist/build.d.ts +1 -0
- package/dist/build.js +1 -0
- package/dist/cloudflare.js +4 -4
- package/dist/frame-binding-wire-script.js +1 -1
- package/dist/frame-binding-wire.d.ts +1 -1
- package/dist/frame-binding-wire.js +1 -1
- package/dist/git-journal.js +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/installation-consent.d.ts +3 -1
- package/dist/installation-consent.js +2 -0
- package/dist/mcp-owner.js +20 -8
- package/dist/pack.d.ts +6 -3
- package/dist/pack.js +28 -11
- package/dist/page/gill-observation.d.ts +4 -0
- package/dist/page/gill-observation.js +31 -0
- package/dist/page/mount.d.ts +4 -8
- package/dist/page/window-launch.d.ts +18 -0
- package/dist/page/window-launch.js +11 -0
- package/dist/page-script.js +4 -4
- package/dist/release.d.ts +11 -0
- package/dist/release.js +4 -1
- package/dist/shell/app-directory.d.ts +50 -45
- package/dist/shell/app-directory.js +19 -18
- package/dist/shell/shell-module.d.ts +26 -2
- package/dist/shell/shell-module.js +51 -2
- package/dist/shell/window-manager.d.ts +5 -1
- package/dist/shell/window-manager.js +21 -2
- package/dist/shell/workbench-directory.d.ts +332 -0
- package/dist/shell/workbench-directory.js +35 -0
- package/dist/shell/workbench-host-types.d.ts +1 -1
- package/dist/shell/workbench-host.js +5 -0
- package/dist/shell/workbench-model.d.ts +3 -1
- package/dist/source/index.d.ts +6 -7
- package/dist/source/index.js +6 -7
- package/dist/source/template-push.d.ts +33 -0
- package/dist/source/template-push.js +223 -0
- package/dist/source/template-repository.d.ts +2 -0
- package/dist/source/template-repository.js +13 -0
- package/dist/source/{template-write.d.ts → template-seed.d.ts} +19 -20
- package/dist/source/template-seed.js +80 -0
- package/dist/standalone/agent-directory.d.ts +4 -0
- package/dist/standalone/agent-directory.js +4 -0
- package/dist/standalone/agents.d.ts +227 -0
- package/dist/standalone/agents.js +834 -0
- package/dist/standalone/app-builds-binding.d.ts +36 -0
- package/dist/standalone/app-builds-binding.js +119 -0
- package/dist/standalone/app-set.d.ts +91 -0
- package/dist/standalone/app-set.js +395 -0
- package/dist/standalone/app-source.d.ts +41 -0
- package/dist/standalone/app-source.js +59 -0
- package/dist/standalone/builds.d.ts +18 -0
- package/dist/standalone/builds.js +101 -0
- package/dist/standalone/computer-egress.d.ts +37 -0
- package/dist/standalone/computer-egress.js +111 -0
- package/dist/standalone/computer.d.ts +122 -0
- package/dist/standalone/computer.js +714 -0
- package/dist/standalone/install-apps-cli.d.ts +2 -0
- package/dist/standalone/install-apps-cli.js +126 -0
- package/dist/standalone/instance.d.ts +35 -0
- package/dist/standalone/instance.js +74 -0
- package/dist/standalone/media.d.ts +76 -0
- package/dist/standalone/media.js +227 -0
- package/dist/standalone/preview.d.ts +51 -0
- package/dist/standalone/preview.js +167 -0
- package/dist/standalone/registry.d.ts +102 -0
- package/dist/standalone/registry.js +193 -0
- package/dist/standalone/tenancy.d.ts +42 -0
- package/dist/standalone/tenancy.js +241 -0
- package/dist/standalone/worker.d.ts +55 -0
- package/dist/standalone/worker.js +147 -0
- package/dist/standalone/workflow-authority-readers.d.ts +16 -0
- package/dist/standalone/workflow-authority-readers.js +35 -0
- package/dist/standalone/workflow-binding.d.ts +216 -0
- package/dist/standalone/workflow-binding.js +217 -0
- package/dist/standalone/workflow-events.d.ts +46 -0
- package/dist/standalone/workflow-events.js +39 -0
- package/dist/standalone/workflow-git-capture.d.ts +7 -0
- package/dist/standalone/workflow-git-capture.js +62 -0
- package/dist/standalone/workflow-host.d.ts +955 -0
- package/dist/standalone/workflow-host.js +778 -0
- package/dist/standalone/workflow-node-capture.d.ts +13 -0
- package/dist/standalone/workflow-node-capture.js +72 -0
- package/dist/standalone/workflow-root-authority.d.ts +27 -0
- package/dist/standalone/workflow-root-authority.js +87 -0
- package/dist/standalone/workflow-schedules.d.ts +52 -0
- package/dist/standalone/workflow-schedules.js +40 -0
- package/dist/standalone/workflow-trigger-host.d.ts +1716 -0
- package/dist/standalone/workflow-trigger-host.js +52 -0
- package/dist/standalone/workflows.d.ts +713 -0
- package/dist/standalone/workflows.js +554 -0
- package/dist/tenancy/auth0.d.ts +64 -0
- package/dist/tenancy/auth0.js +198 -0
- package/dist/tenancy/cloudflare/rate-limit.d.ts +8 -0
- package/dist/tenancy/cloudflare/rate-limit.js +19 -0
- package/dist/tenancy/cloudflare/registry.d.ts +50 -0
- package/dist/tenancy/cloudflare/registry.js +301 -0
- package/dist/tenancy/cloudflare.d.ts +2 -0
- package/dist/tenancy/cloudflare.js +2 -0
- package/dist/tenancy/cold-directory.d.ts +13 -0
- package/dist/tenancy/cold-directory.js +43 -0
- package/dist/tenancy/contracts.d.ts +195 -0
- package/dist/tenancy/contracts.js +1 -0
- package/dist/tenancy/index.d.ts +19 -0
- package/dist/tenancy/index.js +156 -0
- package/dist/tenancy/owner-key.d.ts +26 -0
- package/dist/tenancy/owner-key.js +63 -0
- package/dist/tenancy/oyster.d.ts +4 -0
- package/dist/tenancy/oyster.js +25 -0
- package/dist/tenancy/registry-record.d.ts +7 -0
- package/dist/tenancy/registry-record.js +44 -0
- package/dist/tenancy/registry.d.ts +27 -0
- package/dist/tenancy/registry.js +138 -0
- package/dist/tenancy/resolve.d.ts +7 -0
- package/dist/tenancy/resolve.js +64 -0
- package/dist/workers-git.js +1 -4
- package/dist/workers.d.ts +2 -2
- package/dist/workers.js +3 -1
- package/docs/api/build-contract.md +829 -32
- package/docs/api/build.md +253 -17
- package/docs/api/modules.md +40 -32
- package/docs/api/pack.md +6 -3
- package/docs/api/shell/app-directory.md +267 -0
- package/docs/api/shell/shell-module.md +64 -3
- package/docs/api/shell/workbench-directory.md +119 -0
- package/docs/api/shell/workbench.md +19 -0
- package/docs/api/shell.md +127 -0
- package/docs/api/tenancy/auth0.md +110 -0
- package/docs/api/tenancy/cloudflare.md +221 -0
- package/docs/api/tenancy/cold-directory.md +46 -0
- package/docs/api/tenancy/owner-key.md +63 -0
- package/docs/api/tenancy/registry.md +77 -0
- package/docs/api/tenancy/resolve.md +69 -0
- package/docs/api/tenancy.md +430 -0
- package/docs/api/workers-git.md +140 -21
- package/docs/api/workers.md +15 -2
- package/docs/site/agent-quickstart.mdx +52 -55
- package/examples/standalone/worker.ts +7 -14
- package/examples/standalone/wrangler.jsonc +67 -6
- package/package.json +78 -25
- package/dist/build/r2-artifact-writer.d.ts +0 -31
- package/dist/build/r2-artifact-writer.js +0 -179
- package/dist/source/template-write.js +0 -344
- package/docs/media/architecture.mmd +0 -32
- package/docs/media/architecture.png +0 -0
- package/docs/media/bonsai-network.png +0 -0
- package/docs/media/bonsai-repository.png +0 -0
- package/examples/git/worker.ts +0 -39
- package/examples/git/wrangler.jsonc +0 -32
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
export declare class TeamCandidateBuildError extends Error {
|
|
3
|
+
readonly code: "invalid_recipe" | "output_limit" | "report_failed";
|
|
4
|
+
constructor(code: "invalid_recipe" | "output_limit" | "report_failed");
|
|
5
|
+
}
|
|
6
|
+
export declare const teamCandidateConfigurationSchema: Schema.Struct<{
|
|
7
|
+
/** The build policy the host expects; a builder that predates it refuses
|
|
8
|
+
* the request, and this one refuses any other. */
|
|
9
|
+
readonly buildPolicy: Schema.Literal<"package-json-fungi-build-v2">;
|
|
10
|
+
readonly appId: Schema.refine<import("@fungi.computer/puffball").AppId, Schema.String>;
|
|
11
|
+
readonly sourceRevision: Schema.refine<import("@fungi.computer/puffball").SourceRevision, Schema.String>;
|
|
12
|
+
readonly recipe: Schema.Struct<{
|
|
13
|
+
readonly kind: Schema.optional<Schema.Literals<readonly ["bundled-html", "source-html"]>>;
|
|
14
|
+
readonly bundle: Schema.Literal<true>;
|
|
15
|
+
readonly entry: Schema.String;
|
|
16
|
+
readonly format: Schema.Literals<readonly ["html", "esm", "assets"]>;
|
|
17
|
+
readonly assets: Schema.optional<Schema.Struct<{
|
|
18
|
+
readonly maxManifestBytes: Schema.Number;
|
|
19
|
+
readonly maxObjects: Schema.Number;
|
|
20
|
+
readonly maxObjectBytes: Schema.Number;
|
|
21
|
+
readonly maxTotalBytes: Schema.Number;
|
|
22
|
+
}>>;
|
|
23
|
+
readonly assetBase: Schema.optionalKey<Schema.String>;
|
|
24
|
+
readonly toolchain: Schema.$Record<Schema.String, Schema.String>;
|
|
25
|
+
readonly maxOutputBytes: Schema.Number;
|
|
26
|
+
readonly platform: Schema.Literals<readonly ["browser", "neutral"]>;
|
|
27
|
+
readonly backend: Schema.Union<readonly [Schema.Null, Schema.Struct<{
|
|
28
|
+
readonly entry: Schema.optional<Schema.String>;
|
|
29
|
+
readonly runtime: Schema.Struct<{
|
|
30
|
+
readonly kind: Schema.Literal<"durable-facet">;
|
|
31
|
+
readonly stateSchema: Schema.Number;
|
|
32
|
+
}>;
|
|
33
|
+
readonly compatibilityDate: Schema.String;
|
|
34
|
+
readonly exportName: Schema.String;
|
|
35
|
+
}>]>;
|
|
36
|
+
readonly target: Schema.Tuple<readonly [Schema.Literal<"es2022">]>;
|
|
37
|
+
}>;
|
|
38
|
+
readonly buildRecipeDigest: Schema.refine<import("@fungi.computer/puffball").Sha256Digest, Schema.String>;
|
|
39
|
+
}>;
|
|
40
|
+
/** Where a built candidate's manifest and its objects are written. */
|
|
41
|
+
export declare function teamCandidateLayout(outputPath: string): {
|
|
42
|
+
manifest: string;
|
|
43
|
+
object: (digest: string) => string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Runs inside the admitted Computer, using Oyster's captured builder.
|
|
47
|
+
* This is the one composition that runs a dependency's declared package
|
|
48
|
+
* build script; its executor refuses a recipe pinned under another policy.
|
|
49
|
+
* It writes a new directory: each built object under its digest, and the
|
|
50
|
+
* small manifest that names them, ready to upload one at a time.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildTeamCandidate(input: {
|
|
53
|
+
configuration: unknown;
|
|
54
|
+
repositoryPath: string;
|
|
55
|
+
/** A directory this build creates; it must not exist yet. */
|
|
56
|
+
outputPath: string;
|
|
57
|
+
/** Trusted Computer-local cache; never part of customer configuration. */
|
|
58
|
+
packageStoreDirectory?: string;
|
|
59
|
+
}): Promise<Readonly<{
|
|
60
|
+
digest: string;
|
|
61
|
+
bytes: number;
|
|
62
|
+
/** Milliseconds: capturing the cloned source, installing, compiling. */
|
|
63
|
+
phases: Readonly<{
|
|
64
|
+
capture: number;
|
|
65
|
+
install: number;
|
|
66
|
+
build: number;
|
|
67
|
+
}>;
|
|
68
|
+
}>>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import { appIdSchema, sourceRevisionSchema, sha256DigestSchema, } from "@fungi.computer/puffball/effect";
|
|
7
|
+
import { parseReleaseId } from "@fungi.computer/puffball";
|
|
8
|
+
import { capturedPinnedRecipeSchema, createCapturedCandidatePreparer, digestPinnedRecipe, encodeTeamAppCandidateManifest, TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES, workspaceBuildPolicy, } from "../build-contract.js";
|
|
9
|
+
import { createLocalCommitSourceCapture, createLocalBuildExecutor, stageBuildOutput, } from "../build.js";
|
|
10
|
+
export class TeamCandidateBuildError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(code) {
|
|
13
|
+
super(`Team candidate ${code}`);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.name = "TeamCandidateBuildError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const teamCandidateConfigurationSchema = Schema.Struct({
|
|
19
|
+
/** The build policy the host expects; a builder that predates it refuses
|
|
20
|
+
* the request, and this one refuses any other. */
|
|
21
|
+
buildPolicy: Schema.Literal(workspaceBuildPolicy),
|
|
22
|
+
appId: appIdSchema,
|
|
23
|
+
sourceRevision: sourceRevisionSchema,
|
|
24
|
+
recipe: capturedPinnedRecipeSchema,
|
|
25
|
+
buildRecipeDigest: sha256DigestSchema,
|
|
26
|
+
});
|
|
27
|
+
/** Where a built candidate's manifest and its objects are written. */
|
|
28
|
+
export function teamCandidateLayout(outputPath) {
|
|
29
|
+
return {
|
|
30
|
+
manifest: join(outputPath, "manifest.json"),
|
|
31
|
+
object: (digest) => join(outputPath, "objects", digest.replace(":", "-")),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Runs inside the admitted Computer, using Oyster's captured builder.
|
|
36
|
+
* This is the one composition that runs a dependency's declared package
|
|
37
|
+
* build script; its executor refuses a recipe pinned under another policy.
|
|
38
|
+
* It writes a new directory: each built object under its digest, and the
|
|
39
|
+
* small manifest that names them, ready to upload one at a time.
|
|
40
|
+
*/
|
|
41
|
+
export async function buildTeamCandidate(input) {
|
|
42
|
+
const config = Schema.decodeUnknownSync(teamCandidateConfigurationSchema, {
|
|
43
|
+
onExcessProperty: "error",
|
|
44
|
+
})(input.configuration);
|
|
45
|
+
if (digestPinnedRecipe(config.recipe) !== config.buildRecipeDigest)
|
|
46
|
+
throw new TeamCandidateBuildError("invalid_recipe");
|
|
47
|
+
const phases = { install: 0, build: 0 };
|
|
48
|
+
const onPhase = (phase, milliseconds) => {
|
|
49
|
+
phases[phase] += milliseconds;
|
|
50
|
+
};
|
|
51
|
+
const prepareStarted = performance.now();
|
|
52
|
+
const candidate = await Effect.runPromise(createCapturedCandidatePreparer(config, {
|
|
53
|
+
sourceCapture: createLocalCommitSourceCapture(input.repositoryPath, config.sourceRevision),
|
|
54
|
+
buildExecutor: createLocalBuildExecutor(input.packageStoreDirectory === undefined
|
|
55
|
+
? { packageScripts: "run", onPhase }
|
|
56
|
+
: {
|
|
57
|
+
packageScripts: "run",
|
|
58
|
+
packageStoreDirectory: input.packageStoreDirectory,
|
|
59
|
+
onPhase,
|
|
60
|
+
}),
|
|
61
|
+
}).prepareCandidate(parseReleaseId("rel_build")));
|
|
62
|
+
const prepared = performance.now() - prepareStarted;
|
|
63
|
+
const staged = stageBuildOutput(candidate.output);
|
|
64
|
+
const bytes = Buffer.from(encodeTeamAppCandidateManifest({
|
|
65
|
+
appId: candidate.appId,
|
|
66
|
+
output: staged.manifest,
|
|
67
|
+
provenance: candidate.provenance,
|
|
68
|
+
}));
|
|
69
|
+
if (bytes.byteLength > TEAM_APP_CANDIDATE_MANIFEST_MAX_BYTES)
|
|
70
|
+
throw new TeamCandidateBuildError("output_limit");
|
|
71
|
+
const layout = teamCandidateLayout(input.outputPath);
|
|
72
|
+
// A new directory: an earlier build's output is never mixed in.
|
|
73
|
+
await mkdir(input.outputPath, { mode: 0o700 });
|
|
74
|
+
await mkdir(join(input.outputPath, "objects"), { mode: 0o700 });
|
|
75
|
+
for (const object of staged.objects)
|
|
76
|
+
await writeFile(layout.object(object.digest), object.bytes, {
|
|
77
|
+
flag: "wx",
|
|
78
|
+
mode: 0o600,
|
|
79
|
+
});
|
|
80
|
+
await writeFile(layout.manifest, bytes, { flag: "wx", mode: 0o600 });
|
|
81
|
+
return {
|
|
82
|
+
digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
|
83
|
+
bytes: bytes.byteLength,
|
|
84
|
+
phases: {
|
|
85
|
+
// Snapshot capture and staging: everything but the install and compile.
|
|
86
|
+
capture: Math.max(0, prepared - phases.install - phases.build),
|
|
87
|
+
install: phases.install,
|
|
88
|
+
build: phases.build,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { releaseSchema } from "@fungi.computer/puffball/effect";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { candidateManifestObjects, teamAppCandidateManifestSchema, } from "../build-contract.js";
|
|
5
|
+
import { buildTeamCandidate, teamCandidateConfigurationSchema, teamCandidateLayout, TeamCandidateBuildError, } from "./candidate-builder.js";
|
|
6
|
+
/** Objects in flight at once; each is read from disk only while it uploads. */
|
|
7
|
+
const uploadConcurrency = 4;
|
|
8
|
+
/** Attempts per object when its connection drops before a response. */
|
|
9
|
+
const uploadAttempts = 3;
|
|
10
|
+
/** Report a failed build to the host's build output: its kind, never its text. */
|
|
11
|
+
function reportFailure(cause) {
|
|
12
|
+
process.stderr.write(`${JSON.stringify({
|
|
13
|
+
source: "oyster-candidate-builder",
|
|
14
|
+
failureKind: cause instanceof Error ? "Error" : "NonErrorThrown",
|
|
15
|
+
})}\n`);
|
|
16
|
+
}
|
|
17
|
+
const [configuration, repositoryPath, outputPath] = process.argv.slice(2);
|
|
18
|
+
if (!configuration || !repositoryPath || !outputPath)
|
|
19
|
+
throw new Error("Usage: team-candidate-builder CONFIG_BASE64 REPOSITORY OUTPUT_DIRECTORY");
|
|
20
|
+
try {
|
|
21
|
+
const { uploadGrant, ...config } = Schema.decodeUnknownSync(Schema.Struct({
|
|
22
|
+
...teamCandidateConfigurationSchema.fields,
|
|
23
|
+
uploadGrant: Schema.String,
|
|
24
|
+
}), { onExcessProperty: "error" })(JSON.parse(Buffer.from(configuration, "base64url").toString("utf8")));
|
|
25
|
+
const { phases, ...artifact } = await buildTeamCandidate({
|
|
26
|
+
configuration: config,
|
|
27
|
+
repositoryPath,
|
|
28
|
+
outputPath,
|
|
29
|
+
// Computer scratch survives captured snapshot cleanup, without a global or durable Workspace store.
|
|
30
|
+
packageStoreDirectory: "/var/tmp/fungi-app-package-store",
|
|
31
|
+
});
|
|
32
|
+
const reportUrl = process.env.TEAM_BUILD_REPORT;
|
|
33
|
+
if (!reportUrl)
|
|
34
|
+
throw new TeamCandidateBuildError("report_failed");
|
|
35
|
+
const layout = teamCandidateLayout(outputPath);
|
|
36
|
+
const manifestBytes = await readFile(layout.manifest);
|
|
37
|
+
const manifest = Schema.decodeUnknownSync(Schema.fromJsonString(teamAppCandidateManifestSchema))(new TextDecoder().decode(manifestBytes));
|
|
38
|
+
// Every object first, each streamed alone; the manifest only names them.
|
|
39
|
+
const uploadStarted = performance.now();
|
|
40
|
+
const pending = [...candidateManifestObjects(manifest.output)];
|
|
41
|
+
// A write-once PUT is safe to repeat, so a dropped connection is retried.
|
|
42
|
+
const put = async (object) => {
|
|
43
|
+
for (let attempt = 1;; attempt += 1) {
|
|
44
|
+
const stored = await fetch(`${reportUrl}/objects/${object.digest.replace(":", "-")}`, {
|
|
45
|
+
method: "PUT",
|
|
46
|
+
headers: {
|
|
47
|
+
"Content-Type": object.mediaType,
|
|
48
|
+
"X-Team-App-Build": uploadGrant,
|
|
49
|
+
},
|
|
50
|
+
body: await readFile(layout.object(object.digest)),
|
|
51
|
+
}).catch((cause) => {
|
|
52
|
+
if (attempt >= uploadAttempts)
|
|
53
|
+
throw cause;
|
|
54
|
+
return null;
|
|
55
|
+
});
|
|
56
|
+
await stored?.body?.cancel();
|
|
57
|
+
if (stored?.ok)
|
|
58
|
+
return;
|
|
59
|
+
if (stored !== null || attempt >= uploadAttempts)
|
|
60
|
+
throw new TeamCandidateBuildError("report_failed");
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const upload = async () => {
|
|
64
|
+
for (let object = pending.shift(); object; object = pending.shift())
|
|
65
|
+
await put(object);
|
|
66
|
+
};
|
|
67
|
+
await Promise.all(Array.from({ length: uploadConcurrency }, upload));
|
|
68
|
+
const candidate = await fetch(`${reportUrl}/candidate`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"X-Team-App-Build": uploadGrant,
|
|
73
|
+
},
|
|
74
|
+
body: manifestBytes,
|
|
75
|
+
});
|
|
76
|
+
if (!candidate.ok)
|
|
77
|
+
throw new TeamCandidateBuildError("report_failed");
|
|
78
|
+
const receipt = Schema.decodeUnknownSync(Schema.Struct({
|
|
79
|
+
kind: Schema.Literal("submitted"),
|
|
80
|
+
release: releaseSchema,
|
|
81
|
+
}))(await candidate.json());
|
|
82
|
+
const recorded = await fetch(`${reportUrl}/artifact`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
body: JSON.stringify(artifact),
|
|
86
|
+
});
|
|
87
|
+
if (!recorded.ok)
|
|
88
|
+
throw new TeamCandidateBuildError("report_failed");
|
|
89
|
+
await recorded.body?.cancel();
|
|
90
|
+
const uploaded = performance.now() - uploadStarted;
|
|
91
|
+
// The success detail reports where the build's time went; the owner reads
|
|
92
|
+
// it with the build. The clone ran before this program (TEAM_BUILD_CLONE_MS).
|
|
93
|
+
const cloned = Number(process.env.TEAM_BUILD_CLONE_MS ?? "0");
|
|
94
|
+
const status = await fetch(`${reportUrl}/status`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
status: "succeeded",
|
|
99
|
+
detail: JSON.stringify({
|
|
100
|
+
phasesMs: {
|
|
101
|
+
clone: Math.round((Number.isFinite(cloned) ? cloned : 0) + phases.capture),
|
|
102
|
+
install: Math.round(phases.install),
|
|
103
|
+
build: Math.round(phases.build),
|
|
104
|
+
upload: Math.round(uploaded),
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
if (!status.ok)
|
|
110
|
+
throw new TeamCandidateBuildError("report_failed");
|
|
111
|
+
await status.body?.cancel();
|
|
112
|
+
process.stdout.write(JSON.stringify({ ...artifact, release: receipt.release }));
|
|
113
|
+
}
|
|
114
|
+
catch (cause) {
|
|
115
|
+
reportFailure(cause);
|
|
116
|
+
// Customer source and provider messages can contain secrets; only expose the stage.
|
|
117
|
+
process.stderr.write("Team App source/build failed\n");
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
}
|