Skip to content

Commit 295178d

Browse files
committed
feat(deploy): support specific commit deployments
and cache clearing - Add `clear_cache` column to `deployments` table - Allow specifying `commitSha` during deployment via API and UI - Update `prepareSourceWorkspace` to support explicit git checkout - Add unit tests for scaling engine - Modularize Docker utilities for the scaling engine
1 parent afc28aa commit 295178d

14 files changed

Lines changed: 594 additions & 142 deletions

File tree

apps/api/src/api/deployments/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ export const deploymentsRoutes = new Elysia()
6262
const environment =
6363
String(form.get("environment") ?? "").trim() ||
6464
undefined;
65+
const commitSha =
66+
String(form.get("commitSha") ?? "").trim() ||
67+
undefined;
68+
const clearCache = form.get("clearCache") === "true";
6569
if (
6670
sourceType !== "git" &&
6771
sourceType !== "upload" &&
@@ -86,6 +90,8 @@ export const deploymentsRoutes = new Elysia()
8690
sourceRef: gitUrl,
8791
branch,
8892
environment,
93+
commitSha,
94+
clearCache,
8995
});
9096
orchestrator.enqueue(deployment.id);
9197
return deployment;
@@ -118,6 +124,7 @@ export const deploymentsRoutes = new Elysia()
118124
sourceRef: uploadPath,
119125
branch,
120126
environment,
127+
clearCache,
121128
});
122129
orchestrator.enqueue(deployment.id);
123130
return deployment;

apps/api/src/db/migrate.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ export const migrate = async () => {
4747
console.log("[Migrate] Added projects.source_type column");
4848
}
4949

50+
const deploymentsTableInfo = sqlite.query("PRAGMA table_info('deployments')").all() as { name: string }[];
51+
const deploymentsColumns = deploymentsTableInfo.map(r => r.name);
52+
if (!deploymentsColumns.includes('clear_cache')) {
53+
sqlite.exec("ALTER TABLE deployments ADD COLUMN clear_cache integer NOT NULL DEFAULT 0");
54+
console.log("[Migrate] Added deployments.clear_cache column");
55+
}
56+
5057
await seedFromConfig();
5158
};
5259

apps/api/src/db/repo/deployments.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const mapDeployment = (row: typeof deployments.$inferSelect): Deployment => ({
2020
replicas: row.replicas,
2121
environment: row.environment,
2222
failureReason: row.failureReason,
23+
clearCache: Boolean(row.clearCache),
2324
createdAt: row.createdAt,
2425
updatedAt: row.updatedAt,
2526
});
@@ -38,6 +39,7 @@ export const createDeployment = async (input: CreateDeploymentInput): Promise<De
3839
branch: input.branch ?? null,
3940
commitSha: input.commitSha ?? null,
4041
environment: input.environment ?? null,
42+
clearCache: input.clearCache ? 1 : 0,
4143
createdAt: timestamp,
4244
updatedAt: timestamp,
4345
}).run();

apps/api/src/db/repo/scaling.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export const upsertScalingPolicy = async (input: CreateScalingPolicyInput): Prom
2828
if (input.maxReplicas !== undefined) updates.maxReplicas = input.maxReplicas;
2929
if (input.cpuThresholdPercent !== undefined) updates.cpuThresholdPercent = input.cpuThresholdPercent;
3030
if (input.memoryThresholdPercent !== undefined) updates.memoryThresholdPercent = input.memoryThresholdPercent;
31+
if (input.cooldownSeconds !== undefined) updates.cooldownSeconds = input.cooldownSeconds;
32+
if (input.enabled !== undefined) updates.enabled = input.enabled ? 1 : 0;
3133
db.update(scalingPolicies).set(updates).where(eq(scalingPolicies.projectId, input.projectId)).run();
3234
return mapScalingPolicy(db.select().from(scalingPolicies).where(eq(scalingPolicies.projectId, input.projectId)).get()!);
3335
}
@@ -39,6 +41,8 @@ export const upsertScalingPolicy = async (input: CreateScalingPolicyInput): Prom
3941
maxReplicas: input.maxReplicas ?? 5,
4042
cpuThresholdPercent: input.cpuThresholdPercent ?? 70,
4143
memoryThresholdPercent: input.memoryThresholdPercent ?? 85,
44+
cooldownSeconds: input.cooldownSeconds ?? 120,
45+
enabled: input.enabled ? 1 : 1,
4246
createdAt: timestamp,
4347
updatedAt: timestamp,
4448
}).run();

apps/api/src/db/schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export const deployments = sqliteTable("deployments", {
4343
replicas: integer().notNull().default(1),
4444
environment: text(),
4545
failureReason: text("failure_reason"),
46+
clearCache: integer("clear_cache").notNull().default(0),
4647
createdAt: text("created_at").notNull(),
4748
updatedAt: text("updated_at").notNull(),
4849
});

apps/api/src/orchestrator/pipeline.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,21 +316,24 @@ export class PipelineOrchestrator {
316316
deployment.sourceType ===
317317
"git"
318318
) {
319-
const branchLabel =
320-
deployment.branch
319+
const commitLabel = deployment.commitSha
320+
? ` (commit ${deployment.commitSha.slice(0, 7)})`
321+
: deployment.branch
321322
? ` (branch ${deployment.branch})`
322323
: "";
323324
await emitLog(
324325
deploymentId,
325326
"build",
326-
`Cloning git repository: ${deployment.sourceRef}${branchLabel}`,
327+
`Cloning git repository: ${deployment.sourceRef}${commitLabel}`,
327328
);
328329
workspacePath =
329330
await prepareSourceWorkspace(
330331
deploymentId,
331332
deployment.sourceRef,
332333
deployment.branch ??
333334
undefined,
335+
deployment.commitSha ??
336+
undefined,
334337
);
335338
const sha = await getHeadSha(
336339
workspacePath,
@@ -382,7 +385,7 @@ export class PipelineOrchestrator {
382385
line,
383386
);
384387
},
385-
{ cacheKey, sourceDir: project?.sourceDir, signal: controller.signal },
388+
{ cacheKey, sourceDir: project?.sourceDir, signal: controller.signal, clearCache: deployment.clearCache },
386389
);
387390
} else {
388391
await emitLog(

apps/api/src/orchestrator/railpack.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ export const buildWithRailpack = async (
722722
cacheKey?: string;
723723
sourceDir?: string | null;
724724
signal?: AbortSignal;
725+
clearCache?: boolean;
725726
},
726727
): Promise<RailpackBuildResult> => {
727728
await onLog(
@@ -731,13 +732,18 @@ export const buildWithRailpack = async (
731732
// Ensure builder exists (persists across builds)
732733
await ensureBuilder();
733734

734-
const cacheKey =
735+
let cacheKey =
735736
opts?.cacheKey ??
736737
imageTag
737738
.split(":")[0]
738739
.replace(/-[0-9a-f]{8}$/i, "") // Strip unique deployment short ID suffix
739740
.replace(/[^a-zA-Z0-9_-]/g, "-");
740741

742+
if (opts?.clearCache) {
743+
cacheKey = `${cacheKey}-clear-${Date.now()}`;
744+
await onLog(`Bypassing cache for clean build (cacheKey: ${cacheKey})`);
745+
}
746+
741747
const cleanSourceDir = opts?.sourceDir
742748
? opts.sourceDir.replace(/^\//, "")
743749
: null;

apps/api/src/orchestrator/runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export const ensureContainerRunning = async (containerName: string) => {
9494
};
9595

9696
export const reloadCaddy = async () => {
97+
if (process.env.NODE_ENV === 'test') return;
9798
const caddyContainer = await getCaddyContainer();
9899
await run(dockerBin, ['exec', caddyContainer, 'caddy', 'reload', '--config', '/etc/caddy/Caddyfile']);
99100
};

apps/api/src/orchestrator/source.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,19 @@ const run = (cmd: string, args: string[], cwd?: string) =>
3434
});
3535
});
3636

37-
export const prepareSourceWorkspace = async (deploymentId: string, gitUrl: string, branch?: string) => {
37+
export const prepareSourceWorkspace = async (deploymentId: string, gitUrl: string, branch?: string, commitSha?: string) => {
3838
const root = join(config.workspaceRoot, deploymentId);
3939
await rm(root, { recursive: true, force: true });
4040
await mkdir(root, { recursive: true });
41-
const args = ['clone', '--depth', '1'];
42-
if (branch) args.push('--branch', branch);
43-
args.push(gitUrl, root);
44-
await run('git', args);
41+
if (commitSha) {
42+
await run('git', ['clone', gitUrl, root]);
43+
await run('git', ['checkout', commitSha], root);
44+
} else {
45+
const args = ['clone', '--depth', '1'];
46+
if (branch) args.push('--branch', branch);
47+
args.push(gitUrl, root);
48+
await run('git', args);
49+
}
4550
return root;
4651
};
4752

0 commit comments

Comments
 (0)