From 4ae728c2c585e1c623bc84912840d8c1ee32042b Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:22:06 +0000 Subject: [PATCH 1/4] fix(core): move session backup hook to post-push --- packages/lib/src/core/docker-git-scripts.ts | 4 ++-- .../lib/src/core/templates-entrypoint/git.ts | 16 +++++++++++----- packages/lib/tests/core/templates.test.ts | 16 ++++++++++++++++ scripts/session-backup-gist.js | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/lib/src/core/docker-git-scripts.ts b/packages/lib/src/core/docker-git-scripts.ts index 4a827c3a..582298cd 100644 --- a/packages/lib/src/core/docker-git-scripts.ts +++ b/packages/lib/src/core/docker-git-scripts.ts @@ -11,8 +11,8 @@ /** * Names of docker-git scripts that must be available inside generated containers. * - * These scripts are referenced by git hooks (pre-push, pre-commit) and session - * backup workflows. They are copied into each project's build context under + * These scripts are referenced by git hooks (pre-push, post-push, pre-commit) and + * session backup workflows. They are copied into each project's build context under * `scripts/` and embedded into the Docker image at `/opt/docker-git/scripts/`. * * @pure true diff --git a/packages/lib/src/core/templates-entrypoint/git.ts b/packages/lib/src/core/templates-entrypoint/git.ts index 4e95ea88..4942a310 100644 --- a/packages/lib/src/core/templates-entrypoint/git.ts +++ b/packages/lib/src/core/templates-entrypoint/git.ts @@ -129,6 +129,7 @@ const entrypointGitHooksTemplate = String .raw`# 3) Install global git hooks to protect main/master + managed AGENTS context HOOKS_DIR="/opt/docker-git/hooks" PRE_PUSH_HOOK="$HOOKS_DIR/pre-push" +POST_PUSH_HOOK="$HOOKS_DIR/post-push" mkdir -p "$HOOKS_DIR" cat <<'EOF' > "$PRE_PUSH_HOOK" @@ -256,14 +257,17 @@ done EOF chmod 0755 "$PRE_PUSH_HOOK" -cat <<'EOF' >> "$PRE_PUSH_HOOK" +cat <<'EOF' > "$POST_PUSH_HOOK" +#!/usr/bin/env bash +set -euo pipefail +# 5) Run session backup after successful push REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" cd "$REPO_ROOT" -# CHANGE: resolve session-backup script from /opt/docker-git/scripts (embedded) or repo-local fallback -# WHY: docker-git scripts are now embedded in the container image at /opt/docker-git/scripts -# REF: issue-176 +# CHANGE: run session backup in post-push so source commit has already landed in remote +# WHY: backups should mirror successfully pushed state and not block push validation +# REF: issue-192 if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then if command -v gh >/dev/null 2>&1; then BACKUP_SCRIPT="" @@ -273,11 +277,13 @@ if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then BACKUP_SCRIPT="$REPO_ROOT/scripts/session-backup-gist.js" fi if [ -n "$BACKUP_SCRIPT" ]; then - node "$BACKUP_SCRIPT" --verbose || echo "[session-backup] Warning: session backup failed (non-fatal)" + node "$BACKUP_SCRIPT" || echo "[session-backup] Warning: session backup failed (non-fatal)" fi fi fi EOF +chmod 0755 "$POST_PUSH_HOOK" + git config --system core.hooksPath "$HOOKS_DIR" || true git config --global core.hooksPath "$HOOKS_DIR" || true` diff --git a/packages/lib/tests/core/templates.test.ts b/packages/lib/tests/core/templates.test.ts index 866c13b5..369666d6 100644 --- a/packages/lib/tests/core/templates.test.ts +++ b/packages/lib/tests/core/templates.test.ts @@ -4,6 +4,7 @@ import { defaultTemplateConfig, type TemplateConfig } from "../../src/core/domai import { renderDockerCompose } from "../../src/core/templates/docker-compose.js" import { renderEntrypoint } from "../../src/core/templates-entrypoint.js" import { renderEntrypointDnsRepair } from "../../src/core/templates-entrypoint/dns-repair.js" +import { renderEntrypointGitHooks } from "../../src/core/templates-entrypoint/git.js" const makeTemplateConfig = (overrides: Partial = {}): TemplateConfig => ({ ...defaultTemplateConfig, @@ -47,6 +48,21 @@ describe("renderEntrypointDnsRepair", () => { }) }) +describe("renderEntrypointGitHooks", () => { + it("installs pre-push protection checks and post-push backup hook", () => { + const hooks = renderEntrypointGitHooks() + + expect(hooks).toContain('PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"') + expect(hooks).toContain('POST_PUSH_HOOK="$HOOKS_DIR/post-push"') + expect(hooks).toContain("cat <<'EOF' > \"$PRE_PUSH_HOOK\"") + expect(hooks).toContain("cat <<'EOF' > \"$POST_PUSH_HOOK\"") + expect(hooks).toContain("check_issue_managed_block_range") + expect(hooks).toContain("Run session backup after successful push") + expect(hooks).toContain("node \"$BACKUP_SCRIPT\"") + expect(hooks).not.toContain("node \"$BACKUP_SCRIPT\" --verbose") + }) +}) + describe("renderDockerCompose", () => { it("renders fallback DNS servers for the main container even without Playwright", () => { const compose = renderDockerCompose(makeTemplateConfig()) diff --git a/scripts/session-backup-gist.js b/scripts/session-backup-gist.js index cc352ced..49f1381f 100644 --- a/scripts/session-backup-gist.js +++ b/scripts/session-backup-gist.js @@ -410,7 +410,7 @@ const buildSnapshotReadme = ({ backupRepo, source, manifestUrl, summary, session "", `- Manifest: ${manifestUrl}`, "", - "Generated automatically by the docker-git `pre-push` session backup hook.", + "Generated automatically by the docker-git `post-push` session backup hook.", "", ].join("\n"); From d9bd5bc2c3cb200958ff96bb081ebe1abca56888 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:40:08 +0000 Subject: [PATCH 2/4] fix: restrict session backup PR comments to open PRs --- scripts/session-backup-gist.js | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/scripts/session-backup-gist.js b/scripts/session-backup-gist.js index 49f1381f..332ebdff 100644 --- a/scripts/session-backup-gist.js +++ b/scripts/session-backup-gist.js @@ -12,7 +12,7 @@ * * Options: * --session-dir Path to session directory under $HOME (default: auto-detect ~/.codex, ~/.claude, ~/.qwen, or ~/.gemini) - * --pr-number PR number to post comment to (optional, auto-detected from branch) + * --pr-number Open PR number to post comment to (optional, auto-detected from branch) * --repo Source repository (optional, auto-detected from git remote) * --no-comment Skip posting PR comment * --dry-run Show what would be uploaded without actually uploading @@ -107,7 +107,7 @@ const parseArgs = () => { Options: --session-dir Path to session directory under $HOME - --pr-number PR number to post comment to + --pr-number Open PR number to post comment to --repo Source repository --no-comment Skip posting PR comment --dry-run Show what would be uploaded @@ -245,7 +245,7 @@ const getPrNumberFromBranch = (repo, branch, ghEnv) => { return null; }; -const prExists = (repo, prNumber, ghEnv) => { +const getPrState = (repo, prNumber, ghEnv) => { const result = ghCommand([ "pr", "view", @@ -253,12 +253,16 @@ const prExists = (repo, prNumber, ghEnv) => { "--repo", repo, "--json", - "number", + "state", "--jq", - ".number", + ".state", ], ghEnv); - return result.success && result.stdout === prNumber.toString(); + return result.success ? result.stdout : null; +}; + +const prIsOpen = (repo, prNumber, ghEnv) => { + return getPrState(repo, prNumber, ghEnv) === "OPEN"; }; const getPrNumberFromWorkspaceBranch = (branch) => { @@ -275,9 +279,12 @@ const findPrContext = (repos, branch, verbose, ghEnv) => { for (const repo of repos) { log(verbose, `Checking open PR in ${repo} for branch ${branch}`); const prNumber = getPrNumberFromBranch(repo, branch, ghEnv); - if (prNumber !== null) { + if (prNumber !== null && prIsOpen(repo, prNumber, ghEnv)) { return { repo, prNumber }; } + if (prNumber !== null) { + log(verbose, `Skipping PR #${prNumber} in ${repo}: PR is not open`); + } } const workspacePrNumber = getPrNumberFromWorkspaceBranch(branch); @@ -287,7 +294,7 @@ const findPrContext = (repos, branch, verbose, ghEnv) => { for (const repo of repos) { log(verbose, `Checking workspace PR #${workspacePrNumber} in ${repo} for branch ${branch}`); - if (prExists(repo, workspacePrNumber, ghEnv)) { + if (prIsOpen(repo, workspacePrNumber, ghEnv)) { return { repo, prNumber: workspacePrNumber }; } } @@ -492,7 +499,11 @@ const main = () => { let prContext = null; if (args.prNumber !== null) { - prContext = { repo: sourceRepo, prNumber: args.prNumber }; + if (prIsOpen(sourceRepo, args.prNumber, ghEnv)) { + prContext = { repo: sourceRepo, prNumber: args.prNumber }; + } else { + console.log(`[session-backup] Skipping PR comment: PR #${args.prNumber} is not open`); + } } else if (args.postComment) { prContext = findPrContext(repoCandidates, branch, verbose, ghEnv); } From 59353b4fd22b75145f0d8506d316435c21f939e5 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:50:46 +0000 Subject: [PATCH 3/4] fix(core): prefer repo session backup script in post-push hook --- packages/lib/src/core/templates-entrypoint/git.ts | 10 +++++++--- packages/lib/tests/core/templates.test.ts | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/core/templates-entrypoint/git.ts b/packages/lib/src/core/templates-entrypoint/git.ts index 4942a310..8969a3e4 100644 --- a/packages/lib/src/core/templates-entrypoint/git.ts +++ b/packages/lib/src/core/templates-entrypoint/git.ts @@ -271,14 +271,18 @@ cd "$REPO_ROOT" if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then if command -v gh >/dev/null 2>&1; then BACKUP_SCRIPT="" - if [ -f /opt/docker-git/scripts/session-backup-gist.js ]; then - BACKUP_SCRIPT="/opt/docker-git/scripts/session-backup-gist.js" - elif [ -f "$REPO_ROOT/scripts/session-backup-gist.js" ]; then + if [ -f "$REPO_ROOT/scripts/session-backup-gist.js" ]; then BACKUP_SCRIPT="$REPO_ROOT/scripts/session-backup-gist.js" + elif [ -f /opt/docker-git/scripts/session-backup-gist.js ]; then + BACKUP_SCRIPT="/opt/docker-git/scripts/session-backup-gist.js" fi if [ -n "$BACKUP_SCRIPT" ]; then node "$BACKUP_SCRIPT" || echo "[session-backup] Warning: session backup failed (non-fatal)" + else + echo "[session-backup] Warning: script not found (expected repo or global path)" fi + else + echo "[session-backup] Warning: gh CLI not found (skipping session backup)" fi fi EOF diff --git a/packages/lib/tests/core/templates.test.ts b/packages/lib/tests/core/templates.test.ts index 369666d6..236a21e4 100644 --- a/packages/lib/tests/core/templates.test.ts +++ b/packages/lib/tests/core/templates.test.ts @@ -60,6 +60,10 @@ describe("renderEntrypointGitHooks", () => { expect(hooks).toContain("Run session backup after successful push") expect(hooks).toContain("node \"$BACKUP_SCRIPT\"") expect(hooks).not.toContain("node \"$BACKUP_SCRIPT\" --verbose") + expect(hooks.indexOf('$REPO_ROOT/scripts/session-backup-gist.js')).toBeLessThan( + hooks.indexOf("/opt/docker-git/scripts/session-backup-gist.js") + ) + expect(hooks).toContain("[session-backup] Warning: gh CLI not found") }) }) From a1c3847962a637ced3ce82045b5d753361d35edb Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:56:38 +0000 Subject: [PATCH 4/4] fix(core): increase session backup gh command buffer --- scripts/session-backup-gist.js | 2 ++ scripts/session-backup-repo.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/scripts/session-backup-gist.js b/scripts/session-backup-gist.js index 332ebdff..c3250ec9 100644 --- a/scripts/session-backup-gist.js +++ b/scripts/session-backup-gist.js @@ -29,6 +29,7 @@ const fs = require("node:fs"); const path = require("node:path"); const { execSync, spawnSync } = require("node:child_process"); const os = require("node:os"); +const GH_MAX_BUFFER_BYTES = 32 * 1024 * 1024; const { buildBlobUrl, @@ -142,6 +143,7 @@ const ghCommand = (args, ghEnv) => { const result = spawnSync("gh", args, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], + maxBuffer: GH_MAX_BUFFER_BYTES, env: ghEnv, }); diff --git a/scripts/session-backup-repo.js b/scripts/session-backup-repo.js index fcf69c42..6806a647 100644 --- a/scripts/session-backup-repo.js +++ b/scripts/session-backup-repo.js @@ -7,6 +7,7 @@ const { spawnSync } = require("node:child_process"); const BACKUP_REPO_NAME = "docker-git-sessions"; const BACKUP_DEFAULT_BRANCH = "main"; +const GH_MAX_BUFFER_BYTES = 32 * 1024 * 1024; // Keep each stored object below GitHub's 100 MB limit while transport batches stay smaller. const MAX_REPO_FILE_SIZE = 99 * 1000 * 1000; const MAX_PUSH_BATCH_BYTES = 50 * 1000 * 1000; @@ -163,6 +164,7 @@ const ghCommand = (args, ghEnv, inputFilePath = null) => { const result = spawnSync("gh", resolvedArgs, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], + maxBuffer: GH_MAX_BUFFER_BYTES, env: ghEnv, });