Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/api/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
dialect: "sqlite",
schema: "./src/db/schema.ts",
out: "./src/db/migrations",
dbCredentials: {
url: "./data/dequel.db",
},
});
7 changes: 7 additions & 0 deletions apps/api/src/api/deployments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export const deploymentsRoutes = new Elysia()
const environment =
String(form.get("environment") ?? "").trim() ||
undefined;
const commitSha =
String(form.get("commitSha") ?? "").trim() ||
undefined;
const clearCache = form.get("clearCache") === "true";
Comment on lines +65 to +68

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect how commitSha is consumed downstream to assess injection risk
fd source.ts apps/api/src/orchestrator --exec sed -n '1,200p' {}

Repository: Lftobs/dequel

Length of output: 3740


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect deployment parsing/validation and all commitSha usages
printf '\n== deployments index ==\n'
sed -n '1,260p' apps/api/src/api/deployments/index.ts

printf '\n== commitSha references ==\n'
rg -n "commitSha" apps/api/src -g '!**/node_modules/**'

Repository: Lftobs/dequel

Length of output: 8896


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether any code normalizes or validates commitSha before it reaches the orchestrator
sed -n '1,140p' apps/api/src/db/repo/deployments.ts
printf '\n== types ==\n'
sed -n '210,255p' apps/api/src/types.ts
printf '\n== pipeline commitSha usage ==\n'
sed -n '300,350p' apps/api/src/orchestrator/pipeline.ts

Repository: Lftobs/dequel

Length of output: 7384


🌐 Web query:

git checkout argument starting with hyphen treated as option argv parse -- commit SHA validation best practice

💡 Result:

In Git, the double-hyphen (--) is the standard mechanism to disambiguate arguments [1][2]. It signals the "end of options," instructing Git to interpret all subsequent arguments as pathspecs (files or directories) rather than command-line options, branches, or commit identifiers [1][3][4]. Using -- is essential in the following scenarios: - Files starting with a hyphen: If you have a file named "-filename", running "git checkout -filename" will cause Git to attempt to parse "-f" as an option [1]. The correct syntax is "git checkout -- -filename" [1]. - Disambiguation: If a file name shares the same name as a branch or commit (e.g., you have a file named "master"), using "git checkout master" may switch branches instead of restoring the file [3][5]. "git checkout -- master" explicitly forces Git to treat "master" as a pathspec [3]. Regarding commit SHA validation, while Git natively validates SHAs when you provide them to commands like "git checkout ", there is no built-in "git checkout --validate-sha" flag. Best practice is to rely on Git's internal object database validation: - When you use a partial SHA (a "short SHA"), Git automatically resolves it to a full 40-character object name. If the SHA is ambiguous (matches multiple objects), Git will return an error and suggest the matches. - To programmatically validate a commit SHA before performing an operation, you can use: git rev-parse --verify ^{commit} This command will return the full SHA if valid, or exit with an error code (and print an error to stderr) if the SHA does not exist or does not point to a commit [6]. Note: For modern workflows, it is recommended to use the more specific, explicit commands "git switch" for branch operations and "git restore" for file operations, as they eliminate much of the ambiguity inherent in "git checkout" [7].

Citations:


Validate commitSha before enqueuing the deployment. It’s stored as-is and later passed to git checkout, so malformed or option-like input will just make the deploy fail. Reject non-revision values up front, or resolve it with git rev-parse --verify.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/api/deployments/index.ts` around lines 65 - 68, Validate
commitSha in the deployments flow before enqueueing the job, since the value
from form.get("commitSha") in apps/api/src/api/deployments/index.ts is passed
through to deployment execution and ultimately git checkout. Update the
commitSha handling in the request parsing path to reject malformed or
option-like values up front, or resolve it with git rev-parse --verify before
creating the deployment task, so only valid revisions reach the worker.

if (
sourceType !== "git" &&
sourceType !== "upload" &&
Expand All @@ -86,6 +90,8 @@ export const deploymentsRoutes = new Elysia()
sourceRef: gitUrl,
branch,
environment,
commitSha,
clearCache,
});
orchestrator.enqueue(deployment.id);
return deployment;
Expand Down Expand Up @@ -118,6 +124,7 @@ export const deploymentsRoutes = new Elysia()
sourceRef: uploadPath,
branch,
environment,
clearCache,
});
orchestrator.enqueue(deployment.id);
return deployment;
Expand Down
34 changes: 14 additions & 20 deletions apps/api/src/db/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { existsSync } from "node:fs";
import { readdirSync } from "node:fs";
import { existsSync, readdirSync } from "node:fs";
import { migrate as drizzleMigrate } from "drizzle-orm/bun-sqlite/migrator";
import { getDrizzle } from "./drizzle";
import { getDb } from "./client";
import { sql } from "drizzle-orm";
import { config } from "../utils/config";
import { getSmtpSettings, upsertSmtpSettings } from "./repo/settings";
import { getGithubIntegration, setGithubIntegration } from "./repo/github";
Expand Down Expand Up @@ -30,26 +29,21 @@ export const migrate = async () => {

drizzleMigrate(db, { migrationsFolder });

// Apply schema additions that Drizzle Kit migrations may miss
const sqlite = await getDb();
const tableInfo = sqlite.query("PRAGMA table_info('projects')").all() as { name: string }[];
const columns = tableInfo.map(r => r.name);
if (!columns.includes('port')) {
sqlite.exec("ALTER TABLE projects ADD COLUMN port integer");
console.log("[Migrate] Added projects.port column");
}
if (!columns.includes('source_dir')) {
sqlite.exec("ALTER TABLE projects ADD COLUMN source_dir text");
console.log("[Migrate] Added projects.source_dir column");
}
if (!columns.includes('source_type')) {
sqlite.exec("ALTER TABLE projects ADD COLUMN source_type text NOT NULL DEFAULT 'git'");
console.log("[Migrate] Added projects.source_type column");
}

await addClearCacheColumn(db);
await seedFromConfig();
};

const addClearCacheColumn = async (db: ReturnType<typeof getDrizzle>) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
try {
db.run(sql`ALTER TABLE deployments ADD COLUMN clear_cache integer NOT NULL DEFAULT 0`);
console.log("[Migrate] Added clear_cache column to deployments table");
} catch (err) {
if (err instanceof Error && err.message.includes("duplicate column name")) return;
console.error("[Migrate] Failed to add clear_cache column:", err);
throw err;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

const seedFromConfig = async () => {
if (config.githubClientId && config.githubClientSecret) {
const existing = await getGithubIntegration();
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/db/migrations/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
"breakpoints": true
}
]
}
}
2 changes: 2 additions & 0 deletions apps/api/src/db/repo/deployments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const mapDeployment = (row: typeof deployments.$inferSelect): Deployment => ({
replicas: row.replicas,
environment: row.environment,
failureReason: row.failureReason,
clearCache: Boolean(row.clearCache),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
});
Expand All @@ -38,6 +39,7 @@ export const createDeployment = async (input: CreateDeploymentInput): Promise<De
branch: input.branch ?? null,
commitSha: input.commitSha ?? null,
environment: input.environment ?? null,
clearCache: input.clearCache ? 1 : 0,
createdAt: timestamp,
updatedAt: timestamp,
}).run();
Expand Down
4 changes: 4 additions & 0 deletions apps/api/src/db/repo/scaling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const upsertScalingPolicy = async (input: CreateScalingPolicyInput): Prom
if (input.maxReplicas !== undefined) updates.maxReplicas = input.maxReplicas;
if (input.cpuThresholdPercent !== undefined) updates.cpuThresholdPercent = input.cpuThresholdPercent;
if (input.memoryThresholdPercent !== undefined) updates.memoryThresholdPercent = input.memoryThresholdPercent;
if (input.cooldownSeconds !== undefined) updates.cooldownSeconds = input.cooldownSeconds;
if (input.enabled !== undefined) updates.enabled = input.enabled ? 1 : 0;
db.update(scalingPolicies).set(updates).where(eq(scalingPolicies.projectId, input.projectId)).run();
return mapScalingPolicy(db.select().from(scalingPolicies).where(eq(scalingPolicies.projectId, input.projectId)).get()!);
}
Expand All @@ -39,6 +41,8 @@ export const upsertScalingPolicy = async (input: CreateScalingPolicyInput): Prom
maxReplicas: input.maxReplicas ?? 5,
cpuThresholdPercent: input.cpuThresholdPercent ?? 70,
memoryThresholdPercent: input.memoryThresholdPercent ?? 85,
cooldownSeconds: input.cooldownSeconds ?? 120,
enabled: input.enabled !== undefined ? (input.enabled ? 1 : 0) : 1,
createdAt: timestamp,
updatedAt: timestamp,
}).run();
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const deployments = sqliteTable("deployments", {
replicas: integer().notNull().default(1),
environment: text(),
failureReason: text("failure_reason"),
clearCache: integer("clear_cache").notNull().default(0),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull(),
});
Expand Down
11 changes: 7 additions & 4 deletions apps/api/src/orchestrator/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,21 +316,24 @@ export class PipelineOrchestrator {
deployment.sourceType ===
"git"
) {
const branchLabel =
deployment.branch
const commitLabel = deployment.commitSha
? ` (commit ${deployment.commitSha.slice(0, 7)})`
: deployment.branch
? ` (branch ${deployment.branch})`
: "";
await emitLog(
deploymentId,
"build",
`Cloning git repository: ${deployment.sourceRef}${branchLabel}`,
`Cloning git repository: ${deployment.sourceRef}${commitLabel}`,
);
workspacePath =
await prepareSourceWorkspace(
deploymentId,
deployment.sourceRef,
deployment.branch ??
undefined,
deployment.commitSha ??
undefined,
);
const sha = await getHeadSha(
workspacePath,
Expand Down Expand Up @@ -382,7 +385,7 @@ export class PipelineOrchestrator {
line,
);
},
{ cacheKey, sourceDir: project?.sourceDir, signal: controller.signal },
{ cacheKey, sourceDir: project?.sourceDir, signal: controller.signal, clearCache: deployment.clearCache },
);
} else {
await emitLog(
Expand Down
8 changes: 7 additions & 1 deletion apps/api/src/orchestrator/railpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,7 @@ export const buildWithRailpack = async (
cacheKey?: string;
sourceDir?: string | null;
signal?: AbortSignal;
clearCache?: boolean;
},
): Promise<RailpackBuildResult> => {
await onLog(
Expand All @@ -731,13 +732,18 @@ export const buildWithRailpack = async (
// Ensure builder exists (persists across builds)
await ensureBuilder();

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

if (opts?.clearCache) {
cacheKey = `${cacheKey}-clear-${Date.now()}`;
await onLog(`Bypassing cache for clean build (cacheKey: ${cacheKey})`);
}

const cleanSourceDir = opts?.sourceDir
? opts.sourceDir.replace(/^\//, "")
: null;
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/orchestrator/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const ensureContainerRunning = async (containerName: string) => {
};

export const reloadCaddy = async () => {
if (process.env.NODE_ENV === 'test') return;
const caddyContainer = await getCaddyContainer();
await run(dockerBin, ['exec', caddyContainer, 'caddy', 'reload', '--config', '/etc/caddy/Caddyfile']);
};
Expand Down
22 changes: 17 additions & 5 deletions apps/api/src/orchestrator/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,26 @@ const run = (cmd: string, args: string[], cwd?: string) =>
});
});

export const prepareSourceWorkspace = async (deploymentId: string, gitUrl: string, branch?: string) => {
const isValidSha = (s: string) => /^[0-9a-f]{7,40}$/i.test(s);

export const prepareSourceWorkspace = async (deploymentId: string, gitUrl: string, branch?: string, commitSha?: string) => {
const root = join(config.workspaceRoot, deploymentId);
await rm(root, { recursive: true, force: true });
await mkdir(root, { recursive: true });
const args = ['clone', '--depth', '1'];
if (branch) args.push('--branch', branch);
args.push(gitUrl, root);
await run('git', args);
if (commitSha) {
if (!isValidSha(commitSha)) {
throw new Error(`Invalid commit SHA: ${commitSha}`);
}
await run('git', ['init'], root);
await run('git', ['remote', 'add', 'origin', gitUrl], root);
await run('git', ['fetch', '--depth', '1', 'origin', commitSha], root);
await run('git', ['checkout', 'FETCH_HEAD'], root);
} else {
const args = ['clone', '--depth', '1'];
if (branch) args.push('--branch', branch);
args.push(gitUrl, root);
await run('git', args);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return root;
};

Expand Down
Loading
Loading