OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-deploy commits / 8f72080

Add OTA updater (poll + restart) and rollback support
OTAUpdater polls a remote for release branch changes and exits with code 75 for sidecar restart. rollback() reverts release branch to the previous deploy commit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8f72080feb310ffe81e91a3434b7c4fef4ad45cf
by plankatron <fleeky@gmail.com> · 2026-03-30T14:04:05.000Z

Changed files (2) +74 -1

index.js +1 -1
……@@ -1 +1 @@
1export { DeployPipeline } from './lib/pipeline.js'
1export { DeployPipeline, OTAUpdater, rollback } from './lib/pipeline.js'
lib/pipeline.js +73 -0
……@@ -208,6 +208,79 @@ function exec (cmd, args, opts = {}) {
208208 })
209209}
210210
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 */
217export 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 */
270export 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
211284function exists (p) {
212285 try { fs.statSync(p); return true } catch { return false }
213286}