Skip to content

Commit e92f0c3

Browse files
authored
Merge pull request #199 from skulidropek/issue-198
fix(core): restore post-push backup via git wrapper
2 parents 0fb0997 + 17f1678 commit e92f0c3

File tree

5 files changed

+152
-12
lines changed

5 files changed

+152
-12
lines changed

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
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
const entrypointGitPostPushWrapperInstall = String
2+
.raw`# 5.5) Install git wrapper so post-push actions run for normal git push invocations.
3+
# Git has no client-side post-push hook, so core.hooksPath alone is insufficient.
4+
GIT_WRAPPER_BIN="/usr/local/bin/git"
5+
GIT_REAL_BIN="$(type -aP git | awk -v wrapper="$GIT_WRAPPER_BIN" '$0 != wrapper { print; exit }')"
6+
if [[ -n "$GIT_REAL_BIN" ]]; then
7+
cat <<'EOF' > "$GIT_WRAPPER_BIN"
8+
#!/usr/bin/env bash
9+
set -euo pipefail
10+
11+
# docker-git managed git wrapper
12+
DOCKER_GIT_REAL_GIT_BIN="__DOCKER_GIT_REAL_BIN__"
13+
DOCKER_GIT_POST_PUSH_ACTION="/opt/docker-git/hooks/post-push"
14+
15+
docker_git_git_subcommand() {
16+
local expect_value="0"
17+
local arg=""
18+
for arg in "$@"; do
19+
if [[ "$expect_value" == "1" ]]; then
20+
expect_value="0"
21+
continue
22+
fi
23+
24+
case "$arg" in
25+
--help|-h|--version|--html-path|--man-path|--info-path|--list-cmds|--list-cmds=*)
26+
return 1
27+
;;
28+
-c|-C|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix|--config-env)
29+
expect_value="1"
30+
continue
31+
;;
32+
--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)
33+
continue
34+
;;
35+
--)
36+
return 1
37+
;;
38+
-*)
39+
continue
40+
;;
41+
*)
42+
printf "%s" "$arg"
43+
return 0
44+
;;
45+
esac
46+
done
47+
48+
return 1
49+
}
50+
51+
docker_git_git_push_is_dry_run() {
52+
local expect_value="0"
53+
local parsing_push_args="0"
54+
local arg=""
55+
56+
for arg in "$@"; do
57+
if [[ "$parsing_push_args" == "0" ]]; then
58+
if [[ "$expect_value" == "1" ]]; then
59+
expect_value="0"
60+
continue
61+
fi
62+
63+
case "$arg" in
64+
-c|-C|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix|--config-env)
65+
expect_value="1"
66+
continue
67+
;;
68+
--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)
69+
continue
70+
;;
71+
push)
72+
parsing_push_args="1"
73+
continue
74+
;;
75+
esac
76+
77+
continue
78+
fi
79+
80+
case "$arg" in
81+
--)
82+
break
83+
;;
84+
--dry-run|-n)
85+
return 0
86+
;;
87+
esac
88+
done
89+
90+
return 1
91+
}
92+
93+
docker_git_post_push_action() {
94+
if [[ "${"${"}DOCKER_GIT_SKIP_POST_PUSH_ACTION:-}" == "1" ]]; then
95+
return 0
96+
fi
97+
98+
if [[ -x "$DOCKER_GIT_POST_PUSH_ACTION" ]]; then
99+
DOCKER_GIT_SKIP_POST_PUSH_ACTION=1 "$DOCKER_GIT_POST_PUSH_ACTION" || true
100+
fi
101+
}
102+
103+
subcommand=""
104+
if subcommand="$(docker_git_git_subcommand "$@")" && [[ "$subcommand" == "push" ]]; then
105+
if "$DOCKER_GIT_REAL_GIT_BIN" "$@"; then
106+
status=0
107+
else
108+
status=$?
109+
fi
110+
111+
if [[ "$status" -eq 0 ]] && ! docker_git_git_push_is_dry_run "$@"; then
112+
docker_git_post_push_action
113+
fi
114+
115+
exit "$status"
116+
fi
117+
118+
exec "$DOCKER_GIT_REAL_GIT_BIN" "$@"
119+
EOF
120+
sed -i "s#__DOCKER_GIT_REAL_BIN__#$GIT_REAL_BIN#g" "$GIT_WRAPPER_BIN" || true
121+
chmod 0755 "$GIT_WRAPPER_BIN" || true
122+
fi`
123+
124+
export const renderEntrypointGitPostPushWrapperInstall = (): string => entrypointGitPostPushWrapperInstall

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { TemplateConfig } from "../domain.js"
2+
import { renderEntrypointGitPostPushWrapperInstall } from "./git-post-push-wrapper.js"
23

34
const renderAuthLabelResolution = (): string =>
45
String.raw`# 2) Ensure GitHub auth vars are available for SSH sessions.
@@ -129,7 +130,7 @@ const entrypointGitHooksTemplate = String
129130
.raw`# 3) Install global git hooks to protect main/master + managed AGENTS context
130131
HOOKS_DIR="/opt/docker-git/hooks"
131132
PRE_PUSH_HOOK="$HOOKS_DIR/pre-push"
132-
POST_PUSH_HOOK="$HOOKS_DIR/post-push"
133+
POST_PUSH_ACTION="$HOOKS_DIR/post-push"
133134
mkdir -p "$HOOKS_DIR"
134135
135136
cat <<'EOF' > "$PRE_PUSH_HOOK"
@@ -257,16 +258,17 @@ done
257258
EOF
258259
chmod 0755 "$PRE_PUSH_HOOK"
259260
260-
cat <<'EOF' > "$POST_PUSH_HOOK"
261+
cat <<'EOF' > "$POST_PUSH_ACTION"
261262
#!/usr/bin/env bash
262263
set -euo pipefail
263264
264265
# 5) Run session backup after successful push
265266
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
266267
cd "$REPO_ROOT"
267268
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
269+
# CHANGE: keep post-push backup logic in a reusable action script
270+
# WHY: git has no client-side post-push hook, so the global git wrapper
271+
# invokes this after a successful git push
270272
# REF: issue-192
271273
if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
272274
if command -v gh >/dev/null 2>&1; then
@@ -277,7 +279,7 @@ if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
277279
BACKUP_SCRIPT="/opt/docker-git/scripts/session-backup-gist.js"
278280
fi
279281
if [ -n "$BACKUP_SCRIPT" ]; then
280-
node "$BACKUP_SCRIPT" || echo "[session-backup] Warning: session backup failed (non-fatal)"
282+
DOCKER_GIT_SKIP_POST_PUSH_ACTION=1 node "$BACKUP_SCRIPT" || echo "[session-backup] Warning: session backup failed (non-fatal)"
281283
else
282284
echo "[session-backup] Warning: script not found (expected repo or global path)"
283285
fi
@@ -286,7 +288,9 @@ if [ "${"${"}DOCKER_GIT_SKIP_SESSION_BACKUP:-}" != "1" ]; then
286288
fi
287289
fi
288290
EOF
289-
chmod 0755 "$POST_PUSH_HOOK"
291+
chmod 0755 "$POST_PUSH_ACTION"
292+
293+
${renderEntrypointGitPostPushWrapperInstall()}
290294
291295
git config --system core.hooksPath "$HOOKS_DIR" || true
292296
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)