Add web-based update system with detached process management (#65)

* feat: Add version checking and update functionality

- Add version display component with GitHub release comparison
- Implement update.sh script execution via API
- Add hover tooltip with update instructions
- Create shadcn/ui style Badge component
- Add version router with getCurrentVersion, getLatestRelease, and executeUpdate endpoints
- Update homepage header to show version and update status
- Add Update Now button with loading states and result feedback
- Support automatic page refresh after successful update

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Workflow

* Workflow

* Workflow

* Update update script

* Update update script

* Update update script

* Update update script

* Update update script

* Update update.sh

* Update update.sh

* Update update.sh

* Update update.sh
This commit is contained in:
Michel Roegl-Brunner
2025-10-07 16:10:51 +02:00
committed by GitHub
parent 0b1ce29b64
commit 24430ee77d
9 changed files with 1323 additions and 2 deletions

View File

@@ -1,6 +1,7 @@
import { scriptsRouter } from "~/server/api/routers/scripts";
import { installedScriptsRouter } from "~/server/api/routers/installedScripts";
import { serversRouter } from "~/server/api/routers/servers";
import { versionRouter } from "~/server/api/routers/version";
import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
/**
@@ -12,6 +13,7 @@ export const appRouter = createTRPCRouter({
scripts: scriptsRouter,
installedScripts: installedScriptsRouter,
servers: serversRouter,
version: versionRouter,
});
// export type definition of API

View File

@@ -0,0 +1,148 @@
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { readFile } from "fs/promises";
import { join } from "path";
import { spawn } from "child_process";
interface GitHubRelease {
tag_name: string;
name: string;
published_at: string;
html_url: string;
}
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 fetch('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 fetch('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
}
};
} 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
};
}
}),
// Execute update script
executeUpdate: publicProcedure
.mutation(async () => {
try {
const updateScriptPath = join(process.cwd(), 'update.sh');
// Spawn the update script as a detached process using nohup
// This allows it to run independently and kill the parent Node.js process
const child = spawn('nohup', ['bash', updateScriptPath], {
cwd: process.cwd(),
stdio: ['ignore', 'ignore', 'ignore'],
shell: false,
detached: true
});
// 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'
};
}
})
});