Skip to content

Commit 652aea8

Browse files
feat(cli): add two-phase deploy (build-only + register-only)
Adds support for splitting deployment into separate build and register phases. New CLI options: --build-only, --register-only, --registry, --repository, --base-image-node, --containerfile-module, --skip-digest. Also adds: worker pod service account configuration, security context, DEPLOY_VERSION_SUFFIX env var, custom containerfile module support, and index metadata extraction from Docker images. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a549a1b commit 652aea8

24 files changed

Lines changed: 1685 additions & 134 deletions

apps/supervisor/Containerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm fetch -
2525
FROM deps-fetcher AS dev-deps
2626
ENV NODE_ENV development
2727

28-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --frozen-lockfile --offline --ignore-scripts
28+
# TEMP --no-frozen-lockfile and remove --offline for overrides for CVEs
29+
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --no-frozen-lockfile --ignore-scripts
2930

3031
FROM base AS builder
3132

apps/supervisor/src/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ const Env = z.object({
8181
KUBERNETES_FORCE_ENABLED: BoolEnv.default(false),
8282
KUBERNETES_NAMESPACE: z.string().default("default"),
8383
KUBERNETES_WORKER_NODETYPE_LABEL: z.string().default("v4-worker"),
84+
KUBERNETES_WORKER_SERVICE_ACCOUNT: z.string().optional(), // Service account for worker pods
85+
KUBERNETES_WORKER_AUTOMOUNT_SERVICE_ACCOUNT_TOKEN: BoolEnv.default(false), // Whether to mount SA token
8486
KUBERNETES_IMAGE_PULL_SECRETS: z.string().optional(), // csv
8587
KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT: z.string().default("10Gi"),
8688
KUBERNETES_EPHEMERAL_STORAGE_SIZE_REQUEST: z.string().default("2Gi"),

apps/supervisor/src/workloadManager/kubernetes.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ export class KubernetesWorkloadManager implements WorkloadManager {
117117
"app.kubernetes.io/part-of": "trigger-worker",
118118
"app.kubernetes.io/component": "create",
119119
},
120+
annotations: {
121+
"com.palantir.rubix.service/pod-cert": "{}",
122+
},
120123
},
121124
spec: {
122125
...this.addPlacementTags(this.#defaultPodSpec, opts.placementTags),
@@ -132,6 +135,14 @@ export class KubernetesWorkloadManager implements WorkloadManager {
132135
},
133136
],
134137
resources: this.#getResourcesForMachine(opts.machine),
138+
securityContext: {
139+
runAsNonRoot: true,
140+
runAsUser: 1000,
141+
allowPrivilegeEscalation: false,
142+
capabilities: {
143+
drop: ["ALL"],
144+
},
145+
},
135146
env: [
136147
{
137148
name: "TRIGGER_DEQUEUED_AT_MS",
@@ -306,13 +317,23 @@ export class KubernetesWorkloadManager implements WorkloadManager {
306317
get #defaultPodSpec(): Omit<k8s.V1PodSpec, "containers"> {
307318
return {
308319
restartPolicy: "Never",
309-
automountServiceAccountToken: false,
320+
// Explicit control over service account token mounting (defaults to false for security)
321+
automountServiceAccountToken: env.KUBERNETES_WORKER_AUTOMOUNT_SERVICE_ACCOUNT_TOKEN,
310322
imagePullSecrets: this.getImagePullSecrets(),
311323
...(env.KUBERNETES_SCHEDULER_NAME
312324
? {
313325
schedulerName: env.KUBERNETES_SCHEDULER_NAME,
314326
}
315327
: {}),
328+
// Optionally specify a service account for the worker pods
329+
...(env.KUBERNETES_WORKER_SERVICE_ACCOUNT
330+
? { serviceAccountName: env.KUBERNETES_WORKER_SERVICE_ACCOUNT }
331+
: {}),
332+
securityContext: {
333+
runAsNonRoot: true,
334+
runAsUser: 1000,
335+
fsGroup: 1000,
336+
},
316337
...(env.KUBERNETES_WORKER_NODETYPE_LABEL
317338
? {
318339
nodeSelector: {

apps/webapp/app/env.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ const EnvironmentSchema = z
334334
.transform((v) => v ?? process.env.DEPLOY_REGISTRY_ECR_ASSUME_ROLE_EXTERNAL_ID),
335335

336336
DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"),
337+
DEPLOY_VERSION_SUFFIX: z.string().optional(),
337338
DEPLOY_TIMEOUT_MS: z.coerce
338339
.number()
339340
.int()

apps/webapp/app/presenters/v3/DeploymentListPresenter.server.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,8 @@ WHERE
202202
wd."projectId" = ${project.id}
203203
AND wd."environmentId" = ${environment.id}
204204
ORDER BY
205-
string_to_array(wd."version", '.')::int[] DESC
205+
string_to_array(split_part(wd."version", '-', 1), '.')::int[] DESC,
206+
split_part(wd."version", '-', 2) DESC
206207
LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
207208

208209
const { connectedGithubRepository } = project;
@@ -319,7 +320,13 @@ LIMIT ${pageSize} OFFSET ${pageSize * (page - 1)};`;
319320
FROM ${sqlDatabaseSchema}."WorkerDeployment"
320321
WHERE "projectId" = ${project.id}
321322
AND "environmentId" = ${environment.id}
322-
AND string_to_array(version, '.')::int[] > string_to_array(${version}, '.')::int[]
323+
AND (
324+
string_to_array(split_part(version, '-', 1), '.')::int[] > string_to_array(split_part(${version}, '-', 1), '.')::int[]
325+
OR (
326+
string_to_array(split_part(version, '-', 1), '.')::int[] = string_to_array(split_part(${version}, '-', 1), '.')::int[]
327+
AND split_part(version, '-', 2) > split_part(${version}, '-', 2)
328+
)
329+
)
323330
`;
324331

325332
const count = Number(deploymentsSinceVersion[0].count);

apps/webapp/app/presenters/v3/TestPresenter.server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export class TestPresenter extends BasePresenter {
4545
>`WITH workers AS (
4646
SELECT
4747
bw.*,
48-
ROW_NUMBER() OVER(ORDER BY string_to_array(bw.version, '.')::int[] DESC) AS rn
48+
ROW_NUMBER() OVER(
49+
ORDER BY
50+
string_to_array(split_part(bw.version, '-', 1), '.')::int[] DESC,
51+
split_part(bw.version, '-', 2) DESC
52+
) AS rn
4953
FROM
5054
${sqlDatabaseSchema}."BackgroundWorker" bw
5155
WHERE "runtimeEnvironmentId" = ${envId}

apps/webapp/app/v3/marqs/devQueueConsumer.server.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { FailedTaskRunService } from "../failedTaskRun.server";
2222
import { CancelDevSessionRunsService } from "../services/cancelDevSessionRuns.server";
2323
import { CompleteAttemptService } from "../services/completeAttempt.server";
2424
import { attributesFromAuthenticatedEnv, tracer } from "../tracer.server";
25+
import { compareDeploymentVersions } from "../utils/deploymentVersions";
2526
import { DevSubscriber, devPubSub } from "./devPubSub.server";
2627

2728
const MessageBody = z.discriminatedUnion("type", [
@@ -589,7 +590,7 @@ export class DevQueueConsumer {
589590
}
590591

591592
// Get the latest background worker based on the version.
592-
// Versions are in the format of 20240101.1 and 20240101.2, or even 20240101.10, 20240101.11, etc.
593+
// Versions are in the format of YYYYMMDD.N (e.g., 20240101.1) with optional suffix (e.g., 20240101.1-hardened)
593594
#getLatestBackgroundWorker() {
594595
const workers = Array.from(this._backgroundWorkers.values());
595596

@@ -598,22 +599,10 @@ export class DevQueueConsumer {
598599
}
599600

600601
return workers.reduce((acc, curr) => {
601-
const accParts = acc.version.split(".").map(Number);
602-
const currParts = curr.version.split(".").map(Number);
603-
604-
// Compare the major part
605-
if (accParts[0] < currParts[0]) {
606-
return curr;
607-
} else if (accParts[0] > currParts[0]) {
608-
return acc;
609-
}
610-
611-
// Compare the minor part (assuming all versions have two parts)
612-
if (accParts[1] < currParts[1]) {
613-
return curr;
614-
} else {
615-
return acc;
616-
}
602+
// Use compareDeploymentVersions to properly handle versions with suffixes
603+
const comparison = compareDeploymentVersions(acc.version, curr.version);
604+
// If curr is newer (comparison returns -1), use curr, otherwise use acc
605+
return comparison < 0 ? curr : acc;
617606
});
618607
}
619608
}

apps/webapp/app/v3/services/createBackgroundWorker.server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { BackgroundWorkerId } from "@trigger.dev/core/v3/isomorphic";
1010
import type { BackgroundWorker, TaskQueue, TaskQueueType } from "@trigger.dev/database";
1111
import cronstrue from "cronstrue";
1212
import { $transaction, Prisma, PrismaClientOrTransaction } from "~/db.server";
13+
import { env } from "~/env.server";
1314
import { sanitizeQueueName } from "~/models/taskQueue.server";
1415
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
1516
import { logger } from "~/services/logger.server";
@@ -65,7 +66,7 @@ export class CreateBackgroundWorkerService extends BaseService {
6566
return latestBackgroundWorker;
6667
}
6768

68-
const nextVersion = calculateNextBuildVersion(project.backgroundWorkers[0]?.version);
69+
const nextVersion = calculateNextBuildVersion(project.backgroundWorkers[0]?.version, env.DEPLOY_VERSION_SUFFIX);
6970

7071
logger.debug(`Creating background worker`, {
7172
nextVersion,

apps/webapp/app/v3/services/initializeDeployment.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export class InitializeDeploymentService extends BaseService {
8585
take: 1,
8686
});
8787

88-
const nextVersion = calculateNextBuildVersion(latestDeployment?.version);
88+
const nextVersion = calculateNextBuildVersion(latestDeployment?.version, env.DEPLOY_VERSION_SUFFIX);
8989

9090
if (payload.selfHosted && remoteBuildsEnabled()) {
9191
throw new ServiceValidationError(
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
// Calculate next build version based on the previous version
22
// Version formats are YYYYMMDD.1, YYYYMMDD.2, etc.
3+
// With optional suffix: YYYYMMDD.1-suffix
34
// If there is no previous version, start at Todays date and .1
4-
export function calculateNextBuildVersion(latestVersion?: string | null): string {
5+
export function calculateNextBuildVersion(latestVersion?: string | null, suffix?: string): string {
56
const today = new Date();
67
const year = today.getFullYear();
78
const month = today.getMonth() + 1;
89
const day = today.getDate();
910
const todayFormatted = `${year}${month < 10 ? "0" : ""}${month}${day < 10 ? "0" : ""}${day}`;
1011

1112
if (!latestVersion) {
12-
return `${todayFormatted}.1`;
13+
const baseVersion = `${todayFormatted}.1`;
14+
return suffix ? `${baseVersion}-${suffix}` : baseVersion;
1315
}
1416

15-
const [date, buildNumber] = latestVersion.split(".");
17+
// Extract base version and suffix from latest version
18+
const [baseVersion, existingSuffix] = latestVersion.split("-");
19+
const [date, buildNumber] = baseVersion.split(".");
1620

1721
if (date === todayFormatted) {
1822
const nextBuildNumber = parseInt(buildNumber, 10) + 1;
19-
return `${date}.${nextBuildNumber}`;
23+
const newBaseVersion = `${date}.${nextBuildNumber}`;
24+
return suffix ? `${newBaseVersion}-${suffix}` : newBaseVersion;
2025
}
2126

22-
return `${todayFormatted}.1`;
27+
const newBaseVersion = `${todayFormatted}.1`;
28+
return suffix ? `${newBaseVersion}-${suffix}` : newBaseVersion;
2329
}

0 commit comments

Comments
 (0)