-
Notifications
You must be signed in to change notification settings - Fork 542
[Feature] Add auto-update system with git-based version checking #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
amoscicki
wants to merge
13
commits into
AutoMaker-Org:main
Choose a base branch
from
amoscicki:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4ae06dc
fix: improve auto-update check logic and UI refresh
amoscicki 21538cf
test: verify auto-update detection
amoscicki 78a434f
refactor: abstract update types for future flexibility
amoscicki 553d3d3
fix: add Later button to Update Available toast
amoscicki f912316
docs: add auto-updates documentation
amoscicki 33d95c8
fix: sync autoUpdate settings to server
amoscicki 29f9372
docs: add last updated date
amoscicki 4660f1c
fix: address PR review comments for auto-update feature
amoscicki b92d411
fix: address remaining CodeRabbit review comments
amoscicki 2620027
fix: make getAutomakerRoot more robust by traversing up for package.json
amoscicki 6ce4ec2
refactor: extract withTempGitRemote helper for code reuse
amoscicki 3e47a0d
fix: add defense-in-depth URL validation and remove merge conflict ma…
amoscicki 14f2c92
feat: add EventEmitter support to updates routes and fix fetchInfo pr…
amoscicki File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| /** | ||
| * Common utilities for update routes | ||
| */ | ||
|
|
||
| import { createLogger } from '@automaker/utils'; | ||
| import { exec } from 'child_process'; | ||
| import { promisify } from 'util'; | ||
| import path from 'path'; | ||
| import fs from 'fs'; | ||
| import crypto from 'crypto'; | ||
| import { fileURLToPath } from 'url'; | ||
| import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js'; | ||
|
|
||
| const logger = createLogger('Updates'); | ||
| export const execAsync = promisify(exec); | ||
|
|
||
| // Re-export shared utilities | ||
| export { getErrorMessageShared as getErrorMessage }; | ||
| export const logError = createLogError(logger); | ||
|
|
||
| // ============================================================================ | ||
| // Extended PATH configuration for Electron apps | ||
| // ============================================================================ | ||
|
|
||
| const pathSeparator = process.platform === 'win32' ? ';' : ':'; | ||
| const additionalPaths: string[] = []; | ||
|
|
||
| if (process.platform === 'win32') { | ||
| // Windows paths | ||
| if (process.env.LOCALAPPDATA) { | ||
| additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`); | ||
| } | ||
| if (process.env.PROGRAMFILES) { | ||
| additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`); | ||
| } | ||
| if (process.env['ProgramFiles(x86)']) { | ||
| additionalPaths.push(`${process.env['ProgramFiles(x86)']}\\Git\\cmd`); | ||
| } | ||
| } else { | ||
| // Unix/Mac paths | ||
| additionalPaths.push( | ||
| '/opt/homebrew/bin', // Homebrew on Apple Silicon | ||
| '/usr/local/bin', // Homebrew on Intel Mac, common Linux location | ||
| '/home/linuxbrew/.linuxbrew/bin' // Linuxbrew | ||
| ); | ||
| // pipx, other user installs - only add if HOME is defined | ||
| if (process.env.HOME) { | ||
| additionalPaths.push(`${process.env.HOME}/.local/bin`); | ||
| } | ||
| } | ||
|
|
||
| const extendedPath = [process.env.PATH, ...additionalPaths.filter(Boolean)] | ||
| .filter(Boolean) | ||
| .join(pathSeparator); | ||
|
|
||
| /** | ||
| * Environment variables with extended PATH for executing shell commands. | ||
| */ | ||
| export const execEnv = { | ||
| ...process.env, | ||
| PATH: extendedPath, | ||
| }; | ||
|
|
||
| // ============================================================================ | ||
| // Automaker installation path | ||
| // ============================================================================ | ||
|
|
||
| /** | ||
| * Get the root directory of the Automaker installation. | ||
| * Traverses up from the current file looking for a package.json with name "automaker". | ||
| * This approach is more robust than using fixed relative paths. | ||
| */ | ||
| export function getAutomakerRoot(): string { | ||
| const __filename = fileURLToPath(import.meta.url); | ||
| let currentDir = path.dirname(__filename); | ||
| const root = path.parse(currentDir).root; | ||
|
|
||
| while (currentDir !== root) { | ||
| const packageJsonPath = path.join(currentDir, 'package.json'); | ||
| if (fs.existsSync(packageJsonPath)) { | ||
| try { | ||
| const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); | ||
| // Look for the monorepo root package.json with name "automaker" | ||
| if (packageJson.name === 'automaker') { | ||
| return currentDir; | ||
| } | ||
| } catch { | ||
| // Ignore JSON parse errors, continue searching | ||
| } | ||
| } | ||
| currentDir = path.dirname(currentDir); | ||
| } | ||
|
|
||
| // Fallback to fixed path if marker not found (shouldn't happen in normal usage) | ||
| const fallbackDir = path.dirname(__filename); | ||
| return path.resolve(fallbackDir, '..', '..', '..', '..', '..'); | ||
| } | ||
|
|
||
| /** | ||
| * Check if git is available on the system | ||
| */ | ||
| export async function isGitAvailable(): Promise<boolean> { | ||
| try { | ||
| await execAsync('git --version', { env: execEnv }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Check if a path is a git repository | ||
| */ | ||
| export async function isGitRepo(repoPath: string): Promise<boolean> { | ||
| try { | ||
| await execAsync('git rev-parse --is-inside-work-tree', { cwd: repoPath, env: execEnv }); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get the current HEAD commit hash | ||
| */ | ||
| export async function getCurrentCommit(repoPath: string): Promise<string> { | ||
| const { stdout } = await execAsync('git rev-parse HEAD', { cwd: repoPath, env: execEnv }); | ||
| return stdout.trim(); | ||
| } | ||
|
|
||
| /** | ||
| * Get the short version of a commit hash | ||
| */ | ||
| export async function getShortCommit(repoPath: string): Promise<string> { | ||
| const { stdout } = await execAsync('git rev-parse --short HEAD', { cwd: repoPath, env: execEnv }); | ||
| return stdout.trim(); | ||
| } | ||
|
|
||
| /** | ||
| * Check if the repo has local uncommitted changes | ||
| */ | ||
| export async function hasLocalChanges(repoPath: string): Promise<boolean> { | ||
| const { stdout } = await execAsync('git status --porcelain', { cwd: repoPath, env: execEnv }); | ||
| return stdout.trim().length > 0; | ||
| } | ||
|
|
||
| /** | ||
| * Validate that a URL looks like a valid git remote URL. | ||
| * Also blocks shell metacharacters to prevent command injection. | ||
| */ | ||
| export function isValidGitUrl(url: string): boolean { | ||
| // Allow HTTPS, SSH, and git protocols | ||
| const startsWithValidProtocol = | ||
| url.startsWith('https://') || | ||
| url.startsWith('git@') || | ||
| url.startsWith('git://') || | ||
| url.startsWith('ssh://'); | ||
|
|
||
| // Block shell metacharacters to prevent command injection | ||
| const hasShellChars = /[;`|&<>()$!\\[\] ]/.test(url); | ||
|
|
||
| return startsWithValidProtocol && !hasShellChars; | ||
| } | ||
|
|
||
| /** | ||
| * Execute a callback with a temporary git remote, ensuring cleanup. | ||
| * Centralizes the pattern of adding a temp remote, doing work, and removing it. | ||
| */ | ||
| export async function withTempGitRemote<T>( | ||
| installPath: string, | ||
| sourceUrl: string, | ||
| callback: (tempRemoteName: string) => Promise<T> | ||
| ): Promise<T> { | ||
| // Defense-in-depth: validate URL even though callers should already validate | ||
| if (!isValidGitUrl(sourceUrl)) { | ||
| throw new Error('Invalid git URL format'); | ||
| } | ||
|
|
||
| const tempRemoteName = `automaker-temp-remote-${crypto.randomBytes(8).toString('hex')}`; | ||
| try { | ||
| await execAsync(`git remote add ${tempRemoteName} "${sourceUrl}"`, { | ||
| cwd: installPath, | ||
| env: execEnv, | ||
| }); | ||
| return await callback(tempRemoteName); | ||
| } finally { | ||
| try { | ||
| await execAsync(`git remote remove ${tempRemoteName}`, { | ||
| cwd: installPath, | ||
| env: execEnv, | ||
| }); | ||
| } catch { | ||
| // Ignore cleanup errors | ||
| } | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| /** | ||
| * Update routes - HTTP API for checking and applying updates | ||
| * | ||
| * Provides endpoints for: | ||
| * - Checking if updates are available from upstream | ||
| * - Pulling updates from upstream | ||
| * - Getting current installation info | ||
| */ | ||
|
|
||
| import { Router } from 'express'; | ||
| import type { SettingsService } from '../../services/settings-service.js'; | ||
| import { createCheckHandler } from './routes/check.js'; | ||
| import { createPullHandler } from './routes/pull.js'; | ||
| import { createInfoHandler } from './routes/info.js'; | ||
|
|
||
| export function createUpdatesRoutes(settingsService: SettingsService): Router { | ||
| const router = Router(); | ||
|
|
||
| // GET /api/updates/check - Check if updates are available | ||
| router.get('/check', createCheckHandler(settingsService)); | ||
|
|
||
| // POST /api/updates/pull - Pull updates from upstream | ||
| router.post('/pull', createPullHandler(settingsService)); | ||
|
|
||
| // GET /api/updates/info - Get current installation info | ||
| router.get('/info', createInfoHandler(settingsService)); | ||
|
|
||
| return router; | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.