Skip to content

Commit df38f4a

Browse files
authored
Merge pull request #193 from ProverCoderAI/issue-192
fix: move session backup to post-push hook
2 parents 895f9fd + a1c3847 commit df38f4a

5 files changed

Lines changed: 65 additions & 20 deletions

File tree

packages/lib/src/core/docker-git-scripts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
/**
1212
* Names of docker-git scripts that must be available inside generated containers.
1313
*
14-
* These scripts are referenced by git hooks (pre-push, pre-commit) and session
15-
* backup workflows. They are copied into each project's build context under
14+
* These scripts are referenced by git hooks (pre-push, post-push, pre-commit) and
15+
* session backup workflows. They are copied into each project's build context under
1616
* `scripts/` and embedded into the Docker image at `/opt/docker-git/scripts/`.
1717
*
1818
* @pure true

packages/lib/src/core/templates-entrypoint/git.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const entrypointGitHooksTemplate = String
129129
.raw`# 3) Install global git hooks to protect main/master + managed AGENTS context
130130
HOOKS_DIR="/opt/docker-git/hooks"
131131
PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"
132+
POST_PUSH_HOOK="$HOOKS_DIR/post-push"
132133
mkdir -p "$HOOKS_DIR"
133134
134135
cat <<'EOF' > "$PRE_PUSH_HOOK"
@@ -256,28 +257,37 @@ done
256257
EOF
257258
chmod 0755 "$PRE_PUSH_HOOK"
258259
259-
cat <<'EOF' >> "$PRE_PUSH_HOOK"
260+
cat <<'EOF' > "$POST_PUSH_HOOK"
261+
#!/usr/bin/env bash
262+
set -euo pipefail
260263
264+
# 5) Run session backup after successful push
261265
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
262266
cd "$REPO_ROOT"
263267
264-
# CHANGE: resolve session-backup script from /opt/docker-git/scripts (embedded) or repo-local fallback
265-
# WHY: docker-git scripts are now embedded in the container image at /opt/docker-git/scripts
266-
# REF: issue-176
268+
# CHANGE: run session backup in post-push so source commit has already landed in remote
269+
# WHY: backups should mirror successfully pushed state and not block push validation
270+
# REF: issue-192
267271
if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
268272
if command -v gh >/dev/null 2>&1; then
269273
BACKUP_SCRIPT=""
270-
if [ -f /opt/docker-git/scripts/session-backup-gist.js ]; then
271-
BACKUP_SCRIPT="/opt/docker-git/scripts/session-backup-gist.js"
272-
elif [ -f "$REPO_ROOT/scripts/session-backup-gist.js" ]; then
274+
if [ -f "$REPO_ROOT/scripts/session-backup-gist.js" ]; then
273275
BACKUP_SCRIPT="$REPO_ROOT/scripts/session-backup-gist.js"
276+
elif [ -f /opt/docker-git/scripts/session-backup-gist.js ]; then
277+
BACKUP_SCRIPT="/opt/docker-git/scripts/session-backup-gist.js"
274278
fi
275279
if [ -n "$BACKUP_SCRIPT" ]; then
276-
node "$BACKUP_SCRIPT" --verbose || echo "[session-backup] Warning: session backup failed (non-fatal)"
280+
node "$BACKUP_SCRIPT" || echo "[session-backup] Warning: session backup failed (non-fatal)"
281+
else
282+
echo "[session-backup] Warning: script not found (expected repo or global path)"
277283
fi
284+
else
285+
echo "[session-backup] Warning: gh CLI not found (skipping session backup)"
278286
fi
279287
fi
280288
EOF
289+
chmod 0755 "$POST_PUSH_HOOK"
290+
281291
git config --system core.hooksPath "$HOOKS_DIR" || true
282292
git config --global core.hooksPath "$HOOKS_DIR" || true`
283293

packages/lib/tests/core/templates.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { defaultTemplateConfig, type TemplateConfig } from "../../src/core/domai
44
import { renderDockerCompose } from "../../src/core/templates/docker-compose.js"
55
import { renderEntrypoint } from "../../src/core/templates-entrypoint.js"
66
import { renderEntrypointDnsRepair } from "../../src/core/templates-entrypoint/dns-repair.js"
7+
import { renderEntrypointGitHooks } from "../../src/core/templates-entrypoint/git.js"
78

89
const makeTemplateConfig = (overrides: Partial<TemplateConfig> = {}): TemplateConfig => ({
910
...defaultTemplateConfig,
@@ -47,6 +48,25 @@ describe("renderEntrypointDnsRepair", () => {
4748
})
4849
})
4950

51+
describe("renderEntrypointGitHooks", () => {
52+
it("installs pre-push protection checks and post-push backup hook", () => {
53+
const hooks = renderEntrypointGitHooks()
54+
55+
expect(hooks).toContain('PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"')
56+
expect(hooks).toContain('POST_PUSH_HOOK="$HOOKS_DIR/post-push"')
57+
expect(hooks).toContain("cat <<'EOF' > \"$PRE_PUSH_HOOK\"")
58+
expect(hooks).toContain("cat <<'EOF' > \"$POST_PUSH_HOOK\"")
59+
expect(hooks).toContain("check_issue_managed_block_range")
60+
expect(hooks).toContain("Run session backup after successful push")
61+
expect(hooks).toContain("node \"$BACKUP_SCRIPT\"")
62+
expect(hooks).not.toContain("node \"$BACKUP_SCRIPT\" --verbose")
63+
expect(hooks.indexOf('$REPO_ROOT/scripts/session-backup-gist.js')).toBeLessThan(
64+
hooks.indexOf("/opt/docker-git/scripts/session-backup-gist.js")
65+
)
66+
expect(hooks).toContain("[session-backup] Warning: gh CLI not found")
67+
})
68+
})
69+
5070
describe("renderDockerCompose", () => {
5171
it("renders fallback DNS servers for the main container even without Playwright", () => {
5272
const compose = renderDockerCompose(makeTemplateConfig())

scripts/session-backup-gist.js

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*
1313
* Options:
1414
* --session-dir <path> Path to session directory under $HOME (default: auto-detect ~/.codex, ~/.claude, ~/.qwen, or ~/.gemini)
15-
* --pr-number <number> PR number to post comment to (optional, auto-detected from branch)
15+
* --pr-number <number> Open PR number to post comment to (optional, auto-detected from branch)
1616
* --repo <owner/repo> Source repository (optional, auto-detected from git remote)
1717
* --no-comment Skip posting PR comment
1818
* --dry-run Show what would be uploaded without actually uploading
@@ -29,6 +29,7 @@ const fs = require("node:fs");
2929
const path = require("node:path");
3030
const { execSync, spawnSync } = require("node:child_process");
3131
const os = require("node:os");
32+
const GH_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
3233

3334
const {
3435
buildBlobUrl,
@@ -107,7 +108,7 @@ const parseArgs = () => {
107108
108109
Options:
109110
--session-dir <path> Path to session directory under $HOME
110-
--pr-number <number> PR number to post comment to
111+
--pr-number <number> Open PR number to post comment to
111112
--repo <owner/repo> Source repository
112113
--no-comment Skip posting PR comment
113114
--dry-run Show what would be uploaded
@@ -142,6 +143,7 @@ const ghCommand = (args, ghEnv) => {
142143
const result = spawnSync("gh", args, {
143144
encoding: "utf8",
144145
stdio: ["pipe", "pipe", "pipe"],
146+
maxBuffer: GH_MAX_BUFFER_BYTES,
145147
env: ghEnv,
146148
});
147149

@@ -245,20 +247,24 @@ const getPrNumberFromBranch = (repo, branch, ghEnv) => {
245247
return null;
246248
};
247249

248-
const prExists = (repo, prNumber, ghEnv) => {
250+
const getPrState = (repo, prNumber, ghEnv) => {
249251
const result = ghCommand([
250252
"pr",
251253
"view",
252254
prNumber.toString(),
253255
"--repo",
254256
repo,
255257
"--json",
256-
"number",
258+
"state",
257259
"--jq",
258-
".number",
260+
".state",
259261
], ghEnv);
260262

261-
return result.success && result.stdout === prNumber.toString();
263+
return result.success ? result.stdout : null;
264+
};
265+
266+
const prIsOpen = (repo, prNumber, ghEnv) => {
267+
return getPrState(repo, prNumber, ghEnv) === "OPEN";
262268
};
263269

264270
const getPrNumberFromWorkspaceBranch = (branch) => {
@@ -275,9 +281,12 @@ const findPrContext = (repos, branch, verbose, ghEnv) => {
275281
for (const repo of repos) {
276282
log(verbose, `Checking open PR in ${repo} for branch ${branch}`);
277283
const prNumber = getPrNumberFromBranch(repo, branch, ghEnv);
278-
if (prNumber !== null) {
284+
if (prNumber !== null && prIsOpen(repo, prNumber, ghEnv)) {
279285
return { repo, prNumber };
280286
}
287+
if (prNumber !== null) {
288+
log(verbose, `Skipping PR #${prNumber} in ${repo}: PR is not open`);
289+
}
281290
}
282291

283292
const workspacePrNumber = getPrNumberFromWorkspaceBranch(branch);
@@ -287,7 +296,7 @@ const findPrContext = (repos, branch, verbose, ghEnv) => {
287296

288297
for (const repo of repos) {
289298
log(verbose, `Checking workspace PR #${workspacePrNumber} in ${repo} for branch ${branch}`);
290-
if (prExists(repo, workspacePrNumber, ghEnv)) {
299+
if (prIsOpen(repo, workspacePrNumber, ghEnv)) {
291300
return { repo, prNumber: workspacePrNumber };
292301
}
293302
}
@@ -410,7 +419,7 @@ const buildSnapshotReadme = ({ backupRepo, source, manifestUrl, summary, session
410419
"",
411420
`- Manifest: ${manifestUrl}`,
412421
"",
413-
"Generated automatically by the docker-git `pre-push` session backup hook.",
422+
"Generated automatically by the docker-git `post-push` session backup hook.",
414423
"",
415424
].join("\n");
416425

@@ -492,7 +501,11 @@ const main = () => {
492501

493502
let prContext = null;
494503
if (args.prNumber !== null) {
495-
prContext = { repo: sourceRepo, prNumber: args.prNumber };
504+
if (prIsOpen(sourceRepo, args.prNumber, ghEnv)) {
505+
prContext = { repo: sourceRepo, prNumber: args.prNumber };
506+
} else {
507+
console.log(`[session-backup] Skipping PR comment: PR #${args.prNumber} is not open`);
508+
}
496509
} else if (args.postComment) {
497510
prContext = findPrContext(repoCandidates, branch, verbose, ghEnv);
498511
}

scripts/session-backup-repo.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { spawnSync } = require("node:child_process");
77

88
const BACKUP_REPO_NAME = "docker-git-sessions";
99
const BACKUP_DEFAULT_BRANCH = "main";
10+
const GH_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
1011
// Keep each stored object below GitHub's 100 MB limit while transport batches stay smaller.
1112
const MAX_REPO_FILE_SIZE = 99 * 1000 * 1000;
1213
const MAX_PUSH_BATCH_BYTES = 50 * 1000 * 1000;
@@ -163,6 +164,7 @@ const ghCommand = (args, ghEnv, inputFilePath = null) => {
163164
const result = spawnSync("gh", resolvedArgs, {
164165
encoding: "utf8",
165166
stdio: ["pipe", "pipe", "pipe"],
167+
maxBuffer: GH_MAX_BUFFER_BYTES,
166168
env: ghEnv,
167169
});
168170

0 commit comments

Comments
 (0)