Skip to content

Commit 97f78d6

Browse files
committed
fix(core): restore post-push backup via git wrapper
1 parent 0fb0997 commit 97f78d6

4 files changed

Lines changed: 147 additions & 12 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
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, post-push, pre-commit) and
15-
* session backup workflows. They are copied into each project's build context under
14+
* These scripts are referenced by git hooks (pre-push, pre-commit), the global
15+
* git push post-action runtime, and session backup workflows. They are copied into
16+
* each project's build context under
1617
* `scripts/` and embedded into the Docker image at `/opt/docker-git/scripts/`.
1718
*
1819
* @pure true

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

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +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"
132+
POST_PUSH_ACTION="$HOOKS_DIR/post-push"
133133
mkdir -p "$HOOKS_DIR"
134134
135135
cat <<'EOF' > "$PRE_PUSH_HOOK"
@@ -257,16 +257,17 @@ done
257257
EOF
258258
chmod 0755 "$PRE_PUSH_HOOK"
259259
260-
cat <<'EOF' > "$POST_PUSH_HOOK"
260+
cat <<'EOF' > "$POST_PUSH_ACTION"
261261
#!/usr/bin/env bash
262262
set -euo pipefail
263263
264264
# 5) Run session backup after successful push
265265
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
266266
cd "$REPO_ROOT"
267267
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
268+
# CHANGE: keep post-push backup logic in a reusable action script
269+
# WHY: git has no client-side post-push hook, so the global git wrapper
270+
# invokes this after a successful git push
270271
# REF: issue-192
271272
if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
272273
if command -v gh >/dev/null 2>&1; then
@@ -277,7 +278,7 @@ if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
277278
BACKUP_SCRIPT="/opt/docker-git/scripts/session-backup-gist.js"
278279
fi
279280
if [ -n "$BACKUP_SCRIPT" ]; then
280-
node "$BACKUP_SCRIPT" || echo "[session-backup] Warning: session backup failed (non-fatal)"
281+
DOCKER_GIT_SKIP_POST_PUSH_ACTION=1 node "$BACKUP_SCRIPT" || echo "[session-backup] Warning: session backup failed (non-fatal)"
281282
else
282283
echo "[session-backup] Warning: script not found (expected repo or global path)"
283284
fi
@@ -286,7 +287,129 @@ if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
286287
fi
287288
fi
288289
EOF
289-
chmod 0755 "$POST_PUSH_HOOK"
290+
chmod 0755 "$POST_PUSH_ACTION"
291+
292+
# 5.5) Install git wrapper so post-push actions run for normal git push invocations.
293+
# Git has no client-side post-push hook, so core.hooksPath alone is insufficient.
294+
GIT_WRAPPER_BIN="/usr/local/bin/git"
295+
GIT_REAL_BIN="$(type -aP git | awk -v wrapper="$GIT_WRAPPER_BIN" '$0 != wrapper { print; exit }')"
296+
if [[ -n "$GIT_REAL_BIN" ]]; then
297+
cat <<'EOF' > "$GIT_WRAPPER_BIN"
298+
#!/usr/bin/env bash
299+
set -euo pipefail
300+
301+
# docker-git managed git wrapper
302+
DOCKER_GIT_REAL_GIT_BIN="__DOCKER_GIT_REAL_BIN__"
303+
DOCKER_GIT_POST_PUSH_ACTION="/opt/docker-git/hooks/post-push"
304+
305+
docker_git_git_subcommand() {
306+
local expect_value="0"
307+
local arg=""
308+
for arg in "$@"; do
309+
if [[ "$expect_value" == "1" ]]; then
310+
expect_value="0"
311+
continue
312+
fi
313+
314+
case "$arg" in
315+
--help|-h|--version|--html-path|--man-path|--info-path|--list-cmds|--list-cmds=*)
316+
return 1
317+
;;
318+
-c|-C|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix|--config-env)
319+
expect_value="1"
320+
continue
321+
;;
322+
--git-dir=*|--work-tree=*|--namespace=*|--exec-path=*|--super-prefix=*|--config-env=*|--bare|--no-pager|--paginate|--literal-pathspecs|--no-literal-pathspecs|--glob-pathspecs|--noglob-pathspecs|--icase-pathspecs|--no-optional-locks|--no-lazy-fetch)
323+
continue
324+
;;
325+
--)
326+
return 1
327+
;;
328+
-*)
329+
continue
330+
;;
331+
*)
332+
printf "%s" "$arg"
333+
return 0
334+
;;
335+
esac
336+
done
337+
338+
return 1
339+
}
340+
341+
docker_git_git_push_is_dry_run() {
342+
local expect_value="0"
343+
local parsing_push_args="0"
344+
local arg=""
345+
346+
for arg in "$@"; do
347+
if [[ "$parsing_push_args" == "0" ]]; then
348+
if [[ "$expect_value" == "1" ]]; then
349+
expect_value="0"
350+
continue
351+
fi
352+
353+
case "$arg" in
354+
-c|-C|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix|--config-env)
355+
expect_value="1"
356+
continue
357+
;;
358+
--git-dir=*|--work-tree=*|--namespace=*|--exec-path=*|--super-prefix=*|--config-env=*|--bare|--no-pager|--paginate|--literal-pathspecs|--no-literal-pathspecs|--glob-pathspecs|--noglob-pathspecs|--icase-pathspecs|--no-optional-locks|--no-lazy-fetch)
359+
continue
360+
;;
361+
push)
362+
parsing_push_args="1"
363+
continue
364+
;;
365+
esac
366+
367+
continue
368+
fi
369+
370+
case "$arg" in
371+
--)
372+
break
373+
;;
374+
--dry-run|-n)
375+
return 0
376+
;;
377+
esac
378+
done
379+
380+
return 1
381+
}
382+
383+
docker_git_post_push_action() {
384+
if [[ "${"${"}DOCKER_GIT_SKIP_POST_PUSH_ACTION:-}" == "1" ]]; then
385+
return 0
386+
fi
387+
388+
if [[ -x "$DOCKER_GIT_POST_PUSH_ACTION" ]]; then
389+
DOCKER_GIT_SKIP_POST_PUSH_ACTION=1 "$DOCKER_GIT_POST_PUSH_ACTION" || true
390+
fi
391+
}
392+
393+
subcommand=""
394+
if subcommand="$(docker_git_git_subcommand "$@")" && [[ "$subcommand" == "push" ]]; then
395+
if "$DOCKER_GIT_REAL_GIT_BIN" "$@"; then
396+
status=0
397+
else
398+
status=$?
399+
fi
400+
401+
if [[ "$status" -eq 0 ]] && ! docker_git_git_push_is_dry_run "$@"; then
402+
docker_git_post_push_action
403+
fi
404+
405+
exit "$status"
406+
fi
407+
408+
exec "$DOCKER_GIT_REAL_GIT_BIN" "$@"
409+
EOF
410+
sed -i "s#__DOCKER_GIT_REAL_BIN__#$GIT_REAL_BIN#g" "$GIT_WRAPPER_BIN" || true
411+
chmod 0755 "$GIT_WRAPPER_BIN" || true
412+
fi
290413
291414
git config --system core.hooksPath "$HOOKS_DIR" || true
292415
git config --global core.hooksPath "$HOOKS_DIR" || true`

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,26 @@ describe("renderEntrypointDnsRepair", () => {
4949
})
5050

5151
describe("renderEntrypointGitHooks", () => {
52-
it("installs pre-push protection checks and post-push backup hook", () => {
52+
it("installs pre-push protection checks and a global git post-push runtime", () => {
5353
const hooks = renderEntrypointGitHooks()
5454

5555
expect(hooks).toContain('PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"')
56-
expect(hooks).toContain('POST_PUSH_HOOK="$HOOKS_DIR/post-push"')
56+
expect(hooks).toContain('POST_PUSH_ACTION="$HOOKS_DIR/post-push"')
57+
expect(hooks).toContain('GIT_WRAPPER_BIN="/usr/local/bin/git"')
58+
expect(hooks).toContain('type -aP git')
5759
expect(hooks).toContain("cat <<'EOF' > \"$PRE_PUSH_HOOK\"")
58-
expect(hooks).toContain("cat <<'EOF' > \"$POST_PUSH_HOOK\"")
60+
expect(hooks).toContain("cat <<'EOF' > \"$POST_PUSH_ACTION\"")
61+
expect(hooks).toContain("cat <<'EOF' > \"$GIT_WRAPPER_BIN\"")
5962
expect(hooks).toContain("check_issue_managed_block_range")
6063
expect(hooks).toContain("Run session backup after successful push")
64+
expect(hooks).toContain("git has no client-side post-push hook")
65+
expect(hooks).toContain("docker-git managed git wrapper")
66+
expect(hooks).toContain("DOCKER_GIT_SKIP_POST_PUSH_ACTION=1")
67+
expect(hooks).toContain("docker_git_git_push_is_dry_run")
68+
expect(hooks).toContain("--dry-run|-n")
69+
expect(hooks).toContain("--help|-h|--version|--html-path|--man-path|--info-path|--list-cmds|--list-cmds=*")
70+
expect(hooks).not.toContain('POST_PUSH_RUNTIME="/etc/profile.d/zz-git-post-push.sh"')
71+
expect(hooks).not.toContain("source /etc/profile.d/zz-git-post-push.sh")
6172
expect(hooks).toContain("node \"$BACKUP_SCRIPT\"")
6273
expect(hooks).not.toContain("node \"$BACKUP_SCRIPT\" --verbose")
6374
expect(hooks.indexOf('$REPO_ROOT/scripts/session-backup-gist.js')).toBeLessThan(

scripts/session-backup-gist.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ const buildSnapshotReadme = ({ backupRepo, source, manifestUrl, summary, session
442442
"",
443443
`- Manifest: ${manifestUrl}`,
444444
"",
445-
"Generated automatically by the docker-git `post-push` session backup hook.",
445+
"Generated automatically by the docker-git `git push` post-action.",
446446
"",
447447
].join("\n");
448448

0 commit comments

Comments
 (0)