autonomous-sdlc-harness 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,7 +28,28 @@
28
28
  # must NEVER abort the calling run — a stale remote is recoverable and a halted
29
29
  # run is not. That is why this script uses `set -uo pipefail` deliberately
30
30
  # WITHOUT `set -e`, and why even the fail-closed arm below exits 0: it refuses
31
- # to push, which is the closed outcome here, and says so.
31
+ # to push, which is the closed outcome here, and says so. A push the remote
32
+ # refused for any reason but a `[rejected]` ref is attempted up to
33
+ # PUSH_ATTEMPTS times in all — waiting PUSH_RETRY_DELAY_SECS before the second
34
+ # attempt and three times that before the third — and is still never fatal once
35
+ # given up. PUSH_RETRY_DELAY_SECS is read from the environment as a test and
36
+ # tuning seam: default 5, and anything but a non-negative integer falls back to
37
+ # 5 with one line.
38
+ #
39
+ # A BRANCH ITS REMOTE DELETED STAYS DELETED. A branch this checkout tracks on
40
+ # its remote is never pushed when that remote no longer lists it. It tracks one
41
+ # when `branch.<branch>.merge` is set, or when `refs/remotes/origin/<branch>`
42
+ # resolves; a branch with neither is a first push and is not checked. The remote
43
+ # asked is `branch.<branch>.remote` (else `origin`), for `branch.<branch>.merge`
44
+ # (else `refs/heads/<branch>`), once with `ls-remote --exit-code` before any
45
+ # push attempt. Exit 2 (not listed) skips the push with one line naming the hand
46
+ # push that publishes it again; any other failure is not evidence of a deletion,
47
+ # so the push goes ahead and reports its own result. Reason: Gate 12 round 9,
48
+ # finding 1 — a run stopped because its branch was deleted had that branch
49
+ # re-created by the job's `always()` push step. Accepted costs: a deletion that
50
+ # lands between the `ls-remote` and the push is still pushed back; and a local
51
+ # branch reusing the name of one deleted on origin is not pushed while its stale
52
+ # `refs/remotes/origin/<branch>` survives — the skip line names the hand push.
32
53
  #
33
54
  # DEFENCE IN DEPTH, NOT THE SOLE GUARD. The caller-agnostic backstop is the
34
55
  # committed pre-push hook `init` writes into the configured `githooksDir`, which
@@ -52,7 +73,12 @@
52
73
  #
53
74
  # WHAT IT NEVER DOES. It performs only a fast-forward push of already-committed
54
75
  # work: no `--force`, no `--force-with-lease`, no history-rewriting flag, no
55
- # commit of its own, and no non-zero exit.
76
+ # commit of its own, and no non-zero exit, and never pushes back a branch it
77
+ # tracks after its remote deleted it. It never retries a push git reports
78
+ # as `! [rejected]` (non-fast-forward, fetch first), and never fetches or
79
+ # rebases: a push that lost a race fails loudly, which is the run-control rule
80
+ # of record (docs/github-run-control.md -> "A push that loses a race fails
81
+ # loudly, and is never fetched, rebased or retried").
56
82
  #
57
83
  # Usage: push-branch.sh [<repo-dir>]
58
84
  # <repo-dir> worktree/repository to operate in (default: $PWD)
@@ -67,15 +93,44 @@
67
93
  # no upstream yet push-branch.sh "$d" -> exit 0; `git -C "$b" branch` lists
68
94
  # feat_x and the branch now has an upstream
69
95
  # nothing new push-branch.sh "$d" -> exit 0 ("everything up to date")
96
+ # deleted on origin git -C "$b" update-ref -d refs/heads/feat_x; push-branch.sh "$d"
97
+ # -> exit 0, a "no longer has feat_x" line, and
98
+ # `git -C "$b" branch` lists no feat_x
70
99
  # protected branch git -C "$d" checkout -q -b trunk; push-branch.sh "$d"
71
100
  # -> exit 0, refusal message, "$b" unchanged
72
101
  # unresolvable printf 'x' > "$d/harness.config.json"; push-branch.sh "$d"
73
102
  # -> exit 0, refusal message, nothing pushed
74
103
  # push fails git -C "$d" remote set-url origin /nonexistent.git
75
104
  # -> exit 0 with a visible failure line
105
+ # refused once printf '%s\n' '#!/bin/sh' 'c="$GIT_DIR/refusals"' \
106
+ # '[ -e "$c" ] && exit 0' ': > "$c"; exit 1' > "$b/hooks/pre-receive"
107
+ # chmod +x "$b/hooks/pre-receive"
108
+ # git -C "$d" commit -qm x --allow-empty
109
+ # PUSH_RETRY_DELAY_SECS=0 push-branch.sh "$d"
110
+ # -> exit 0, one "retrying" line, and `git -C "$b" rev-parse
111
+ # feat_x` equals `git -C "$d" rev-parse HEAD`
112
+ # remote moved c=$(mktemp -d); git clone -q -b feat_x "$b" "$c"
113
+ # git -C "$c" commit -qm other --allow-empty; git -C "$c" push -q
114
+ # git -C "$d" commit -qm mine --allow-empty; push-branch.sh "$d"
115
+ # -> exit 0, a "(not retried)" line, one push attempt
76
116
 
77
117
  set -uo pipefail
78
118
 
119
+ # Attempts in all, and the wait before the second; the third waits three times
120
+ # that (EVERY FAILURE PATH IS NON-FATAL above).
121
+ PUSH_ATTEMPTS=3
122
+ PUSH_RETRY_DELAY_SECS="${PUSH_RETRY_DELAY_SECS:-5}"
123
+ case "$PUSH_RETRY_DELAY_SECS" in
124
+ '' | *[!0-9]*)
125
+ echo "push-branch.sh: PUSH_RETRY_DELAY_SECS='$PUSH_RETRY_DELAY_SECS' is not a non-negative integer — using 5"
126
+ PUSH_RETRY_DELAY_SECS=5
127
+ ;;
128
+ *)
129
+ # Base 10, so a leading zero is not read as octal.
130
+ PUSH_RETRY_DELAY_SECS=$((10#$PUSH_RETRY_DELAY_SECS))
131
+ ;;
132
+ esac
133
+
79
134
  # The library is reached by a path computed from this script's own location — no
80
135
  # session root, and no runtime-substituted token, is assumed. Not finding it is
81
136
  # non-fatal like everything else here.
@@ -121,20 +176,64 @@ case "$protected" in
121
176
  ;;
122
177
  esac
123
178
 
124
- # Push the branch. Use the existing upstream when one is configured; otherwise
125
- # set it on the fly, so a branch with no upstream is still pushed. Fast-forward
126
- # only.
127
- if git -C "$top" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
128
- git -C "$top" push
129
- else
130
- git -C "$top" push --set-upstream origin "$branch"
179
+ # A BRANCH ITS REMOTE DELETED STAYS DELETED (header). Once, before the retry
180
+ # loop; a branch tracking nothing is a first push and skips the check.
181
+ track_merge="$(git -C "$top" config --get "branch.$branch.merge" 2>/dev/null)"
182
+ if [ -n "$track_merge" ] ||
183
+ git -C "$top" rev-parse --verify --quiet "refs/remotes/origin/$branch" >/dev/null 2>&1; then
184
+ track_remote="$(git -C "$top" config --get "branch.$branch.remote" 2>/dev/null)"
185
+ [ -n "$track_remote" ] || track_remote=origin
186
+ track_ref="${track_merge:-refs/heads/$branch}"
187
+ git -C "$top" ls-remote --exit-code --heads "$track_remote" "$track_ref" >/dev/null 2>&1
188
+ ls_status=$?
189
+ case "$ls_status" in
190
+ 0) ;;
191
+ 2)
192
+ echo "push-branch.sh: $track_remote no longer has $branch, which this checkout tracks; not pushing it back (a branch deleted on its remote stays deleted). To publish it again on purpose: git push --set-upstream $track_remote $branch"
193
+ exit 0
194
+ ;;
195
+ *)
196
+ echo "push-branch.sh: could not ask $track_remote whether it still has $branch (ls-remote exited $ls_status); pushing as before"
197
+ ;;
198
+ esac
131
199
  fi
132
- push_status=$?
133
200
 
134
- if [ "$push_status" -eq 0 ]; then
135
- echo "push-branch.sh: pushed $branch to origin"
201
+ # Push the branch; the deleted-on-its-remote check above has already run. Use
202
+ # the existing upstream when one is configured; otherwise set it on the fly, so
203
+ # a branch with no upstream is still pushed. Fast-forward only.
204
+ if git -C "$top" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
205
+ push_cmd=(git -C "$top" push)
136
206
  else
137
- # Visible but non-blocking: a push problem must never abort the calling run.
138
- echo "push-branch.sh: push failed for $branch (see output above)"
207
+ push_cmd=(git -C "$top" push --set-upstream origin "$branch")
139
208
  fi
209
+
210
+ # Every failure line keeps the `push failed for <branch>` prefix: logs and
211
+ # Gate 12 records quote it.
212
+ attempt=1
213
+ delay="$PUSH_RETRY_DELAY_SECS"
214
+ while :; do
215
+ push_out="$("${push_cmd[@]}" 2>&1)"
216
+ push_status=$?
217
+ # git writes its ref-status lines to stderr; keep them there.
218
+ [ -n "$push_out" ] && printf '%s\n' "$push_out" >&2
219
+
220
+ if [ "$push_status" -eq 0 ]; then
221
+ echo "push-branch.sh: pushed $branch to origin"
222
+ break
223
+ fi
224
+ # A here-string, not a pipe: `grep -q` exiting early would fail a pipeline
225
+ # under pipefail.
226
+ if grep -q '^ ! \[rejected\]' <<<"$push_out"; then
227
+ echo "push-branch.sh: push failed for $branch: origin has commits this branch does not (not retried)"
228
+ break
229
+ fi
230
+ if [ "$attempt" -ge "$PUSH_ATTEMPTS" ]; then
231
+ echo "push-branch.sh: push failed for $branch after $PUSH_ATTEMPTS attempts (see output above)"
232
+ break
233
+ fi
234
+ echo "push-branch.sh: push failed for $branch (attempt $attempt of $PUSH_ATTEMPTS); retrying in ${delay}s"
235
+ sleep "$delay"
236
+ attempt=$((attempt + 1))
237
+ delay=$((delay * 3))
238
+ done
140
239
  exit 0