Adds success and error logging to the Next.js app preparation process in server.js, including guidance for missing production builds. In versionRouter, always fetches the latest update.sh from GitHub before running updates, logging the outcome and falling back to the local script if fetching fails.
299 lines
10 KiB
TypeScript
299 lines
10 KiB
TypeScript
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
|
import { readFile, writeFile, stat } from "fs/promises";
|
|
import { join } from "path";
|
|
import { spawn } from "child_process";
|
|
import { env } from "~/env";
|
|
import { existsSync, createWriteStream } from "fs";
|
|
import stripAnsi from "strip-ansi";
|
|
|
|
interface GitHubRelease {
|
|
tag_name: string;
|
|
name: string;
|
|
published_at: string;
|
|
html_url: string;
|
|
body: string;
|
|
}
|
|
|
|
// Helper function to fetch from GitHub API with optional authentication
|
|
async function fetchGitHubAPI(url: string) {
|
|
const headers: HeadersInit = {
|
|
'Accept': 'application/vnd.github.v3+json',
|
|
'User-Agent': 'ProxmoxVE-Local'
|
|
};
|
|
|
|
// Add authentication header if token is available
|
|
if (env.GITHUB_TOKEN) {
|
|
headers.Authorization = `token ${env.GITHUB_TOKEN}`;
|
|
}
|
|
|
|
return fetch(url, { headers });
|
|
}
|
|
|
|
export const versionRouter = createTRPCRouter({
|
|
// Get current local version
|
|
getCurrentVersion: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const versionPath = join(process.cwd(), 'VERSION');
|
|
const version = await readFile(versionPath, 'utf-8');
|
|
return {
|
|
success: true,
|
|
version: version.trim()
|
|
};
|
|
} catch (error) {
|
|
console.error('Error reading VERSION file:', error);
|
|
return {
|
|
success: false,
|
|
error: 'Failed to read VERSION file',
|
|
version: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
getLatestRelease: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GitHub API error: ${response.status}`);
|
|
}
|
|
|
|
const release: GitHubRelease = await response.json();
|
|
|
|
return {
|
|
success: true,
|
|
release: {
|
|
tagName: release.tag_name,
|
|
name: release.name,
|
|
publishedAt: release.published_at,
|
|
htmlUrl: release.html_url
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Error fetching latest release:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch latest release',
|
|
release: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
|
|
getVersionStatus: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
|
|
const versionPath = join(process.cwd(), 'VERSION');
|
|
const currentVersion = (await readFile(versionPath, 'utf-8')).trim();
|
|
|
|
|
|
const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases/latest');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GitHub API error: ${response.status}`);
|
|
}
|
|
|
|
const release: GitHubRelease = await response.json();
|
|
const latestVersion = release.tag_name.replace('v', '');
|
|
|
|
|
|
const isUpToDate = currentVersion === latestVersion;
|
|
|
|
return {
|
|
success: true,
|
|
currentVersion,
|
|
latestVersion,
|
|
isUpToDate,
|
|
updateAvailable: !isUpToDate,
|
|
releaseInfo: {
|
|
tagName: release.tag_name,
|
|
name: release.name,
|
|
publishedAt: release.published_at,
|
|
htmlUrl: release.html_url,
|
|
body: release.body
|
|
}
|
|
};
|
|
} catch (error) {
|
|
console.error('Error checking version status:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to check version status',
|
|
currentVersion: null,
|
|
latestVersion: null,
|
|
isUpToDate: false,
|
|
updateAvailable: false,
|
|
releaseInfo: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get all releases for release notes
|
|
getAllReleases: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases');
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GitHub API error: ${response.status}`);
|
|
}
|
|
|
|
const releases: GitHubRelease[] = await response.json();
|
|
|
|
// Sort by published date (newest first)
|
|
const sortedReleases = releases
|
|
.filter(release => !release.tag_name.includes('beta') && !release.tag_name.includes('alpha'))
|
|
.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime());
|
|
|
|
return {
|
|
success: true,
|
|
releases: sortedReleases.map(release => ({
|
|
tagName: release.tag_name,
|
|
name: release.name,
|
|
publishedAt: release.published_at,
|
|
htmlUrl: release.html_url,
|
|
body: release.body
|
|
}))
|
|
};
|
|
} catch (error) {
|
|
console.error('Error fetching all releases:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to fetch releases',
|
|
releases: []
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Get update logs from the log file
|
|
getUpdateLogs: publicProcedure
|
|
.query(async () => {
|
|
try {
|
|
const logPath = join(process.cwd(), 'update.log');
|
|
|
|
if (!existsSync(logPath)) {
|
|
return {
|
|
success: true,
|
|
logs: [],
|
|
isComplete: false,
|
|
logFileModifiedTime: null
|
|
};
|
|
}
|
|
|
|
// Get log file modification time for session validation
|
|
let logFileModifiedTime: number | null = null;
|
|
try {
|
|
const stats = await stat(logPath);
|
|
logFileModifiedTime = stats.mtimeMs;
|
|
} catch (statError) {
|
|
// If we can't get stats, continue without timestamp
|
|
console.warn('Could not get log file stats:', statError);
|
|
}
|
|
|
|
const logs = await readFile(logPath, 'utf-8');
|
|
const logLines = logs.split('\n')
|
|
.filter(line => line.trim())
|
|
.map(line => stripAnsi(line)); // Strip ANSI color codes
|
|
|
|
// Check if update is complete by looking for completion indicators
|
|
const isComplete = logLines.some(line =>
|
|
line.includes('Update complete') ||
|
|
line.includes('Server restarting') ||
|
|
line.includes('npm start') ||
|
|
line.includes('Restarting server') ||
|
|
line.includes('Server started') ||
|
|
line.includes('Ready on http') ||
|
|
line.includes('Application started') ||
|
|
line.includes('Service enabled and started successfully') ||
|
|
line.includes('Service is running') ||
|
|
line.includes('Update completed successfully')
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
logs: logLines,
|
|
isComplete,
|
|
logFileModifiedTime
|
|
};
|
|
} catch (error) {
|
|
console.error('Error reading update logs:', error);
|
|
return {
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to read update logs',
|
|
logs: [],
|
|
isComplete: false,
|
|
logFileModifiedTime: null
|
|
};
|
|
}
|
|
}),
|
|
|
|
// Execute update script
|
|
executeUpdate: publicProcedure
|
|
.mutation(async () => {
|
|
try {
|
|
const updateScriptPath = join(process.cwd(), 'update.sh');
|
|
const logPath = join(process.cwd(), 'update.log');
|
|
|
|
// Clear/create the log file
|
|
await writeFile(logPath, '', 'utf-8');
|
|
|
|
// Always fetch the latest update.sh from GitHub before running
|
|
// This ensures we always use the newest update script, avoiding
|
|
// the "chicken-and-egg" problem where old scripts can't update properly
|
|
const updateScriptUrl = 'https://raw.githubusercontent.com/community-scripts/ProxmoxVE-Local/main/update.sh';
|
|
try {
|
|
const response = await fetch(updateScriptUrl);
|
|
if (response.ok) {
|
|
const latestScript = await response.text();
|
|
await writeFile(updateScriptPath, latestScript, { mode: 0o755 });
|
|
// Log that we fetched the latest script
|
|
await writeFile(logPath, '[INFO] Fetched latest update.sh from GitHub\n', { flag: 'a' });
|
|
} else {
|
|
// If fetch fails, log warning but continue with local script
|
|
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh (HTTP ${response.status}), using local version\n`, { flag: 'a' });
|
|
}
|
|
} catch (fetchError) {
|
|
// If fetch fails, log warning but continue with local script
|
|
const errorMsg = fetchError instanceof Error ? fetchError.message : 'Unknown error';
|
|
await writeFile(logPath, `[WARNING] Could not fetch latest update.sh: ${errorMsg}, using local version\n`, { flag: 'a' });
|
|
}
|
|
|
|
// Spawn the update script as a detached process using nohup
|
|
// This allows it to run independently and kill the parent Node.js process
|
|
// Redirect output to log file
|
|
const child = spawn('bash', [updateScriptPath], {
|
|
cwd: process.cwd(),
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
shell: false,
|
|
detached: true
|
|
});
|
|
|
|
// Capture stdout and stderr to log file
|
|
const logStream = createWriteStream(logPath, { flags: 'a' });
|
|
child.stdout?.pipe(logStream);
|
|
child.stderr?.pipe(logStream);
|
|
|
|
// Unref the child process so it doesn't keep the parent alive
|
|
child.unref();
|
|
|
|
// Immediately return success since we can't wait for completion
|
|
// The script will handle its own logging and restart
|
|
return {
|
|
success: true,
|
|
message: 'Update started in background. The server will restart automatically when complete.',
|
|
output: '',
|
|
error: ''
|
|
};
|
|
} catch (error) {
|
|
console.error('Error executing update script:', error);
|
|
return {
|
|
success: false,
|
|
message: `Failed to execute update script: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
output: '',
|
|
error: error instanceof Error ? error.message : 'Unknown error'
|
|
};
|
|
}
|
|
})
|
|
});
|