forked from duckdb/duckdb-r
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathseries-cutover.sh
More file actions
executable file
·159 lines (145 loc) · 6.65 KB
/
Copy pathseries-cutover.sh
File metadata and controls
executable file
·159 lines (145 loc) · 6.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/bin/bash
# Atomically replace a series with its forward counterpart.
#
# A forward series <S>-fwd-* is the same series rebuilt on a newer `main`
# (.claude/skills/series-forward.md). Once its green ref covers at least the
# upstream commits the old green covered, this script swaps all four series
# refs in one atomic push, so consumers of <S>-green never observe a
# half-replaced series. The swap is the one sanctioned non-fast-forward move
# of a green ref.
#
# It is also the one move the series loop never makes: the loop reports a ready
# cutover and stops (.claude/skills/series-loop.md), because retiring the
# lineage r-universe builds from is a decision, not a stage. This script is the
# mechanical half of that rule — it runs from a terminal, on a typed
# confirmation, and nowhere else.
#
# A base series ref that does not exist yet is created rather than swapped:
# a series that started as -fwd has no counterpart to replace.
#
# Usage: series-cutover.sh <series> [remote] [upstream-clone]
# series-cutover.sh main origin ../duckdb
#
# The upstream clone is needed for the coverage gate (an ancestry check
# between vendored upstream SHAs); without it the gate degrades to a warning.
set -euo pipefail
S=${1:?usage: series-cutover.sh <series> [remote] [upstream-clone]}
remote=${2:-origin}
upstream=${3:-}
# Fail before the fetch, not after it: an unattended firing has no terminal, so
# there is nothing for it to confirm with and no reason to do any work first.
if [ ! -t 0 ] || [ ! -t 1 ]; then
echo "Error: cutover is a manual operation; run this script from a terminal." >&2
echo " The series loop reports a ready cutover and stops; a human runs it." >&2
echo " See .claude/skills/series-forward.md and series-loop.md." >&2
exit 1
fi
git fetch -q "$remote"
# See scripts/series-advance.sh: the pathspec narrows the walk, the subject
# decides, and an empty answer explains itself on stderr.
vendored_sha() {
local subjects sha n
subjects=$(git log -n 20 --format=%s "$1" -- src/duckdb || true)
sha=$(sed -nr 's/^.*duckdb.duckdb@([0-9a-f]+)( .*)?$/\1/p' <<<"$subjects" | head -n 1)
if [ -z "$sha" ]; then
n=$(grep -c . <<<"$subjects" || true)
if [ "$n" -ge 20 ]; then
echo "vendored_sha: 20 src/duckdb commits on $1, none of them vendoring;" >&2
echo " if that is genuine, raise the bound in this helper" >&2
else
echo "vendored_sha: no vendor commit among $n src/duckdb commits on $1" >&2
fi
fi
echo "$sha"
}
for r in build dev green build-base; do
git rev-parse -q --verify "refs/remotes/$remote/$S-fwd-$r" >/dev/null ||
{ echo "Error: $S-fwd-$r does not exist on $remote"; exit 1; }
done
# A base series ref may legitimately be missing: a series started as -fwd has
# no counterpart to replace, and the cutover creates the ref rather than
# swapping it. Only the forward refs are required.
missing=()
for r in build dev green build-base; do
git rev-parse -q --verify "refs/remotes/$remote/$S-$r" >/dev/null ||
missing+=("$S-$r")
done
[ ${#missing[@]} -eq 0 ] ||
echo "Note: ${missing[*]} missing on $remote, will be created from $S-fwd-*"
if git rev-parse -q --verify "refs/remotes/$remote/$S-green" >/dev/null; then
old_up=$(vendored_sha "refs/remotes/$remote/$S-green")
else
old_up=
fi
new_up=$(vendored_sha "refs/remotes/$remote/$S-fwd-green")
echo "old green vendors: ${old_up:-<nothing>}"
echo "new green vendors: ${new_up:-<nothing>}"
# Coverage gate: the forward green must vendor at least what the old green
# vendored, so verification never moves backwards at cutover.
if [ -n "$old_up" ]; then
if [ -z "$new_up" ]; then
echo "Error: old green vendors $old_up but forward green vendors nothing"
exit 1
fi
if [ -n "$upstream" ]; then
git -C "$upstream" merge-base --is-ancestor "$old_up" "$new_up" || {
echo "Error: forward green does not cover old green; coverage would regress"
exit 1
}
else
echo "Warning: no upstream clone given, coverage gate not verified"
fi
fi
# Convergence: the coverage gate above asks whether the forward has vendored far
# enough, which is a statement about how much upstream it reaches and none at all
# about what it carries. This asks the other half -- whether the two branches
# still hold the same package -- and prints it here so the operator confirms with
# it on screen. It reports rather than refuses: the invariant's second half is
# *explicable*, and whether a difference is explicable is a judgement no script
# can make -- which is why this prints and the human decides.
echo
rc=0
"$(dirname "$0")/series-converge.sh" "$S" "$remote" --no-fetch || rc=$?
# 1 is a divergence to read; 2 is the comparison not being available at all --
# a series that started as `-fwd` has no `<S>-dev` to compare against, and the
# script has already said so on its own.
if [ "$rc" -eq 1 ]; then
echo
echo " ^ read these before confirming. A swap does not resolve them: it makes"
echo " them the serving branch's, on the lineage r-universe builds from."
fi
echo
leases=()
refspecs=()
echo "refs to swap:"
for r in build dev green build-base; do
new=$(git rev-parse "refs/remotes/$remote/$S-fwd-$r")
# An empty expected value leases the ref as "must not exist yet", which is
# what a base ref from `missing` needs. The refspecs carry no leading `+`:
# a forced refspec defeats --force-with-lease outright, and the lease alone
# already authorizes the non-fast-forward swap.
cur=$(git rev-parse -q --verify "refs/remotes/$remote/$S-$r") || cur=
leases+=("--force-with-lease=refs/heads/$S-$r:$cur")
refspecs+=("$new:refs/heads/$S-$r")
short=${cur:0:7}
printf ' %-20s %s -> %s\n' "$S-$r" "${short:-<new>}" "${new:0:7}"
done
# The gate above says the swap is allowed; this asks whether it is wanted. It
# comes last so the operator confirms with the coverage lines and the four ref
# moves on screen, and it takes the series name rather than a keystroke because
# the mistake worth catching is cutting over the wrong series.
printf 'Replace series %s with %s-fwd-*? Type the series name to confirm: ' "$S" "$S"
read -r confirm
[ "$confirm" = "$S" ] || { echo "Aborted; nothing was pushed."; exit 1; }
git push --atomic "${leases[@]}" "$remote" "${refspecs[@]}"
if [ ${#missing[@]} -eq 4 ]; then
echo "Series $S created from its forward counterpart."
else
echo "Series $S replaced by its forward counterpart."
fi
# Best-effort: some git proxies refuse deletions. Until these refs are gone,
# the loop ignores a forward series whose refs equal its base series.
if ! git push "$remote" ":refs/heads/$S-fwd-build" ":refs/heads/$S-fwd-dev" \
":refs/heads/$S-fwd-green" ":refs/heads/$S-fwd-build-base"; then
echo "Warning: could not delete $S-fwd-* refs; remove them via the forge UI"
fi