Initial commit: Web2App static site to Tauri platform
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
170
server/routes/builds.ts
Normal file
170
server/routes/builds.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Router } from "express";
|
||||
import multer from "multer";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
getBuild,
|
||||
insertBuild,
|
||||
listBuilds,
|
||||
toPublicBuild,
|
||||
updateBuild,
|
||||
type BuildStatus,
|
||||
} from "../db/index.js";
|
||||
import {
|
||||
getActionsRunUrl,
|
||||
getReleaseAssets,
|
||||
getWorkflowRun,
|
||||
triggerBuildWorkflow,
|
||||
uploadSiteZip,
|
||||
} from "../services/github.js";
|
||||
import { slugifyIdentifier, validateZipBuffer, ZipValidationError } from "../services/zip.js";
|
||||
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 100 * 1024 * 1024 },
|
||||
});
|
||||
|
||||
const maxUploadMb = Number(process.env.MAX_UPLOAD_MB ?? "50");
|
||||
const maxUploadBytes = maxUploadMb * 1024 * 1024;
|
||||
|
||||
export const buildsRouter = Router();
|
||||
|
||||
buildsRouter.get("/", (_req, res) => {
|
||||
const builds = listBuilds().map(toPublicBuild);
|
||||
res.json({ builds });
|
||||
});
|
||||
|
||||
buildsRouter.get("/:id", async (req, res) => {
|
||||
const record = getBuild(req.params.id);
|
||||
if (!record) {
|
||||
res.status(404).json({ error: "Build not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshed = await refreshBuildStatus(record);
|
||||
res.json({
|
||||
...toPublicBuild(refreshed),
|
||||
actionsUrl: getActionsRunUrl(refreshed.workflow_run_id),
|
||||
});
|
||||
});
|
||||
|
||||
buildsRouter.post("/", upload.single("file"), async (req, res) => {
|
||||
try {
|
||||
const appName = String(req.body.appName ?? "").trim();
|
||||
const identifierInput = String(req.body.identifier ?? "").trim();
|
||||
|
||||
if (!appName) {
|
||||
res.status(400).json({ error: "appName is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.file) {
|
||||
res.status(400).json({ error: "file is required" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!req.file.originalname.toLowerCase().endsWith(".zip")) {
|
||||
res.status(400).json({ error: "Only .zip files are supported" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { normalizedBuffer } = validateZipBuffer(req.file.buffer, maxUploadBytes);
|
||||
const jobId = nanoid(10);
|
||||
const appIdentifier =
|
||||
identifierInput ||
|
||||
`com.web2app.${slugifyIdentifier(appName) || jobId.toLowerCase()}`;
|
||||
|
||||
insertBuild({ id: jobId, appName, appIdentifier });
|
||||
|
||||
await uploadSiteZip(jobId, normalizedBuffer);
|
||||
updateBuild(jobId, { status: "queued" });
|
||||
|
||||
const workflowRunId = await triggerBuildWorkflow({
|
||||
jobId,
|
||||
appName,
|
||||
appIdentifier,
|
||||
});
|
||||
|
||||
updateBuild(jobId, {
|
||||
workflow_run_id: workflowRunId,
|
||||
status: "in_progress",
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: jobId,
|
||||
status: "in_progress",
|
||||
workflowRunId,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ZipValidationError) {
|
||||
res.status(400).json({ error: error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
res.status(500).json({
|
||||
error: error instanceof Error ? error.message : "Failed to create build",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshBuildStatus(record: ReturnType<typeof getBuild> & object) {
|
||||
if (!record.workflow_run_id) return record;
|
||||
if (record.status === "completed" || record.status === "failed") {
|
||||
return record;
|
||||
}
|
||||
|
||||
try {
|
||||
const run = await getWorkflowRun(record.workflow_run_id);
|
||||
|
||||
if (run.status === "queued") {
|
||||
return updateBuild(record.id, { status: "queued" }) ?? record;
|
||||
}
|
||||
|
||||
if (run.status === "in_progress") {
|
||||
return updateBuild(record.id, { status: "in_progress" }) ?? record;
|
||||
}
|
||||
|
||||
if (run.status === "completed") {
|
||||
if (run.conclusion === "success") {
|
||||
const assets = await getReleaseAssets(record.id);
|
||||
return (
|
||||
updateBuild(record.id, {
|
||||
status: "completed",
|
||||
windows_url: assets.windowsUrl,
|
||||
android_url: assets.androidUrl,
|
||||
error:
|
||||
assets.windowsUrl && assets.androidUrl
|
||||
? null
|
||||
: "Build finished but release assets were not found yet",
|
||||
}) ?? record
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
updateBuild(record.id, {
|
||||
status: "failed",
|
||||
error: `Workflow failed with conclusion: ${run.conclusion ?? "unknown"}`,
|
||||
}) ?? record
|
||||
);
|
||||
}
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
return (
|
||||
updateBuild(record.id, {
|
||||
status: "failed",
|
||||
error: error instanceof Error ? error.message : "Status refresh failed",
|
||||
}) ?? record
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildPublicMeta(record: ReturnType<typeof getBuild>) {
|
||||
if (!record) return null;
|
||||
return {
|
||||
...toPublicBuild(record),
|
||||
actionsUrl: getActionsRunUrl(record.workflow_run_id),
|
||||
};
|
||||
}
|
||||
|
||||
export type { BuildStatus };
|
||||
Reference in New Issue
Block a user