feat: support an explicit start command override per installation
Some checks failed
CI / test (push) Has been cancelled
Some checks failed
CI / test (push) Has been cancelled
Needed to install third-party repos we don't control (e.g. the upstream gitea.com/gitea/gitea-mcp project) where nixpacks detects a working build but the wrong/no start command, and pushing a nixpacks.toml to the source repo isn't an option. Passed through as nixpacks' own --start-cmd flag. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,8 @@ installationsRouter.post("/", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
installationsRouter.patch("/:id/config", (req, res) => {
|
installationsRouter.patch("/:id/config", (req, res) => {
|
||||||
const { env, portEnvVar, hostEnvVar, httpPath, internalPort, basicAuthUsername, basicAuthPassword } = req.body ?? {};
|
const { env, portEnvVar, hostEnvVar, httpPath, internalPort, basicAuthUsername, basicAuthPassword, startCommand } =
|
||||||
|
req.body ?? {};
|
||||||
try {
|
try {
|
||||||
const installation = configureInstallation(req.params.id, {
|
const installation = configureInstallation(req.params.id, {
|
||||||
env,
|
env,
|
||||||
@@ -68,6 +69,7 @@ installationsRouter.patch("/:id/config", (req, res) => {
|
|||||||
internalPort,
|
internalPort,
|
||||||
basicAuthUsername,
|
basicAuthUsername,
|
||||||
basicAuthPassword,
|
basicAuthPassword,
|
||||||
|
startCommand,
|
||||||
});
|
});
|
||||||
res.json(toPublicJson(installation));
|
res.json(toPublicJson(installation));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -54,7 +54,12 @@ export function detectNodeVersion(repoDir: string): string | undefined {
|
|||||||
return DEFAULT_NODE_VERSION;
|
return DEFAULT_NODE_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runNixpacksBuild(slug: string, repoDir: string, imageTag: string): Promise<BuildResult> {
|
export function runNixpacksBuild(
|
||||||
|
slug: string,
|
||||||
|
repoDir: string,
|
||||||
|
imageTag: string,
|
||||||
|
startCommand?: string | null,
|
||||||
|
): Promise<BuildResult> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const buildId = Date.now().toString();
|
const buildId = Date.now().toString();
|
||||||
const logPath = buildLogPath(slug, buildId);
|
const logPath = buildLogPath(slug, buildId);
|
||||||
@@ -62,6 +67,13 @@ export function runNixpacksBuild(slug: string, repoDir: string, imageTag: string
|
|||||||
const logStream = fs.createWriteStream(logPath, { flags: "a" });
|
const logStream = fs.createWriteStream(logPath, { flags: "a" });
|
||||||
|
|
||||||
const args = ["build", repoDir, "--name", imageTag];
|
const args = ["build", repoDir, "--name", imageTag];
|
||||||
|
if (startCommand) {
|
||||||
|
// Lets an installation override the start command without needing a nixpacks.toml in
|
||||||
|
// the source repo — necessary for third-party repos we don't control (e.g. installing
|
||||||
|
// gitea.com/gitea/gitea-mcp as-is, which nixpacks builds fine but whose detected start
|
||||||
|
// command needs CLI flags like `-t http -p <port>` appended).
|
||||||
|
args.push("--start-cmd", startCommand);
|
||||||
|
}
|
||||||
const nodeVersion = detectNodeVersion(repoDir);
|
const nodeVersion = detectNodeVersion(repoDir);
|
||||||
if (nodeVersion) {
|
if (nodeVersion) {
|
||||||
// nixpacks does NOT read NIXPACKS_* config from the process environment — only from
|
// nixpacks does NOT read NIXPACKS_* config from the process environment — only from
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS installations (
|
|||||||
for (const ddl of [
|
for (const ddl of [
|
||||||
"ALTER TABLE installations ADD COLUMN basic_auth_username TEXT",
|
"ALTER TABLE installations ADD COLUMN basic_auth_username TEXT",
|
||||||
"ALTER TABLE installations ADD COLUMN basic_auth_password_encrypted TEXT",
|
"ALTER TABLE installations ADD COLUMN basic_auth_password_encrypted TEXT",
|
||||||
|
"ALTER TABLE installations ADD COLUMN start_command TEXT",
|
||||||
]) {
|
]) {
|
||||||
try {
|
try {
|
||||||
db.exec(ddl);
|
db.exec(ddl);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export interface Installation {
|
|||||||
bearerToken: string | null;
|
bearerToken: string | null;
|
||||||
basicAuthUsername: string | null;
|
basicAuthUsername: string | null;
|
||||||
basicAuthPassword: string | null;
|
basicAuthPassword: string | null;
|
||||||
|
startCommand: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -54,6 +55,7 @@ interface Row {
|
|||||||
env_json_encrypted: string | null;
|
env_json_encrypted: string | null;
|
||||||
basic_auth_username: string | null;
|
basic_auth_username: string | null;
|
||||||
basic_auth_password_encrypted: string | null;
|
basic_auth_password_encrypted: string | null;
|
||||||
|
start_command: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -78,6 +80,7 @@ function rowToInstallation(row: Row): Installation {
|
|||||||
bearerToken: row.bearer_token_encrypted ? decryptSecret(row.bearer_token_encrypted) : null,
|
bearerToken: row.bearer_token_encrypted ? decryptSecret(row.bearer_token_encrypted) : null,
|
||||||
basicAuthUsername: row.basic_auth_username,
|
basicAuthUsername: row.basic_auth_username,
|
||||||
basicAuthPassword: row.basic_auth_password_encrypted ? decryptSecret(row.basic_auth_password_encrypted) : null,
|
basicAuthPassword: row.basic_auth_password_encrypted ? decryptSecret(row.basic_auth_password_encrypted) : null,
|
||||||
|
startCommand: row.start_command,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
};
|
};
|
||||||
@@ -140,6 +143,7 @@ export interface InstallationUpdate {
|
|||||||
bearerToken?: string | null;
|
bearerToken?: string | null;
|
||||||
basicAuthUsername?: string | null;
|
basicAuthUsername?: string | null;
|
||||||
basicAuthPassword?: string | null;
|
basicAuthPassword?: string | null;
|
||||||
|
startCommand?: string | null;
|
||||||
gitRef?: string | null;
|
gitRef?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +166,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta
|
|||||||
patch.basicAuthUsername !== undefined ? patch.basicAuthUsername : current.basicAuthUsername,
|
patch.basicAuthUsername !== undefined ? patch.basicAuthUsername : current.basicAuthUsername,
|
||||||
basicAuthPassword:
|
basicAuthPassword:
|
||||||
patch.basicAuthPassword !== undefined ? patch.basicAuthPassword : current.basicAuthPassword,
|
patch.basicAuthPassword !== undefined ? patch.basicAuthPassword : current.basicAuthPassword,
|
||||||
|
startCommand: patch.startCommand !== undefined ? patch.startCommand : current.startCommand,
|
||||||
gitRef: patch.gitRef !== undefined ? patch.gitRef : current.gitRef,
|
gitRef: patch.gitRef !== undefined ? patch.gitRef : current.gitRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,6 +184,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta
|
|||||||
bearer_token_encrypted = @bearerTokenEncrypted,
|
bearer_token_encrypted = @bearerTokenEncrypted,
|
||||||
basic_auth_username = @basicAuthUsername,
|
basic_auth_username = @basicAuthUsername,
|
||||||
basic_auth_password_encrypted = @basicAuthPasswordEncrypted,
|
basic_auth_password_encrypted = @basicAuthPasswordEncrypted,
|
||||||
|
start_command = @startCommand,
|
||||||
git_ref = @gitRef,
|
git_ref = @gitRef,
|
||||||
updated_at = @updatedAt
|
updated_at = @updatedAt
|
||||||
WHERE id = @id`,
|
WHERE id = @id`,
|
||||||
@@ -196,6 +202,7 @@ export function updateInstallation(id: string, patch: InstallationUpdate): Insta
|
|||||||
bearerTokenEncrypted: next.bearerToken ? encryptSecret(next.bearerToken) : null,
|
bearerTokenEncrypted: next.bearerToken ? encryptSecret(next.bearerToken) : null,
|
||||||
basicAuthUsername: next.basicAuthUsername,
|
basicAuthUsername: next.basicAuthUsername,
|
||||||
basicAuthPasswordEncrypted: next.basicAuthPassword ? encryptSecret(next.basicAuthPassword) : null,
|
basicAuthPasswordEncrypted: next.basicAuthPassword ? encryptSecret(next.basicAuthPassword) : null,
|
||||||
|
startCommand: next.startCommand,
|
||||||
gitRef: next.gitRef,
|
gitRef: next.gitRef,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export function configureInstallation(
|
|||||||
internalPort?: number;
|
internalPort?: number;
|
||||||
basicAuthUsername?: string | null;
|
basicAuthUsername?: string | null;
|
||||||
basicAuthPassword?: string | null;
|
basicAuthPassword?: string | null;
|
||||||
|
startCommand?: string | null;
|
||||||
},
|
},
|
||||||
): Installation {
|
): Installation {
|
||||||
return updateInstallation(id, patch);
|
return updateInstallation(id, patch);
|
||||||
@@ -68,7 +69,12 @@ export async function buildInstallation(id: string): Promise<Installation> {
|
|||||||
const imageTag = `mcpmgr/${installation.slug}:latest`;
|
const imageTag = `mcpmgr/${installation.slug}:latest`;
|
||||||
const previousImageId = installation.imageTag ? await getImageId(installation.imageTag) : undefined;
|
const previousImageId = installation.imageTag ? await getImageId(installation.imageTag) : undefined;
|
||||||
|
|
||||||
const result = await runNixpacksBuild(installation.slug, repoDir(installation.slug), imageTag);
|
const result = await runNixpacksBuild(
|
||||||
|
installation.slug,
|
||||||
|
repoDir(installation.slug),
|
||||||
|
imageTag,
|
||||||
|
installation.startCommand,
|
||||||
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return updateInstallation(id, {
|
return updateInstallation(id, {
|
||||||
|
|||||||
Reference in New Issue
Block a user