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