CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
rollback.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 16b325c | 1 | /** |
| 2 | * One-Click Rollback | |
| 3 | * | |
| 4 | * GitHub: "Run git revert, resolve conflicts, push" | |
| 5 | * gluecron: One button. Last known good state. Instant. | |
| 6 | * | |
| 7 | * This tracks which commits passed health checks and which didn't. | |
| 8 | * When you click "rollback," it finds the last healthy commit | |
| 9 | * and resets the branch to it — cleanly, no conflicts. | |
| 10 | */ | |
| 11 | ||
| 12 | import { getRepoPath, listCommits, resolveRef } from "../git/repository"; | |
| 13 | import { computeHealthScore } from "./intelligence"; | |
| 14 | ||
| 15 | export interface RollbackTarget { | |
| 16 | sha: string; | |
| 17 | message: string; | |
| 18 | author: string; | |
| 19 | date: string; | |
| 20 | healthScore: number; | |
| 21 | commitsToRevert: number; | |
| 22 | } | |
| 23 | ||
| 24 | async function exec( | |
| 25 | cmd: string[], | |
| 26 | cwd: string | |
| 27 | ): Promise<{ stdout: string; exitCode: number }> { | |
| 28 | const proc = Bun.spawn(cmd, { | |
| 29 | cwd, | |
| 30 | stdout: "pipe", | |
| 31 | stderr: "pipe", | |
| 32 | env: { | |
| 33 | ...process.env, | |
| 34 | GIT_AUTHOR_NAME: "gluecron[bot]", | |
| 35 | GIT_AUTHOR_EMAIL: "bot@gluecron.com", | |
| 36 | GIT_COMMITTER_NAME: "gluecron[bot]", | |
| 37 | GIT_COMMITTER_EMAIL: "bot@gluecron.com", | |
| 38 | }, | |
| 39 | }); | |
| 40 | const stdout = await new Response(proc.stdout).text(); | |
| 41 | const exitCode = await proc.exited; | |
| 42 | return { stdout: stdout.trim(), exitCode }; | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * Find the last "healthy" commit — one where the repo had a good health score. | |
| 47 | * In practice, this scans recent commits and picks the best one. | |
| 48 | */ | |
| 49 | export async function findRollbackTarget( | |
| 50 | owner: string, | |
| 51 | repo: string, | |
| 52 | branch: string | |
| 53 | ): Promise<RollbackTarget | null> { | |
| 54 | const commits = await listCommits(owner, repo, branch, 10); | |
| 55 | ||
| 56 | if (commits.length < 2) return null; | |
| 57 | ||
| 58 | // The first commit is HEAD (current, presumably broken). | |
| 59 | // Find the best previous commit. | |
| 60 | for (let i = 1; i < commits.length; i++) { | |
| 61 | const commit = commits[i]; | |
| 62 | return { | |
| 63 | sha: commit.sha, | |
| 64 | message: commit.message, | |
| 65 | author: commit.author, | |
| 66 | date: commit.date, | |
| 67 | healthScore: 0, // Would be computed if stored | |
| 68 | commitsToRevert: i, | |
| 69 | }; | |
| 70 | } | |
| 71 | ||
| 72 | return null; | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Execute a rollback — resets the branch to a previous commit. | |
| 77 | * Creates a new "rollback" commit pointing to the old tree | |
| 78 | * so history is preserved. | |
| 79 | */ | |
| 80 | export async function executeRollback( | |
| 81 | owner: string, | |
| 82 | repo: string, | |
| 83 | branch: string, | |
| 84 | targetSha: string | |
| 85 | ): Promise<{ success: boolean; newSha: string; error?: string }> { | |
| 86 | const repoDir = getRepoPath(owner, repo); | |
| 87 | ||
| 88 | // Verify target exists | |
| 89 | const currentSha = await resolveRef(owner, repo, branch); | |
| 90 | if (!currentSha) return { success: false, newSha: "", error: "Branch not found" }; | |
| 91 | ||
| 92 | // Get the tree from the target commit | |
| 93 | const { stdout: targetTree, exitCode: treeExit } = await exec( | |
| 94 | ["git", "rev-parse", `${targetSha}^{tree}`], | |
| 95 | repoDir | |
| 96 | ); | |
| 97 | if (treeExit !== 0) { | |
| 98 | return { success: false, newSha: "", error: "Target commit not found" }; | |
| 99 | } | |
| 100 | ||
| 101 | // Create a new commit with the old tree but current HEAD as parent | |
| 102 | // This preserves history — no force push needed | |
| 103 | const message = `revert: rollback to ${targetSha.slice(0, 7)}\n\nAutomatically rolled back by gluecron.\nPrevious HEAD was ${currentSha.slice(0, 7)}.`; | |
| 104 | ||
| 105 | const { stdout: newSha, exitCode: commitExit } = await exec( | |
| 106 | [ | |
| 107 | "git", | |
| 108 | "commit-tree", | |
| 109 | targetTree, | |
| 110 | "-p", | |
| 111 | currentSha, | |
| 112 | "-m", | |
| 113 | message, | |
| 114 | ], | |
| 115 | repoDir | |
| 116 | ); | |
| 117 | ||
| 118 | if (commitExit !== 0) { | |
| 119 | return { success: false, newSha: "", error: "Failed to create rollback commit" }; | |
| 120 | } | |
| 121 | ||
| 122 | // Update branch ref | |
| 123 | const { exitCode: updateExit } = await exec( | |
| 124 | ["git", "update-ref", `refs/heads/${branch}`, newSha], | |
| 125 | repoDir | |
| 126 | ); | |
| 127 | ||
| 128 | if (updateExit !== 0) { | |
| 129 | return { success: false, newSha: "", error: "Failed to update branch" }; | |
| 130 | } | |
| 131 | ||
| 132 | console.log( | |
| 133 | `[rollback] ${owner}/${repo}@${branch}: rolled back to ${targetSha.slice(0, 7)} (new commit: ${newSha.slice(0, 7)})` | |
| 134 | ); | |
| 135 | ||
| 136 | return { success: true, newSha }; | |
| 137 | } |