feat: release 2.7.0-rc2

This commit is contained in:
2026-10-06 15:31:51 +00:00 Unverified
parent cd9fb512cd
commit c8de9ddcba
42 changed files with 6127 additions and 593 deletions
+122 -4
View File
@@ -4,12 +4,18 @@ import { join } from "node:path";
import {
BUILD_PROTOCOL_VERSION,
assertSha256Digest,
validateBuildpackUri,
type BuildEvent,
type BuildRequest,
type BuildStatus,
type Sha256Digest,
} from "../shared/build-protocol";
import { createBuildJob, type KubernetesJob } from "./build-job";
import {
createBuildpacksJob,
DEFAULT_BUILDPACKS_IMAGE,
validateBuildpacksImage,
} from "./buildpacks-job";
import {
BUILD_RECORD_API_VERSION,
BuildStoreConflictError,
@@ -26,6 +32,7 @@ import {
type MaterializeTiming,
} from "./materialize";
import { parseImageReference, resolveRegistryDigest } from "./registry";
import { stageBuildpackPackage } from "./buildpack-package";
export const DEFAULT_MAX_BLOB_BYTES = 1024 * 1024 * 1024;
export const DEFAULT_MAX_UPLOAD_CHUNK_BYTES = 8 * 1024 * 1024;
@@ -40,7 +47,12 @@ export interface BuildCas extends MaterializeCas {
}
export type JobPhase =
"queued" | "creating" | "starting" | "running" | "succeeded" | "failed";
| "queued"
| "creating"
| "starting"
| "running"
| "succeeded"
| "failed";
export interface BuildJobObservation {
phase: JobPhase;
@@ -79,6 +91,8 @@ export interface BuildControllerOptions {
pushRegistryInsecure?: boolean;
cacheRegistryInsecure?: boolean;
buildkitImage?: string;
buildpacksImage?: string;
stageBuildpack?: typeof stageBuildpackPackage;
serviceAccountName?: string;
registrySecretName?: string;
nodeSelector?: Record<string, string>;
@@ -182,6 +196,33 @@ function validateRequest(request: BuildRequest): void {
throw new BuildValidationError("Build ID is too long");
if (!request.spec || !["amd64", "arm64"].includes(request.spec.architecture))
throw new BuildValidationError("Invalid build architecture");
if (
request.spec.builder !== undefined &&
request.spec.builder !== "buildkit" &&
request.spec.builder !== "buildpacks"
)
throw new BuildValidationError("Invalid build builder");
if (request.spec.buildpackUri !== undefined) {
if (request.spec.builder !== "buildpacks")
throw new BuildValidationError(
"buildpackUri requires the buildpacks builder",
);
try {
validateBuildpackUri(request.spec.buildpackUri);
} catch (error) {
throw new BuildValidationError((error as Error).message);
}
}
if (request.spec.builder === "buildpacks") {
if (
request.spec.dockerfile !== undefined ||
request.spec.target !== undefined ||
request.spec.buildArgs?.length
)
throw new BuildValidationError(
"Buildpacks does not support dockerfile, target, or buildArgs",
);
}
if (
!Array.isArray(request.spec.buildArgs) ||
!request.spec.buildArgs.every((value) => typeof value === "string")
@@ -446,6 +487,15 @@ export class BuildController {
}),
}));
validateRequest(request);
if (request.spec.builder === "buildpacks") {
try {
validateBuildpacksImage(
this.options.buildpacksImage ?? DEFAULT_BUILDPACKS_IMAGE,
);
} catch (error) {
throw new BuildValidationError((error as Error).message);
}
}
const imageKey = `${request.project}\0${request.service}\0${request.spec.image}`;
const started = performance.now();
const previous =
@@ -585,6 +635,13 @@ export class BuildController {
join(this.options.workspaceRoot, superseded.spec.workspaceSubPath),
{ recursive: true, force: true },
).catch(() => undefined);
await rm(
join(
this.options.workspaceRoot,
`${superseded.spec.workspaceSubPath}-buildpack`,
),
{ recursive: true, force: true },
).catch(() => undefined);
}
});
if (!result.created) return recordStatus(stored);
@@ -615,6 +672,15 @@ export class BuildController {
materialize,
),
);
const packageSubPath = `${stored.spec.workspaceSubPath}-buildpack`;
if (request.spec.buildpackUri)
await timed("buildpackStageMs", () =>
(this.options.stageBuildpack ?? stageBuildpackPackage)(
request.spec.buildpackUri!,
request.spec.architecture,
join(this.options.workspaceRoot, packageSubPath),
),
);
if (
this.options.store.ownsBuild &&
!(await timed("finalOwnershipMs", () =>
@@ -625,10 +691,29 @@ export class BuildController {
`Build '${request.id}' lost image lock before Job creation`,
);
const job = await timed("jobSpecMs", async () => {
const cacheImage =
let cacheImage =
typeof this.options.cacheImage === "function"
? this.options.cacheImage(request)
: this.options.cacheImage;
if (request.spec.buildpackUri) {
const suffix = createHash("sha256")
.update(
JSON.stringify([
cacheImage,
request.spec.architecture,
request.spec.buildpackUri,
]),
)
.digest("hex")
.slice(0, 24);
const colon = cacheImage.lastIndexOf(":");
const slash = cacheImage.lastIndexOf("/");
if (cacheImage.includes("@"))
throw new BuildValidationError(
"Build cache must be a tag, not a digest",
);
cacheImage = `${colon > slash ? cacheImage.slice(0, colon) : cacheImage}:buildpack-${suffix}`;
}
const pushImage = this.options.pushImage
? this.options.pushImage(request)
: request.spec.image;
@@ -642,7 +727,7 @@ export class BuildController {
})
: destination.image,
);
return createBuildJob({
const jobOptions = {
name: jobName,
namespace: this.options.namespace,
spec: request.spec,
@@ -663,7 +748,22 @@ export class BuildController {
registrySecretName: this.options.registrySecretName,
nodeSelector: this.options.nodeSelector,
tolerations: this.options.tolerations,
});
};
return request.spec.builder === "buildpacks"
? createBuildpacksJob({
...jobOptions,
buildpacksImage: this.options.buildpacksImage,
...(request.spec.buildpackUri && {
buildpackSubPath: jobWorkspaceSubPath(
this.options.workspaceRoot,
packageSubPath,
),
}),
cacheRegistryInsecure:
this.options.cacheRegistryInsecure ??
this.options.pushRegistryInsecure,
})
: createBuildJob(jobOptions);
});
await timed("jobCreateMs", () => this.options.kubernetes.createJob(job));
markJobCreated();
@@ -680,6 +780,17 @@ export class BuildController {
} catch {
// Job cleanup is best effort; the record still releases its lock.
}
await rm(
join(this.options.workspaceRoot, stored.spec.workspaceSubPath),
{ recursive: true, force: true },
).catch(() => undefined);
await rm(
join(
this.options.workspaceRoot,
`${stored.spec.workspaceSubPath}-buildpack`,
),
{ recursive: true, force: true },
).catch(() => undefined);
await this.failBuild(
stored.metadata.name,
error instanceof Error ? error.message : String(error),
@@ -975,6 +1086,13 @@ export class BuildController {
recursive: true,
force: true,
});
await rm(
join(
this.options.workspaceRoot,
`${record.spec.workspaceSubPath}-buildpack`,
),
{ recursive: true, force: true },
);
}
async getBuildResult(id: string): Promise<{