Install the push guard from precommit, not from memory

af9e3c688b03 · AtlantisPleb · · parent b68217655804

Install the push guard from precommit, not from memory

A rule that lives only in `AGENTS.md` binds whoever read it. Agents work in
fresh worktrees all day, and the guard was one unread paragraph away from
being decorative.

`mix precommit` now runs the installer in `--ensure` mode, so a clone becomes
guarded on the way to its first push. `--ensure` never fails the build: a
machine that has chosen `core.hooksPath`, or that keeps its own pre-push hook,
has made a decision the installer states and leaves alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016o8HwTaqLKEWCHTjsjFtrB
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified AGENTS.md
  • modified INVARIANTS.md
  • modified mix.exs
  • modified ops/dev/install-push-guard.sh
  • modified test/openagents/push_remote_contract_test.exs

Diff

5 files changed, +65 -18

AGENTS.md modified +3 -1

@@ -26,7 +26,9 @@ mirror; pushing to it directly leaves the forge behind a mirror it does not

26 26
know about, and nothing reports the divergence until a clone disagrees with
27 27
the site. Automatic mirroring to GitHub is not configured today, so GitHub
28 28
stays at whatever was last pushed to it. `ops/ci/push-remote-check.sh` refuses
29
a non-forge push. Install it once per clone, on every machine:
29
a non-forge push, and `mix precommit` installs it into the clone you are
30
working in, so running precommit before you push is enough. To install it by
31
hand:
30 32
31 33
```sh
32 34
sh ops/dev/install-push-guard.sh
INVARIANTS.md modified +6 -3

@@ -1821,9 +1821,12 @@ The check is a guard, not a deployment: it refuses a wrong destination and

1821 1821
makes no claim about the candidate. `ops/dev/install-push-guard.sh` installs
1822 1822
it at Git's default hook path, so a clone refuses the wrong destination
1823 1823
without also owing a release-gate receipt for every push; a machine that sets
1824
`core.hooksPath` runs the guard and the gate together instead. Neither is
1825
automatic: an uninstalled clone is unguarded, which is why the command belongs
1826
in `AGENTS.md` rather than in someone's memory.
1824
`core.hooksPath` runs the guard and the gate together instead. `mix precommit` runs the installer in `--ensure`
1825
mode, so a clone becomes guarded on the way to its first push without anyone
1826
having read this entry; `--ensure` never fails the build, because a machine
1827
that has chosen `core.hooksPath` or that keeps its own pre-push hook has made
1828
a decision the installer will not overrule. A clone that never runs precommit
1829
is still unguarded, which is why `AGENTS.md` states the rule as well.
1827 1830
1828 1831
Evidence: `ops/ci/push-remote-check.sh`, `ops/dev/install-push-guard.sh`,
1829 1832
`.githooks/pre-push`,
mix.exs modified +1

@@ -143,6 +143,7 @@ defmodule OpenAgents.MixProject do

143 143
        "phx.digest"
144 144
      ],
145 145
      precommit: [
146
        "cmd sh ops/dev/install-push-guard.sh --ensure",
146 147
        "hex.audit",
147 148
        "deps.audit",
148 149
        "compile --warnings-as-errors",
ops/dev/install-push-guard.sh modified +31 -14

@@ -10,39 +10,54 @@ set -eu

10 10
# push to GitHub without also demanding a release-gate receipt.
11 11
#
12 12
# Hooks live in the common directory, so one install covers every worktree of
13
# this clone. Run it once per clone, on every machine.
13
# this clone.
14 14
#
15
#   sh ops/dev/install-push-guard.sh [--force]
15
#   sh ops/dev/install-push-guard.sh            # install, refusing to clobber
16
#   sh ops/dev/install-push-guard.sh --force    # replace a foreign pre-push
17
#   sh ops/dev/install-push-guard.sh --ensure   # `mix precommit` calls this
18
#
19
# `--ensure` is the automatic path. Anyone who runs `mix precommit` before
20
# pushing ends up guarded without having read a word of this, which is the
21
# only version of a policy that survives contact with a fresh worktree. It
22
# never fails the build: a machine that has chosen `core.hooksPath`, or that
23
# keeps its own pre-push hook, has made a decision this script will not
24
# overrule.
16 25
#
17 26
# The guard is copied next to the hook rather than run from the worktree. A
18 27
# worktree can sit on a branch older than the guard, or on one that never had
19 28
# it, and a hook that execs a missing file refuses every push -- including the
20
# ones to the forge -- with a confusing error. Re-run the installer to pick up
21
# a newer guard.
29
# ones to the forge -- with a confusing error.
22 30
23
force=${1:-}
31
mode=${1:-}
24 32
25 33
repo_root=$(git rev-parse --show-toplevel)
26 34
common_dir=$(cd "$(git rev-parse --git-common-dir)" && pwd)
27 35
hook_path="$common_dir/hooks/pre-push"
36
guard_path="$common_dir/hooks/openagents-push-remote-check.sh"
28 37
marker='openagents-push-guard'
29 38
39
note() {
40
  if [ "$mode" = "--ensure" ]; then
41
    echo "$1"
42
    exit 0
43
  fi
44
45
  echo "$1" >&2
46
  exit 1
47
}
48
30 49
configured_path=$(git config --get core.hooksPath || true)
31 50
32 51
if [ -n "$configured_path" ]; then
33
  echo "core.hooksPath is set to $configured_path, so Git ignores $hook_path." >&2
34
  echo "That path's own pre-push hook decides; nothing installed." >&2
35
  exit 1
52
  note "core.hooksPath is $configured_path, so Git ignores $hook_path; that path's pre-push decides."
36 53
fi
37 54
38
if [ -e "$hook_path" ] && ! grep -q "$marker" "$hook_path" 2>/dev/null && [ "$force" != "--force" ]; then
39
  echo "$hook_path exists and is not the push guard. Re-run with --force to replace it." >&2
40
  exit 1
55
if [ -e "$hook_path" ] && ! grep -q "$marker" "$hook_path" 2>/dev/null && [ "$mode" != "--force" ]; then
56
  note "$hook_path exists and is not the push guard; re-run with --force to replace it."
41 57
fi
42 58
43 59
mkdir -p "$common_dir/hooks"
44 60
45
guard_path="$common_dir/hooks/openagents-push-remote-check.sh"
46 61
cp "$repo_root/ops/ci/push-remote-check.sh" "$guard_path"
47 62
chmod +x "$guard_path"
48 63

@@ -57,5 +72,7 @@ HOOK

57 72
58 73
chmod +x "$hook_path"
59 74
60
echo "Installed the forge-only push guard at $hook_path"
61
echo "It covers every worktree of $repo_root."
75
if [ "$mode" != "--ensure" ]; then
76
  echo "Installed the forge-only push guard at $hook_path"
77
  echo "It covers every worktree of $repo_root."
78
fi
test/openagents/push_remote_contract_test.exs modified +24

@@ -101,6 +101,30 @@ defmodule OpenAgents.PushRemoteContractTest do

101 101
    assert File.read!(hook) =~ "openagents-push-guard"
102 102
  end
103 103
104
  test "--ensure installs quietly and never fails the build" do
105
    root = Path.join(System.tmp_dir!(), "push-guard-#{System.unique_integer([:positive])}")
106
    on_exit(fn -> File.rm_rf!(root) end)
107
108
    File.mkdir_p!(Path.join(root, "ops/ci"))
109
    File.mkdir_p!(Path.join(root, "ops/dev"))
110
    File.cp!(@script, Path.join(root, @script))
111
    File.cp!(@installer, Path.join(root, @installer))
112
    {_output, 0} = System.cmd("git", ["init", "-q", root])
113
114
    assert {"", 0} = System.cmd("sh", [@installer, "--ensure"], cd: root, stderr_to_stdout: true)
115
    assert File.read!(Path.join(root, ".git/hooks/pre-push")) =~ "openagents-push-guard"
116
117
    # A hook someone else installed is a decision, not an obstacle: say so and
118
    # let precommit continue.
119
    File.write!(Path.join(root, ".git/hooks/pre-push"), "#!/bin/sh\nexit 0\n")
120
121
    assert {output, 0} =
122
             System.cmd("sh", [@installer, "--ensure"], cd: root, stderr_to_stdout: true)
123
124
    assert output =~ "not the push guard"
125
    assert File.read!(Path.join(root, ".git/hooks/pre-push")) == "#!/bin/sh\nexit 0\n"
126
  end
127
104 128
  test "the pre-push hook runs the check before the release gate" do
105 129
    hook = File.read!(".githooks/pre-push")
106 130

This page updates live while a promote is in flight · changelog