[OCaml] High Intensity Training Online
build Verify master on the server
Move verification enforcement to the deployment host. The server checks candidate master before updating the branch, then deploys only an accepted revision. Remove optional client hooks so clones have no local push requirement.
Changed files
.githooks/pre-push
@@ -1,19 +0,0 @@
1
Removed:
#!/usr/bin/env sh
2
Removed:
# Run the definition-of-done gates before any push leaves this machine, so the
3
Removed:
# deployment host's post-receive hook only ever sees a ref that type-checks,
4
Removed:
# passes every test, and is formatted.
5
Removed:
#
6
Removed:
# Enable once per clone: scripts/install-hooks.sh
7
Removed:
# Bypass deliberately: git push --no-verify
8
Removed:
9
Removed:
set -eu
10
Removed:
11
Removed:
cd "$(git rev-parse --show-toplevel)"
12
Removed:
13
Removed:
# Prefer the project's opam switch when direnv provides it, so the hook uses the
14
Removed:
# same toolchain as a developer shell.
15
Removed:
if command -v direnv >/dev/null 2>&1; then
16
Removed:
exec direnv exec . sh scripts/ci.sh
17
Removed:
else
18
Removed:
exec sh scripts/ci.sh
19
Removed:
fi
README.org
@@ -73,15 +73,22 @@
73
73
#+end_src
74
74
75
75
Format the files that you change with ~ocamlformat --inplace~. Run
76
Removed:
~scripts/install-hooks.sh~ once for each clone. The pre-push hook then runs
77
Removed:
~scripts/ci.sh~ before each push.
76
Added:
~scripts/ci.sh~ when you need local verification. The deployment server
77
Added:
verifies each candidate ~master~ revision before it accepts the update.
78
78
79
79
* Deploy
80
80
81
Removed:
~deploy/setup.sh~ prepares the deployment host. ~deploy/post-receive~ builds
82
Removed:
each push to ~master~, then replaces the live release. The pre-push hook runs
83
Removed:
the full verification gate before a push. If the health check fails, the hook
84
Removed:
goes back to the previous release.
81
Added:
~deploy/setup.sh~ prepares the deployment host. Its ~install~ subcommand
82
Added:
installs the trusted CI helper and the receive hooks. Run it again after a hook
83
Added:
or CI helper update.
84
Added:
85
Added:
The ~pre-receive~ hook archives and verifies a candidate ~master~ revision
86
Added:
before Git updates ~master~. A failed check rejects the push. The
87
Added:
~post-receive~ hook builds and replaces the live release only after that check
88
Added:
passes. The server installs test and formatting dependencies for verification.
89
Added:
90
Added:
A client hook is a local safeguard. A clone can omit or bypass it. The
91
Added:
~pre-receive~ hook is the authority for ~master~ and deployment safety.
85
92
86
93
* Doctrine
87
94
deploy/post-receive
@@ -6,11 +6,12 @@
6
6
# hook builds and publishes directly, with no privilege hop. The only
7
7
# privileged action is restarting the service, through one scoped sudoers rule.
8
8
#
9
Removed:
# For a push to master: check out the pushed revision into a fresh timestamped
10
Removed:
# release, build it against the shared opam switch, flip /srv/hito/current,
11
Removed:
# restart the service, and confirm it answers HTTP. Any build failure leaves
12
Removed:
# the live release untouched. A failed health check rolls current back to the
13
Removed:
# previous release and restarts.
9
Added:
# For an accepted push to master: check out the verified revision into a fresh
10
Added:
# timestamped release, build it against the shared opam switch, flip
11
Added:
# /srv/hito/current, restart the service, and confirm it answers HTTP. The
12
Added:
# pre-receive hook rejects a failed master verification. Any build failure
13
Added:
# leaves the live release untouched. A failed health check restores the
14
Added:
# previous release.
14
15
#
15
16
# Style: Google Shell Style Guide.
16
17
@@ -58,21 +59,17 @@
58
59
git --git-dir="${BARE}" archive "${sha}" | tar -x -C "${rel}"
59
60
}
60
61
61
Removed:
# Build the release artifact against the shared switch. The full log stays
62
Removed:
# inside the release so a successful deploy leaves a record, not only a failed
63
Removed:
# one.
62
Added:
# Build the verified release artifact against the shared switch. The full log
63
Added:
# stays inside the release so a successful deploy leaves a record, not only a
64
Added:
# failed one.
64
65
build_release() {
65
66
local rel="$1"
66
67
local buildlog="${rel}/deploy.log"
67
68
eval "$(opam env --switch="${SHARED}" --set-switch)"
68
69
cd "${rel}"
69
70
70
Removed:
err "syncing dependencies"
71
Removed:
opam install . --deps-only -y >"${buildlog}" 2>&1 \
72
Removed:
|| { tail -30 "${buildlog}" >&2; return 1; }
73
Removed:
74
71
err "dune build (release)"
75
Removed:
dune build >>"${buildlog}" 2>&1 \
72
Added:
dune build >"${buildlog}" 2>&1 \
76
73
|| { tail -40 "${buildlog}" >&2; return 1; }
77
74
}
78
75
deploy/pre-receive
@@ -0,0 +1,76 @@
1
Added:
#!/usr/bin/env bash
2
Added:
# hito pre-receive hook. The bare repository runs this hook before it moves a
3
Added:
# ref. The hook checks each candidate master revision in an isolated directory.
4
Added:
# A failed check rejects the push, so an unverified revision cannot deploy.
5
Added:
#
6
Added:
# deploy/setup.sh installs this hook and its trusted hito-ci helper. The helper
7
Added:
# is outside the received tree. A pushed revision cannot weaken this check.
8
Added:
#
9
Added:
# Style: Google Shell Style Guide.
10
Added:
11
Added:
set -euo pipefail
12
Added:
13
Added:
readonly DEPLOY_BRANCH="master"
14
Added:
readonly BARE="/srv/git/hito.git"
15
Added:
readonly SHARED="/srv/hito/shared"
16
Added:
readonly CI_HELPER="${BARE}/hooks/hito-ci"
17
Added:
readonly ZERO="0000000000000000000000000000000000000000"
18
Added:
19
Added:
export OPAMROOT="/srv/git/.opam"
20
Added:
export OPAMSWITCH="${SHARED}"
21
Added:
22
Added:
err() {
23
Added:
printf ' hito-verify: %s\n' "$*" >&2
24
Added:
}
25
Added:
26
Added:
fail() {
27
Added:
err "REJECTED: $*"
28
Added:
exit 1
29
Added:
}
30
Added:
31
Added:
verify_master() {
32
Added:
local old="$1" new="$2"
33
Added:
local stage buildlog
34
Added:
35
Added:
if [[ "${new}" == "${ZERO}" ]]; then
36
Added:
fail "master cannot be deleted"
37
Added:
fi
38
Added:
if [[ "${old}" != "${ZERO}" ]] \
39
Added:
&& ! git --git-dir="${BARE}" merge-base --is-ancestor "${old}" "${new}"; then
40
Added:
fail "master must advance by fast-forward"
41
Added:
fi
42
Added:
if [[ ! -x "${CI_HELPER}" ]]; then
43
Added:
fail "missing trusted CI helper; run deploy/setup.sh install"
44
Added:
fi
45
Added:
46
Added:
stage="$(mktemp -d /tmp/hito-verify.XXXXXX)"
47
Added:
trap 'rm -rf "${stage}"' RETURN
48
Added:
buildlog="${stage}/verify.log"
49
Added:
50
Added:
err "verifying candidate master ${new:0:12}"
51
Added:
git --git-dir="${BARE}" archive "${new}" | tar -x -C "${stage}" \
52
Added:
|| fail "archive candidate master"
53
Added:
54
Added:
eval "$(opam env --switch="${SHARED}" --set-switch)"
55
Added:
(
56
Added:
cd "${stage}"
57
Added:
opam install . --deps-only --with-test --with-dev-setup -y >"${buildlog}" 2>&1
58
Added:
BUILD_DIR="_build-ci" sh "${CI_HELPER}" >>"${buildlog}" 2>&1
59
Added:
) || {
60
Added:
tail -40 "${buildlog}" >&2 || true
61
Added:
fail "verification failed for ${new:0:12}"
62
Added:
}
63
Added:
64
Added:
err "verified candidate master ${new:0:12}"
65
Added:
}
66
Added:
67
Added:
main() {
68
Added:
local old new ref
69
Added:
while read -r old new ref; do
70
Added:
if [[ "${ref}" == "refs/heads/${DEPLOY_BRANCH}" ]]; then
71
Added:
verify_master "${old}" "${new}"
72
Added:
fi
73
Added:
done
74
Added:
}
75
Added:
76
Added:
main "$@"
deploy/setup.sh
@@ -5,8 +5,8 @@
5
5
# sudo bash setup.sh install One-time host setup: create the git-owned
6
6
# /srv/hito layout, the hito runtime user and
7
7
# its data/ directory, build the shared switch as
8
Removed:
# git, install the hook, the scoped sudoers rule,
9
Removed:
# and the systemd unit from hito.service beside
8
Added:
# git, install the receive hooks, the scoped sudoers
9
Added:
# rule, and the systemd unit from hito.service beside
10
10
# this script. Idempotent. Creates no release;
11
11
# the first push (or 'deploy') does.
12
12
# sudo bash setup.sh deploy Reinstall hito.service if it changed, build
@@ -23,8 +23,8 @@
23
23
# 'systemctl restart hito', through a scoped sudoers rule. The hito system user
24
24
# runs the service and owns only /srv/hito/data, which holds the database. A
25
25
# compromised service cannot rewrite releases or the deploy pipeline, and git
26
Removed:
# cannot read the database. The post-receive hook beside this script is the
27
Removed:
# per-push equivalent of 'deploy'.
26
Added:
# cannot read the database. The receive hooks enforce the master check before
27
Added:
# the post-receive hook deploys each accepted push.
28
28
#
29
29
# Style: Google Shell Style Guide.
30
30
@@ -79,10 +79,14 @@
79
79
as_svc opam switch create "${SHARED}" "${OCAML_COMPILER}" --no-install -y
80
80
}
81
81
82
Removed:
install_hook() {
82
Added:
install_hooks() {
83
83
install -o "${SVC_USER}" -g "${SVC_GROUP}" -m 0755 \
84
Added:
"${HERE}/pre-receive" "${BARE}/hooks/pre-receive"
85
Added:
install -o "${SVC_USER}" -g "${SVC_GROUP}" -m 0755 \
84
86
"${HERE}/post-receive" "${BARE}/hooks/post-receive"
85
Removed:
echo " installed ${BARE}/hooks/post-receive"
87
Added:
install -o "${SVC_USER}" -g "${SVC_GROUP}" -m 0755 \
88
Added:
"${HERE}/../scripts/ci.sh" "${BARE}/hooks/hito-ci"
89
Added:
echo " installed receive hooks and trusted CI helper"
86
90
}
87
91
88
92
# Create the unprivileged runtime account once. No login shell, no home.
@@ -157,10 +161,10 @@
157
161
git --git-dir='${BARE}' archive '${sha}' | tar -x -C '${stage}'
158
162
cd '${stage}'
159
163
eval \"\$(opam env --switch='${SHARED}' --set-switch)\"
160
Removed:
opam install . --deps-only --with-test -y 2>&1 | tee deploy.log
161
Removed:
dune build @check 2>&1 | tee -a deploy.log
162
Removed:
dune runtest 2>&1 | tee -a deploy.log
163
Removed:
dune build 2>&1 | tee -a deploy.log
164
Added:
opam install . --deps-only --with-test --with-dev-setup -y \
165
Added:
2>&1 | tee deploy.log
166
Added:
BUILD_DIR=_build-ci sh '${HERE}/../scripts/ci.sh' 2>&1 | tee -a deploy.log
167
Added:
dune build 2>&1 | tee -a deploy.log
164
168
"
165
169
build_secs=$((SECONDS - start))
166
170
@@ -227,7 +231,7 @@
227
231
mkdir -p "${OPAMROOT_DIR}"
228
232
chown "${SVC_USER}:${SVC_GROUP}" "${OPAMROOT_DIR}"
229
233
echo "== shared opam switch =="; ensure_switch
230
Removed:
echo "== post-receive hook =="; install_hook
234
Added:
echo "== receive hooks =="; install_hooks
231
235
echo "== scoped sudoers =="; install_sudoers
232
236
echo "== systemd unit =="; install_unit
233
237
echo
scripts/ci.sh
@@ -5,10 +5,9 @@
5
5
# 2. dune runtest runs every Alcotest suite
6
6
# 3. dune build @fmt confirms ocamlformat and dune formatting are applied
7
7
#
8
Removed:
# Exit status is non-zero if any gate fails, so the pre-push hook in .githooks,
9
Removed:
# a cron job, or the deployment host's receive hook can all enforce the same bar
10
Removed:
# with one call. The build directory is isolated so a developer's watch build
11
Removed:
# is never disturbed.
8
Added:
# Exit status is non-zero if any gate fails, so a cron job or the deployment
9
Added:
# host's trusted receive-hook helper can enforce the same bar with one call. The
10
Added:
# build directory is isolated so a developer's watch build is never disturbed.
12
11
#
13
12
# Usage: scripts/ci.sh run all gates
14
13
# BUILD_DIR=... scripts/ci.sh override the build directory
scripts/install-hooks.sh
@@ -1,10 +0,0 @@
1
Removed:
#!/usr/bin/env sh
2
Removed:
# Point git at the versioned hooks in .githooks, so the pre-push gate is part of
3
Removed:
# the repository rather than a per-clone manual step. Run once per clone.
4
Removed:
5
Removed:
set -eu
6
Removed:
7
Removed:
cd "$(git rev-parse --show-toplevel)"
8
Removed:
git config core.hooksPath .githooks
9
Removed:
chmod +x .githooks/*
10
Removed:
printf 'git hooks enabled: %s\n' "$(git config --get core.hooksPath)"