feat: add prepush lifecycle hook (#498)

* feat: add prepush lifecycle hook

Add `prepushScript` configuration — an optional shell script that runs
automatically before pushing code to the remote repository. Reuses the
existing `executeLifecycleHook` infrastructure (bash execution, 2-min
timeout, error propagation on non-zero exit). When unconfigured the
hook is a no-op.

Made-with: Cursor

* fix: add prepushScript to run-context API response, fix UI separator

Include prepushScript in the settings returned by the run-context
endpoint so the hook actually fires in production. Also fix the
separator pattern in AgentSettings to match the existing convention
(spacer + hr + spacer instead of margin).

Made-with: Cursor
This commit is contained in:
Colin McDonnell
2026-03-26 04:28:24 +00:00
committed by pullfrog[bot]
parent 7454e66533
commit cb8e33360c
5 changed files with 13 additions and 1 deletions
+5 -1
View File
@@ -146600,6 +146600,7 @@ ${status}`
}
const refspec = branch === pushDest.remoteBranch ? branch : `${branch}:${pushDest.remoteBranch}`;
const pushArgs = force ? ["--force", "-u", pushDest.remoteName, refspec] : ["-u", pushDest.remoteName, refspec];
await executeLifecycleHook({ event: "prepush", script: ctx.prepushScript });
log.debug(`pushing ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch}`);
if (force) {
log.warning(`force pushing - this will overwrite remote history`);
@@ -150509,6 +150510,7 @@ var defaultSettings = {
modes: [],
setupScript: null,
postCheckoutScript: null,
prepushScript: null,
push: "restricted",
shell: "restricted",
prApproveEnabled: false,
@@ -150547,7 +150549,8 @@ async function fetchRunContext(params) {
...data.settings,
modes: data.settings?.modes ?? [],
setupScript: data.settings?.setupScript ?? null,
postCheckoutScript: data.settings?.postCheckoutScript ?? null
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
prepushScript: data.settings?.prepushScript ?? null
},
apiToken: data.apiToken,
oss: data.oss ?? false,
@@ -150976,6 +150979,7 @@ async function main() {
apiToken: runContext.apiToken,
modes: modes2,
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
prepushScript: runContext.repoSettings.prepushScript,
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
modeInstructions: runContext.repoSettings.modeInstructions,
toolState,
+1
View File
@@ -275,6 +275,7 @@ export async function main(): Promise<MainResult> {
apiToken: runContext.apiToken,
modes,
postCheckoutScript: runContext.repoSettings.postCheckoutScript,
prepushScript: runContext.repoSettings.prepushScript,
prApproveEnabled: runContext.repoSettings.prApproveEnabled,
modeInstructions: runContext.repoSettings.modeInstructions,
toolState,
+3
View File
@@ -2,6 +2,7 @@ import { regex } from "arkregex";
import { type } from "arktype";
import { log } from "../utils/cli.ts";
import { $git } from "../utils/gitAuth.ts";
import { executeLifecycleHook } from "../utils/lifecycle.ts";
import { $ } from "../utils/shell.ts";
import type { StoredPushDest, ToolContext } from "./server.ts";
import { execute, tool } from "./shared.ts";
@@ -143,6 +144,8 @@ export function PushBranchTool(ctx: ToolContext) {
? ["--force", "-u", pushDest.remoteName, refspec]
: ["-u", pushDest.remoteName, refspec];
await executeLifecycleHook({ event: "prepush", script: ctx.prepushScript });
log.debug(`pushing ${branch} to ${pushDest.remoteName}/${pushDest.remoteBranch}`);
if (force) {
log.warning(`force pushing - this will overwrite remote history`);
+1
View File
@@ -135,6 +135,7 @@ export interface ToolContext {
apiToken: string;
modes: Mode[];
postCheckoutScript: string | null;
prepushScript: string | null;
prApproveEnabled: boolean;
modeInstructions: Record<string, string>;
toolState: ToolState;
+3
View File
@@ -14,6 +14,7 @@ export interface RepoSettings {
modes: Mode[];
setupScript: string | null;
postCheckoutScript: string | null;
prepushScript: string | null;
push: PushPermission;
shell: ShellPermission;
prApproveEnabled: boolean;
@@ -33,6 +34,7 @@ const defaultSettings: RepoSettings = {
modes: [],
setupScript: null,
postCheckoutScript: null,
prepushScript: null,
push: "restricted",
shell: "restricted",
prApproveEnabled: false,
@@ -93,6 +95,7 @@ export async function fetchRunContext(params: {
modes: data.settings?.modes ?? [],
setupScript: data.settings?.setupScript ?? null,
postCheckoutScript: data.settings?.postCheckoutScript ?? null,
prepushScript: data.settings?.prepushScript ?? null,
},
apiToken: data.apiToken,
oss: data.oss ?? false,