Skip to content

Commit 7829da9

Browse files
amarbakir-govsignalsclaude
authored andcommitted
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 ae8c1d6 commit 7829da9

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
@@ -92,6 +92,8 @@ const Env = z
9292
KUBERNETES_FORCE_ENABLED: BoolEnv.default(false),
9393
KUBERNETES_NAMESPACE: z.string().default("default"),
9494
KUBERNETES_WORKER_NODETYPE_LABEL: z.string().default("v4-worker"),
95+
KUBERNETES_WORKER_SERVICE_ACCOUNT: z.string().optional(), // Service account for worker pods
96+
KUBERNETES_WORKER_AUTOMOUNT_SERVICE_ACCOUNT_TOKEN: BoolEnv.default(false), // Whether to mount SA token
9597
KUBERNETES_IMAGE_PULL_SECRETS: z.string().optional(), // csv
9698
KUBERNETES_EPHEMERAL_STORAGE_SIZE_LIMIT: z.string().default("10Gi"),
9799
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),
@@ -133,6 +136,14 @@ export class KubernetesWorkloadManager implements WorkloadManager {
133136
},
134137
],
135138
resources: this.#getResourcesForMachine(opts.machine),
139+
securityContext: {
140+
runAsNonRoot: true,
141+
runAsUser: 1000,
142+
allowPrivilegeEscalation: false,
143+
capabilities: {
144+
drop: ["ALL"],
145+
},
146+
},
136147
env: [
137148
{
138149
name: "TRIGGER_DEQUEUED_AT_MS",
@@ -307,13 +318,23 @@ export class KubernetesWorkloadManager implements WorkloadManager {
307318
get #defaultPodSpec(): Omit<k8s.V1PodSpec, "containers"> {
308319
return {
309320
restartPolicy: "Never",
310-
automountServiceAccountToken: false,
321+
// Explicit control over service account token mounting (defaults to false for security)
322+
automountServiceAccountToken: env.KUBERNETES_WORKER_AUTOMOUNT_SERVICE_ACCOUNT_TOKEN,
311323
imagePullSecrets: this.getImagePullSecrets(),
312324
...(env.KUBERNETES_SCHEDULER_NAME
313325
? {
314326
schedulerName: env.KUBERNETES_SCHEDULER_NAME,
315327
}
316328
: {}),
329+
// Optionally specify a service account for the worker pods
330+
...(env.KUBERNETES_WORKER_SERVICE_ACCOUNT
331+
? { serviceAccountName: env.KUBERNETES_WORKER_SERVICE_ACCOUNT }
332+
: {}),
333+
securityContext: {
334+
runAsNonRoot: true,
335+
runAsUser: 1000,
336+
fsGroup: 1000,
337+
},
317338
...(env.KUBERNETES_WORKER_NODETYPE_LABEL
318339
? {
319340
nodeSelector: {

apps/webapp/app/env.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ const EnvironmentSchema = z
344344
COMPUTE_TEMPLATE_SHADOW_ROLLOUT_PCT: z.string().optional(),
345345

346346
DEPLOY_IMAGE_PLATFORM: z.string().default("linux/amd64"),
347+
DEPLOY_VERSION_SUFFIX: z.string().optional(),
347348
DEPLOY_TIMEOUT_MS: z.coerce
348349
.number()
349350
.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", [
@@ -590,7 +591,7 @@ export class DevQueueConsumer {
590591
}
591592

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

@@ -599,22 +600,10 @@ export class DevQueueConsumer {
599600
}
600601

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

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, stringifyDuration } from "@trigger.dev/core/v3/isom
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";
@@ -66,7 +67,7 @@ export class CreateBackgroundWorkerService extends BaseService {
6667
return latestBackgroundWorker;
6768
}
6869

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

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class InitializeDeploymentService extends BaseService {
107107
take: 1,
108108
});
109109

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

112112
if (payload.selfHosted && remoteBuildsEnabled()) {
113113
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)