feat: Implement SSH-based version checking for private repositories

- Replace HTTPS GitHub API calls with SSH git commands
- Use existing deploy key for private repository access
- Add proper error handling for SSH authentication issues
- Support fetching latest tags via git ls-remote
- Maintain compatibility with private repositories using deploy keys

This allows the version checking system to work with private repositories
that have SSH deploy keys configured, using the same authentication
as the local git operations.
This commit is contained in:
Muhammad Ibrahim
2025-09-17 22:16:52 +01:00
parent babf58bb98
commit 16821d6b5e
4 changed files with 238 additions and 24 deletions

View File

@@ -0,0 +1,160 @@
const express = require('express');
const { authenticateToken } = require('../middleware/auth');
const { requireManageSettings } = require('../middleware/permissions');
const { PrismaClient } = require('@prisma/client');
const { exec } = require('child_process');
const { promisify } = require('util');
const prisma = new PrismaClient();
const execAsync = promisify(exec);
const router = express.Router();
// Get current version info
router.get('/current', authenticateToken, async (req, res) => {
try {
// For now, return hardcoded version - this should match your agent version
const currentVersion = '1.2.3';
res.json({
version: currentVersion,
buildDate: new Date().toISOString(),
environment: process.env.NODE_ENV || 'development'
});
} catch (error) {
console.error('Error getting current version:', error);
res.status(500).json({ error: 'Failed to get current version' });
}
});
// Check for updates from GitHub
router.get('/check-updates', authenticateToken, requireManageSettings, async (req, res) => {
try {
// Get GitHub repo URL from settings
const settings = await prisma.settings.findFirst();
if (!settings || !settings.githubRepoUrl) {
return res.status(400).json({ error: 'GitHub repository URL not configured' });
}
// Extract owner and repo from GitHub URL
// Support both SSH and HTTPS formats:
// git@github.com:owner/repo.git
// https://github.com/owner/repo.git
const repoUrl = settings.githubRepoUrl;
let owner, repo;
if (repoUrl.includes('git@github.com:')) {
const match = repoUrl.match(/git@github\.com:([^\/]+)\/([^\/]+)\.git/);
if (match) {
[, owner, repo] = match;
}
} else if (repoUrl.includes('github.com/')) {
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
if (match) {
[, owner, repo] = match;
}
}
if (!owner || !repo) {
return res.status(400).json({ error: 'Invalid GitHub repository URL format' });
}
// Use SSH to fetch latest tag from private repository
const sshRepoUrl = `git@github.com:${owner}/${repo}.git`;
try {
// Fetch the latest tag using SSH
const { stdout: latestTag } = await execAsync(
`git ls-remote --tags --sort=-version:refname ${sshRepoUrl} | head -n 1 | sed 's/.*refs\\/tags\\///' | sed 's/\\^\\{\\}//'`,
{ timeout: 10000 }
);
const latestVersion = latestTag.trim().replace('v', ''); // Remove 'v' prefix
const currentVersion = '1.2.3';
// Simple version comparison (assumes semantic versioning)
const isUpdateAvailable = compareVersions(latestVersion, currentVersion) > 0;
// Get additional tag information
let tagInfo = {};
try {
const { stdout: tagDetails } = await execAsync(
`git ls-remote --tags ${sshRepoUrl} | grep "${latestTag.trim()}" | head -n 1`,
{ timeout: 5000 }
);
// Extract commit hash and other details if needed
const parts = tagDetails.trim().split('\t');
if (parts.length >= 2) {
tagInfo.commitHash = parts[0];
}
} catch (tagDetailError) {
console.warn('Could not fetch tag details:', tagDetailError.message);
}
res.json({
currentVersion,
latestVersion,
isUpdateAvailable,
latestRelease: {
tagName: latestTag.trim(),
version: latestVersion,
commitHash: tagInfo.commitHash,
repository: `${owner}/${repo}`,
sshUrl: sshRepoUrl
}
});
} catch (sshError) {
console.error('SSH Git error:', sshError.message);
// Check if it's a permission/access issue
if (sshError.message.includes('Permission denied') || sshError.message.includes('Host key verification failed')) {
return res.status(403).json({
error: 'SSH access denied to repository',
suggestion: 'Ensure your SSH key is properly configured and has access to the repository. Check your ~/.ssh/config and known_hosts.'
});
}
if (sshError.message.includes('not found') || sshError.message.includes('does not exist')) {
return res.status(404).json({
error: 'Repository not found',
suggestion: 'Check that the repository URL is correct and accessible.'
});
}
return res.status(500).json({
error: 'Failed to fetch repository information',
details: sshError.message,
suggestion: 'Check SSH key configuration and repository access permissions.'
});
}
} catch (error) {
console.error('Error checking for updates:', error);
res.status(500).json({
error: 'Failed to check for updates',
details: error.message
});
}
});
// Simple version comparison function
function compareVersions(version1, version2) {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part) return 1;
if (v1Part < v2Part) return -1;
}
return 0;
}
module.exports = router;

View File

@@ -16,6 +16,7 @@ const permissionsRoutes = require('./routes/permissionsRoutes');
const settingsRoutes = require('./routes/settingsRoutes');
const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes');
const repositoryRoutes = require('./routes/repositoryRoutes');
const versionRoutes = require('./routes/versionRoutes');
// Initialize Prisma client
const prisma = new PrismaClient();
@@ -134,6 +135,7 @@ app.use(`/api/${apiVersion}/permissions`, permissionsRoutes);
app.use(`/api/${apiVersion}/settings`, settingsRoutes);
app.use(`/api/${apiVersion}/dashboard-preferences`, dashboardPreferencesRoutes);
app.use(`/api/${apiVersion}/repositories`, repositoryRoutes);
app.use(`/api/${apiVersion}/version`, versionRoutes);
// Error handling middleware
app.use((err, req, res, next) => {

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Save, Server, Globe, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon } from 'lucide-react';
import { settingsAPI, agentVersionAPI } from '../utils/api';
import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api';
const Settings = () => {
const [formData, setFormData] = useState({
@@ -37,6 +37,15 @@ const Settings = () => {
isDefault: false
});
// Version checking state
const [versionInfo, setVersionInfo] = useState({
currentVersion: '1.2.3',
latestVersion: null,
isUpdateAvailable: false,
checking: false,
error: null
});
const queryClient = useQueryClient();
// Fetch current settings
@@ -152,6 +161,31 @@ const Settings = () => {
}
});
// Version checking functions
const checkForUpdates = async () => {
setVersionInfo(prev => ({ ...prev, checking: true, error: null }));
try {
const response = await versionAPI.checkUpdates();
const data = response.data;
setVersionInfo({
currentVersion: data.currentVersion,
latestVersion: data.latestVersion,
isUpdateAvailable: data.isUpdateAvailable,
checking: false,
error: null
});
} catch (error) {
console.error('Version check error:', error);
setVersionInfo(prev => ({
...prev,
checking: false,
error: error.response?.data?.error || 'Failed to check for updates'
}));
}
};
const handleInputChange = (field, value) => {
console.log(`handleInputChange: ${field} = ${value}`);
setFormData(prev => {
@@ -694,7 +728,7 @@ const Settings = () => {
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">Current Version</span>
</div>
<span className="text-lg font-mono text-secondary-900 dark:text-white">1.2.3</span>
<span className="text-lg font-mono text-secondary-900 dark:text-white">{versionInfo.currentVersion}</span>
</div>
<div className="bg-white dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-600">
@@ -702,20 +736,29 @@ const Settings = () => {
<Download className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<span className="text-sm font-medium text-secondary-700 dark:text-secondary-300">Latest Version</span>
</div>
<span className="text-lg font-mono text-secondary-900 dark:text-white">Checking...</span>
<span className="text-lg font-mono text-secondary-900 dark:text-white">
{versionInfo.checking ? (
<span className="text-blue-600 dark:text-blue-400">Checking...</span>
) : versionInfo.latestVersion ? (
<span className={versionInfo.isUpdateAvailable ? 'text-orange-600 dark:text-orange-400' : 'text-green-600 dark:text-green-400'}>
{versionInfo.latestVersion}
{versionInfo.isUpdateAvailable && ' (Update Available!)'}
</span>
) : (
<span className="text-secondary-500 dark:text-secondary-400">Not checked</span>
)}
</span>
</div>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => {
// TODO: Implement version check
console.log('Checking for updates...');
}}
onClick={checkForUpdates}
disabled={versionInfo.checking}
className="btn-primary flex items-center gap-2"
>
<Download className="h-4 w-4" />
Check for Updates
{versionInfo.checking ? 'Checking...' : 'Check for Updates'}
</button>
<button
@@ -729,25 +772,28 @@ const Settings = () => {
Enable Notifications
</button>
</div>
{versionInfo.error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 rounded-lg p-4">
<div className="flex">
<AlertCircle className="h-5 w-5 text-red-400 dark:text-red-300" />
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">Version Check Failed</h3>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
{versionInfo.error}
</p>
{versionInfo.error.includes('private') && (
<p className="mt-2 text-xs text-red-600 dark:text-red-400">
For private repositories, you may need to configure GitHub authentication or make the repository public.
</p>
)}
</div>
</div>
</div>
)}
</div>
</div>
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-lg p-4">
<div className="flex">
<AlertCircle className="h-5 w-5 text-amber-400 dark:text-amber-300" />
<div className="ml-3">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">Setup Instructions</h3>
<div className="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p className="mb-2">To enable version checking, you need to:</p>
<ol className="list-decimal list-inside space-y-1 ml-4">
<li>Create a version tag (e.g., v1.2.3) in your GitHub repository</li>
<li>Ensure the repository is publicly accessible or configure access tokens</li>
<li>Click "Check for Updates" to verify the connection</li>
</ol>
</div>
</div>
</div>
</div>
</div>
)}
</div>

View File

@@ -178,6 +178,12 @@ export const formatDate = (date) => {
return new Date(date).toLocaleString()
}
// Version API
export const versionAPI = {
getCurrent: () => api.get('/version/current'),
checkUpdates: () => api.get('/version/check-updates'),
}
export const formatRelativeTime = (date) => {
const now = new Date()
const diff = now - new Date(date)