| … | … | @@ -208,6 +208,79 @@ function exec (cmd, args, opts = {}) { |
| 208 | 208 | }) |
| 209 | 209 | } |
| 210 | 210 | |
| 211 | /** |
| 212 | * OTA Updater — poll a remote for release branch changes |
| 213 | * |
| 214 | * Checks a git remote periodically. If release branch has new commits, |
| 215 | * fetches them and exits with code 75 (sidecar restarts the process). |
| 216 | */ |
| 217 | export class OTAUpdater { |
| 218 | constructor (opts = {}) { |
| 219 | this.repoPath = opts.repoPath // path to local release clone |
| 220 | this.remote = opts.remote // git remote URL (punch:// or file://) |
| 221 | this.branch = opts.branch || 'release' |
| 222 | this.interval = opts.interval || 60000 // 1 min |
| 223 | this.onUpdate = opts.onUpdate || null |
| 224 | this._timer = null |
| 225 | this._running = false |
| 226 | } |
| 227 | |
| 228 | start () { |
| 229 | if (this._timer) return |
| 230 | this._timer = setInterval(() => this.check(), this.interval) |
| 231 | this.check() // immediate first check |
| 232 | } |
| 233 | |
| 234 | stop () { |
| 235 | if (this._timer) { clearInterval(this._timer); this._timer = null } |
| 236 | } |
| 237 | |
| 238 | async check () { |
| 239 | if (this._running) return |
| 240 | this._running = true |
| 241 | |
| 242 | try { |
| 243 | // Get local HEAD |
| 244 | const localHead = (await exec('git', ['-C', this.repoPath, 'rev-parse', `refs/heads/${this.branch}`])).trim() |
| 245 | |
| 246 | // Fetch remote |
| 247 | await exec('git', ['-C', this.repoPath, 'fetch', this.remote, this.branch], { timeout: 30000 }) |
| 248 | |
| 249 | // Get remote HEAD |
| 250 | const remoteHead = (await exec('git', ['-C', this.repoPath, 'rev-parse', 'FETCH_HEAD'])).trim() |
| 251 | |
| 252 | if (localHead !== remoteHead) { |
| 253 | console.log(`[OTA] Update available: ${localHead.slice(0, 8)} → ${remoteHead.slice(0, 8)}`) |
| 254 | await exec('git', ['-C', this.repoPath, 'reset', '--hard', 'FETCH_HEAD']) |
| 255 | |
| 256 | if (this.onUpdate) this.onUpdate(remoteHead) |
| 257 | else process.exit(75) // sidecar restart code |
| 258 | } |
| 259 | } catch (err) { |
| 260 | // Silent on poll failures — network may be down |
| 261 | } finally { |
| 262 | this._running = false |
| 263 | } |
| 264 | } |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * Rollback — revert release branch to previous deploy |
| 269 | */ |
| 270 | export async function rollback (bareRepoPath) { |
| 271 | // Get current release HEAD |
| 272 | const current = (await exec('git', ['--git-dir', bareRepoPath, 'rev-parse', 'refs/heads/release'])).trim() |
| 273 | |
| 274 | // Get parent commit |
| 275 | const parent = (await exec('git', ['--git-dir', bareRepoPath, 'rev-parse', 'refs/heads/release~1'])).trim() |
| 276 | |
| 277 | // Update ref |
| 278 | await exec('git', ['--git-dir', bareRepoPath, 'update-ref', 'refs/heads/release', parent]) |
| 279 | |
| 280 | console.log(`[Deploy] Rolled back: ${current.slice(0, 8)} → ${parent.slice(0, 8)}`) |
| 281 | return { previous: current, current: parent } |
| 282 | } |
| 283 | |
| 211 | 284 | function exists (p) { |
| 212 | 285 | try { fs.statSync(p); return true } catch { return false } |
| 213 | 286 | } |