toes/ISSUES.md
Chris Wanstrath ebf3ffc3af spicy
2026-01-30 16:16:59 -08:00

195 lines
5.3 KiB
Markdown

# Issues - Versioned Deployment Implementation
## Critical Issues
### 1. Watch Filter Breaks File Change Detection
**Location**: `src/server/apps.ts:589-593`
**Problem**: The watch logic ignores all changes deeper than 2 levels:
```ts
// Ignore changes deeper than 2 levels (inside timestamp dirs)
if (parts.length > 2) return
// For versioned apps, only care about changes to "current" directory
if (parts.length === 2 && parts[1] !== 'current' && parts[1] !== 'package.json') return
```
Files inside `current/` are 3 levels deep: `appname/current/somefile.ts`
This **ignores all file changes** inside the current directory, breaking hot reload and auto-restart.
**Fix**:
```ts
// Ignore changes inside old timestamp dirs (but allow current/)
if (parts.length > 2 && parts[1] !== 'current') return
```
---
### 2. App Restart Race Condition
**Location**: `src/server/api/sync.ts:145-148`
**Problem**: Activation uses arbitrary 1 second delay without confirming stop completed:
```ts
// Restart app to use new version
const app = allApps().find(a => a.name === appName)
if (app?.state === 'running') {
stopApp(appName)
setTimeout(() => startApp(appName), 1000) // ⚠️ Arbitrary 1s delay
}
```
**Issues**:
- 1 second may not be enough for app to fully stop
- No confirmation that stop completed before start
- If app crashes during startup, activation still returns success
**Fix**: Make activation async and wait for stop to complete, or move restart logic to after symlink succeeds and poll for stop completion.
---
## Major Issues
### 3. No Version Cleanup
**Problem**: Old timestamp directories accumulate forever with no cleanup mechanism.
**Impact**: Disk space grows indefinitely as deployments pile up.
**Recommendation**: Add cleanup logic:
- Keep last N versions (e.g., 5-10)
- Delete versions older than X days
- Expose as `toes clean <app>` command or automatic post-activation
---
### 4. safePath Behavior Change
**Location**: `src/server/api/sync.ts:14-21`
**Problem**: Security model changed by resolving symlinks in base path:
```ts
const canonicalBase = existsSync(base) ? realpathSync(base) : base
```
Previously paths were checked against literal base, now against resolved base. This changes behavior if someone creates a symlink attack.
**Recommendation**: Document this intentional change, or keep original check and add separate symlink resolution only where needed.
---
### 5. Non-Atomic Deploy Copy
**Location**: `src/server/api/sync.ts:133-141`
**Problem**: Race condition possible during deploy:
```ts
if (existsSync(currentLink)) {
const currentReal = realpathSync(currentLink)
cpSync(currentReal, newVersion, { recursive: true })
}
```
If `current` changes between `existsSync` and `cpSync`, stale code might be copied.
**Impact**: Low probability for single-user system, but possible during concurrent deploys.
**Fix**: Read symlink once and reuse: `const currentReal = existsSync(currentLink) ? realpathSync(currentLink) : null`
---
### 6. Upload Error Handling Inconsistency
**Location**: `src/cli/commands/sync.ts:113-115`
**Problem**: Upload continues if a file fails but doesn't track failures:
```ts
if (success) {
console.log(` ${color.green('↑')} ${file}`)
} else {
console.log(` ${color.red('✗')} ${file}`)
// Continue even if one file fails
}
```
If any file fails to upload, deployment still activates with incomplete files.
**Fix**: Either:
- Abort entire deployment on first failure
- Track failures and warn/abort before activating
---
## Minor Issues
### 7. POST Body Check Too Loose
**Location**: `src/cli/http.ts:42-44`
**Problem**: Falsy values like `0`, `false`, or `""` would incorrectly skip body:
```ts
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
```
**Fix**:
```ts
headers: body !== undefined ? { 'Content-Type': 'application/json' } : undefined,
body: body !== undefined ? JSON.stringify(body) : undefined,
```
---
### 8. Unconventional Timestamp Format
**Location**: `src/server/api/sync.ts:115-123`
**Problem**: Unusual array construction with separator as element:
```ts
const timestamp = [
now.getFullYear(),
String(now.getMonth() + 1).padStart(2, '0'),
String(now.getDate()).padStart(2, '0'),
'-', // Unusual to put separator as array element
String(now.getHours()).padStart(2, '0'),
String(now.getMinutes()).padStart(2, '0'),
String(now.getSeconds()).padStart(2, '0'),
].join('')
```
**Recommendation**: Use template literal:
```ts
const pad = (n: number) => String(n).padStart(2, '0')
const timestamp = `${now.getFullYear()}${pad(now.getMonth()+1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
```
Or ISO format: `now.toISOString().replace(/[:.]/g, '-').slice(0, -5)`
---
## Positive Points
✓ Atomic symlink swapping with temp file pattern is correct
✓ Clear 3-step deployment protocol (deploy, upload, activate)
✓ Proper use of `realpathSync` to resolve canonical paths for running apps
✓ Good separation of concerns in API routes
---
## Priority
1. **BLOCKER**: Fix watch filter (#1) - breaks hot reload
2. **HIGH**: Fix restart race condition (#2) - affects deployment reliability
3. **HIGH**: Add version cleanup (#3) - disk space concern
4. **MEDIUM**: Fix upload error handling (#6) - data integrity
5. **LOW**: Everything else