Make the image builds deterministic

This commit is contained in:
Saadi Myftija
2025-12-11 14:58:03 +01:00
parent b71bf89444
commit 5f8edb7924
9 changed files with 74 additions and 46 deletions
+2 -1
View File
@@ -92,10 +92,10 @@
"@opentelemetry/resources": "2.0.1",
"@opentelemetry/sdk-trace-node": "2.0.1",
"@opentelemetry/semantic-conventions": "1.36.0",
"@s2-dev/streamstore": "^0.17.6",
"@trigger.dev/build": "workspace:4.2.0",
"@trigger.dev/core": "workspace:4.2.0",
"@trigger.dev/schema-to-json": "workspace:4.2.0",
"@s2-dev/streamstore": "^0.17.6",
"ansi-escapes": "^7.0.0",
"braces": "^3.0.3",
"c12": "^1.11.1",
@@ -117,6 +117,7 @@
"import-in-the-middle": "1.11.0",
"import-meta-resolve": "^4.1.0",
"ini": "^5.0.0",
"json-stable-stringify": "^1.3.0",
"jsonc-parser": "3.2.1",
"magicast": "^0.3.4",
"minimatch": "^10.0.1",
+15 -12
View File
@@ -12,7 +12,7 @@ import { join, relative, sep } from "node:path";
import { generateContainerfile } from "../deploy/buildImage.js";
import { writeFile } from "node:fs/promises";
import { buildManifestToJSON } from "../utilities/buildManifest.js";
import { readPackageJSON, writePackageJSON } from "pkg-types";
import { readPackageJSON } from "pkg-types";
import { writeJSONFile } from "../utilities/fileSystem.js";
import { isWindows } from "std-env";
import { pathToFileURL } from "node:url";
@@ -192,20 +192,23 @@ async function writeDeployFiles({
) ?? {};
// Step 3: Write the resolved dependencies to the package.json file
await writePackageJSON(join(outputPath, "package.json"), {
...packageJson,
name: packageJson.name ?? "trigger-project",
dependencies: {
...dependencies,
await writeJSONFile(
join(outputPath, "package.json"),
{
...packageJson,
name: packageJson.name ?? "trigger-project",
dependencies: {
...dependencies,
},
trustedDependencies: Object.keys(dependencies).sort(),
devDependencies: {},
peerDependencies: {},
scripts: {},
},
trustedDependencies: Object.keys(dependencies),
devDependencies: {},
peerDependencies: {},
scripts: {},
});
true
);
await writeJSONFile(join(outputPath, "build.json"), buildManifestToJSON(buildManifest));
await writeJSONFile(join(outputPath, "metafile.json"), bundleResult.metafile);
await writeContainerfile(outputPath, buildManifest);
}
+3 -1
View File
@@ -414,7 +414,9 @@ export async function createBuildManifestFromBundle({
otelImportHook: {
include: resolvedConfig.instrumentedPackageNames ?? [],
},
outputHashes: bundle.outputHashes,
// `outputHashes` is only needed for dev builds for the deduplication mechanism during rebuilds.
// For deploys builds, we omit it to ensure deterministic builds
outputHashes: target === "dev" ? bundle.outputHashes : {},
};
if (!workerDir) {
+10 -16
View File
@@ -193,12 +193,12 @@ async function remoteBuildImage(options: DepotBuildImageOptions): Promise<BuildI
"--metadata-file",
"metadata.json",
"--build-arg",
`SOURCE_DATE_EPOCH=0`,
"--build-arg",
`TRIGGER_PROJECT_ID=${options.projectId}`,
"--build-arg",
`TRIGGER_DEPLOYMENT_ID=${options.deploymentId}`,
"--build-arg",
`TRIGGER_DEPLOYMENT_VERSION=${options.deploymentVersion}`,
"--build-arg",
`TRIGGER_CONTENT_HASH=${options.contentHash}`,
"--build-arg",
`TRIGGER_PROJECT_REF=${options.projectRef}`,
@@ -210,6 +210,8 @@ async function remoteBuildImage(options: DepotBuildImageOptions): Promise<BuildI
`TRIGGER_SECRET_KEY=${options.apiKey}`,
...(buildArgs || []),
...(options.extraCACerts ? ["--build-arg", `NODE_EXTRA_CA_CERTS=${options.extraCACerts}`] : []),
"--output",
"type=image,rewrite-timestamp=true",
"--progress",
"plain",
".",
@@ -509,19 +511,17 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
options.imagePlatform,
options.network ? `--network=${options.network}` : undefined,
addHost ? `--add-host=${addHost}` : undefined,
push ? "--push" : undefined,
load ? "--load" : undefined,
"--provenance",
"false",
"--metadata-file",
"metadata.json",
"--build-arg",
`SOURCE_DATE_EPOCH=0`,
"--build-arg",
`TRIGGER_PROJECT_ID=${options.projectId}`,
"--build-arg",
`TRIGGER_DEPLOYMENT_ID=${options.deploymentId}`,
"--build-arg",
`TRIGGER_DEPLOYMENT_VERSION=${options.deploymentVersion}`,
"--build-arg",
`TRIGGER_CONTENT_HASH=${options.contentHash}`,
"--build-arg",
`TRIGGER_PROJECT_REF=${options.projectRef}`,
@@ -533,10 +533,12 @@ async function localBuildImage(options: SelfHostedBuildImageOptions): Promise<Bu
`TRIGGER_SECRET_KEY=${options.apiKey}`,
...(buildArgs || []),
...(options.extraCACerts ? ["--build-arg", `NODE_EXTRA_CA_CERTS=${options.extraCACerts}`] : []),
"--output",
`type=image,name=${imageTag},rewrite-timestamp=true${push ? ",push=true" : ""}${
load ? ",load=true" : ""
}`,
"--progress",
"plain",
"-t",
imageTag,
".", // The build context
].filter(Boolean) as string[];
@@ -761,15 +763,11 @@ USER bun
WORKDIR /app
ARG TRIGGER_PROJECT_ID
ARG TRIGGER_DEPLOYMENT_ID
ARG TRIGGER_DEPLOYMENT_VERSION
ARG TRIGGER_CONTENT_HASH
ARG TRIGGER_PROJECT_REF
ARG NODE_EXTRA_CA_CERTS
ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \
TRIGGER_DEPLOYMENT_ID=\${TRIGGER_DEPLOYMENT_ID} \
TRIGGER_DEPLOYMENT_VERSION=\${TRIGGER_DEPLOYMENT_VERSION} \
TRIGGER_CONTENT_HASH=\${TRIGGER_CONTENT_HASH} \
TRIGGER_PROJECT_REF=\${TRIGGER_PROJECT_REF} \
UV_USE_IO_URING=0 \
@@ -875,15 +873,11 @@ USER node
WORKDIR /app
ARG TRIGGER_PROJECT_ID
ARG TRIGGER_DEPLOYMENT_ID
ARG TRIGGER_DEPLOYMENT_VERSION
ARG TRIGGER_CONTENT_HASH
ARG TRIGGER_PROJECT_REF
ARG NODE_EXTRA_CA_CERTS
ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \
TRIGGER_DEPLOYMENT_ID=\${TRIGGER_DEPLOYMENT_ID} \
TRIGGER_DEPLOYMENT_VERSION=\${TRIGGER_DEPLOYMENT_VERSION} \
TRIGGER_CONTENT_HASH=\${TRIGGER_CONTENT_HASH} \
TRIGGER_PROJECT_REF=\${TRIGGER_PROJECT_REF} \
UV_USE_IO_URING=0 \
@@ -10,6 +10,7 @@ import { CliApiClient } from "../apiClient.js";
import { indexWorkerManifest } from "../indexing/indexWorkerManifest.js";
import { resolveSourceFiles } from "../utilities/sourceFiles.js";
import { execOptionsForRuntime } from "@trigger.dev/core/v3/build";
import { writeJSONFile } from "../utilities/fileSystem.js";
async function loadBuildManifest() {
const manifestContents = await readFile("./build.json", "utf-8");
@@ -88,7 +89,8 @@ async function indexDeployment({
console.log("Writing index.json", process.cwd());
await writeFile(join(process.cwd(), "index.json"), JSON.stringify(workerManifest, null, 2));
const { timings, ...manifestWithoutTimings } = workerManifest;
await writeJSONFile(join(process.cwd(), "index.json"), manifestWithoutTimings, true);
const sourceFiles = resolveSourceFiles(buildManifest.sources, workerManifest.tasks);
@@ -12,8 +12,6 @@ const DateEnv = z
const Env = z.object({
// Set at build time
TRIGGER_CONTENT_HASH: z.string(),
TRIGGER_DEPLOYMENT_ID: z.string(),
TRIGGER_DEPLOYMENT_VERSION: z.string(),
TRIGGER_PROJECT_ID: z.string(),
TRIGGER_PROJECT_REF: z.string(),
NODE_ENV: z.string().default("production"),
@@ -21,6 +19,8 @@ const Env = z.object({
UV_USE_IO_URING: z.string().optional(),
// Set at runtime
TRIGGER_DEPLOYMENT_ID: z.string(),
TRIGGER_DEPLOYMENT_VERSION: z.string(),
TRIGGER_WORKLOAD_CONTROLLER_ID: z.string().default(`controller_${randomUUID()}`),
TRIGGER_ENV_ID: z.string(),
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url(),
@@ -1,10 +1,12 @@
import { BuildManifest } from "@trigger.dev/core/v3/schemas";
export function buildManifestToJSON(manifest: BuildManifest): BuildManifest {
const { deploy, build, ...rest } = manifest;
const { deploy, build, externals, ...rest } = manifest;
return {
...rest,
// sort externals for deterministic builds
externals: externals?.slice().sort((a, b) => a.name.localeCompare(b.name)),
deploy: {},
build: {},
};
+2 -1
View File
@@ -1,4 +1,5 @@
import fsSync from "fs";
import stringify from "json-stable-stringify";
import fsModule, { writeFile } from "fs/promises";
import fs from "node:fs";
import { homedir, tmpdir } from "node:os";
@@ -159,7 +160,7 @@ export async function safeReadJSONFile(path: string) {
}
export async function writeJSONFile(path: string, json: any, pretty = false) {
await safeWriteFile(path, JSON.stringify(json, undefined, pretty ? 2 : undefined));
await safeWriteFile(path, stringify(json, pretty ? { space: 2 } : undefined) ?? "");
}
// Will create the directory if it doesn't exist
+34 -11
View File
@@ -1444,6 +1444,9 @@ importers:
ini:
specifier: ^5.0.0
version: 5.0.0
json-stable-stringify:
specifier: ^1.3.0
version: 1.3.0
jsonc-parser:
specifier: 3.2.1
version: 3.2.1
@@ -11803,13 +11806,14 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bind@1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
call-bind@1.0.8:
resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
@@ -14834,6 +14838,10 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
json-stable-stringify@1.3.0:
resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==}
engines: {node: '>= 0.4'}
json-stringify-safe@5.0.1:
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
@@ -14860,6 +14868,9 @@ packages:
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
jsonify@0.0.1:
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
jsonpath-plus@10.3.0:
resolution: {integrity: sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==}
engines: {node: '>=18.0.0'}
@@ -31881,7 +31892,7 @@ snapshots:
array.prototype.flat@1.3.2:
dependencies:
call-bind: 1.0.2
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-shim-unscopables: 1.0.0
@@ -31895,7 +31906,7 @@ snapshots:
array.prototype.flatmap@1.3.2:
dependencies:
call-bind: 1.0.2
call-bind: 1.0.7
define-properties: 1.2.1
es-abstract: 1.23.3
es-shim-unscopables: 1.0.0
@@ -32328,11 +32339,6 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
call-bind@1.0.2:
dependencies:
function-bind: 1.1.2
get-intrinsic: 1.3.0
call-bind@1.0.7:
dependencies:
es-define-property: 1.0.1
@@ -32341,6 +32347,13 @@ snapshots:
get-intrinsic: 1.3.0
set-function-length: 1.2.2
call-bind@1.0.8:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
get-intrinsic: 1.3.0
set-function-length: 1.2.2
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -35816,6 +35829,14 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
json-stable-stringify@1.3.0:
dependencies:
call-bind: 1.0.8
call-bound: 1.0.4
isarray: 2.0.5
jsonify: 0.0.1
object-keys: 1.1.1
json-stringify-safe@5.0.1: {}
json5@1.0.2:
@@ -35842,6 +35863,8 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
jsonify@0.0.1: {}
jsonpath-plus@10.3.0:
dependencies:
'@jsep-plugin/assignment': 1.3.0(jsep@1.4.0)
@@ -40313,7 +40336,7 @@ snapshots:
string.prototype.padend@3.1.4:
dependencies:
call-bind: 1.0.2
call-bind: 1.0.7
define-properties: 1.1.4
es-abstract: 1.21.1