ZapretKVN-android/scripts/test-publish-github-stable.sh
loop-uh 0abb52ebfb Retry the releases listing after creating a stable draft
The GitHub releases listing is eventually consistent: reading it right
after `gh release create --draft` intermittently returned a page without
the fresh draft, so release_json produced no output and jq -e exit code
4 killed the publisher through set -e. This reproduced twice while
publishing v0.2.16.

release_json_with_retry now retries every post-create read (bounded by
ZAPRET_RELEASE_LIST_ATTEMPTS x ZAPRET_RELEASE_LIST_RETRY_SECONDS,
default 5 x 4s) without weakening any verification. The resume probe
keeps a single read because an empty listing is its expected outcome.
The unit test simulates the listing lag through a list-lag counter in
the gh mock and fails when retries are disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 16:03:49 +03:00

183 lines
5.9 KiB
Shell
Executable file

#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PUBLISHER="$PROJECT_ROOT/scripts/publish-github-stable.sh"
TAG="v9.8.7"
REPOSITORY="example/release-test"
TEST_ROOT="$(mktemp -d)"
trap 'rm -rf -- "$TEST_ROOT"' EXIT
BUNDLE_DIR="$TEST_ROOT/bundle"
MOCK_BIN="$TEST_ROOT/bin"
MOCK_STATE="$TEST_ROOT/state"
mkdir -p "$BUNDLE_DIR" "$MOCK_BIN" "$MOCK_STATE"
ASSET_NAMES=(
"Zapret-KVN-$TAG-arm64-v8a.apk"
"Zapret-KVN-$TAG-arm64-v8a.apk.sha256"
"Zapret-KVN-$TAG-armeabi-v7a.apk"
"Zapret-KVN-$TAG-armeabi-v7a.apk.sha256"
"Zapret-KVN-$TAG-x86_64.apk"
"Zapret-KVN-$TAG-x86_64.apk.sha256"
"release-metadata-v2.json"
"release-metadata.json"
)
for name in "${ASSET_NAMES[@]}"; do
printf 'fixture:%s\n' "$name" > "$BUNDLE_DIR/$name"
done
printf 'Release test notes\n' > "$BUNDLE_DIR/RELEASE_NOTES.md"
cat > "$MOCK_BIN/gh" <<'MOCK_GH'
#!/usr/bin/env bash
set -euo pipefail
STATE_DIR="${MOCK_GH_STATE:?}"
TAG="${MOCK_GH_TAG:?}"
STATE_FILE="$STATE_DIR/release-state"
ASSETS_FILE="$STATE_DIR/assets.tsv"
LOG_FILE="$STATE_DIR/calls.log"
release_json() {
local state draft assets name size digest
[[ -f "$STATE_FILE" ]] || exit 1
state="$(<"$STATE_FILE")"
draft=false
[[ "$state" == draft ]] && draft=true
assets='[]'
if [[ -f "$ASSETS_FILE" ]]; then
while IFS=$'\t' read -r name size digest; do
[[ -n "$name" ]] || continue
assets="$(
jq -c \
--arg name "$name" \
--argjson size "$size" \
--arg digest "$digest" \
'. + [{name: $name, size: $size, state: "uploaded", digest: $digest}]' \
<<<"$assets"
)"
done < "$ASSETS_FILE"
fi
jq -n \
--arg tag "$TAG" \
--argjson draft "$draft" \
--argjson assets "$assets" \
'{
tag_name: $tag,
draft: $draft,
prerelease: false,
assets: $assets,
html_url: ("https://example.invalid/releases/tag/" + $tag)
}'
}
case "${1:-} ${2:-}" in
"api repos/"*)
release_json
;;
"api --paginate")
[[ "${3:-}" == repos/* ]]
if [[ -f "$STATE_DIR/list-lag" ]]; then
lag="$(<"$STATE_DIR/list-lag")"
if (( lag > 0 )); then
printf '%s\n' "$((lag - 1))" > "$STATE_DIR/list-lag"
printf '[]\n'
exit 0
fi
fi
release_json | jq -s '.'
;;
"release create")
[[ ! -e "$STATE_FILE" ]]
printf 'draft\n' > "$STATE_FILE"
printf 'CREATE\n' >> "$LOG_FILE"
printf 'https://example.invalid/draft\n'
;;
"release upload")
[[ "$#" -eq 6 && "${5:-}" == --repo ]]
[[ "$(<"$STATE_FILE")" == draft ]]
asset="${4:?}"
name="$(basename "$asset")"
printf 'UPLOAD\t%s\n' "$name" >> "$LOG_FILE"
if [[ "${MOCK_GH_FAIL_ASSET:-}" == "$name" ]]; then
exit 23
fi
if [[ -f "$ASSETS_FILE" ]] && cut -f1 "$ASSETS_FILE" | grep -Fx "$name" >/dev/null; then
echo "duplicate asset: $name" >&2
exit 1
fi
printf '%s\t%s\tsha256:%s\n' \
"$name" \
"$(stat -c '%s' "$asset")" \
"$(sha256sum "$asset" | awk '{print $1}')" \
>> "$ASSETS_FILE"
;;
"release edit")
[[ "$(<"$STATE_FILE")" == draft ]]
printf 'published\n' > "$STATE_FILE"
printf 'EDIT\n' >> "$LOG_FILE"
printf 'https://example.invalid/releases/tag/%s\n' "$TAG"
;;
*)
echo "Unexpected mock gh call: $*" >&2
exit 1
;;
esac
MOCK_GH
chmod 700 "$MOCK_BIN/gh"
export PATH="$MOCK_BIN:$PATH"
export MOCK_GH_STATE="$MOCK_STATE"
export MOCK_GH_TAG="$TAG"
export ZAPRET_RELEASE_UPLOAD_TIMEOUT_SECONDS=5
export ZAPRET_RELEASE_UPLOAD_ATTEMPTS=1
export ZAPRET_RELEASE_LIST_ATTEMPTS=3
export ZAPRET_RELEASE_LIST_RETRY_SECONDS=1
# The releases listing lags behind the freshly created draft: the resume
# probe and the first post-create read both see an empty listing, so the
# publisher must retry instead of dying on the eventual-consistency gap.
printf '2\n' > "$MOCK_STATE/list-lag"
export MOCK_GH_FAIL_ASSET="Zapret-KVN-$TAG-armeabi-v7a.apk"
if "$PUBLISHER" "$TAG" "$BUNDLE_DIR" "$REPOSITORY" >/dev/null 2>&1; then
echo "Interrupted publication test unexpectedly succeeded" >&2
exit 1
fi
[[ "$(<"$MOCK_STATE/release-state")" == draft ]]
[[ "$(grep -c '^CREATE$' "$MOCK_STATE/calls.log")" -eq 1 ]]
[[ "$(grep -c '^EDIT$' "$MOCK_STATE/calls.log" || true)" -eq 0 ]]
[[ "$(grep -c $'^UPLOAD\t' "$MOCK_STATE/calls.log")" -eq 3 ]]
[[ "$(<"$MOCK_STATE/list-lag")" -eq 0 ]]
unset MOCK_GH_FAIL_ASSET
"$PUBLISHER" "$TAG" "$BUNDLE_DIR" "$REPOSITORY" >/dev/null
[[ "$(<"$MOCK_STATE/release-state")" == published ]]
[[ "$(grep -c '^CREATE$' "$MOCK_STATE/calls.log")" -eq 1 ]]
[[ "$(grep -c $'^UPLOAD\t' "$MOCK_STATE/calls.log")" -eq 9 ]]
[[ "$(grep -c '^EDIT$' "$MOCK_STATE/calls.log")" -eq 1 ]]
[[ "$(wc -l < "$MOCK_STATE/assets.tsv")" -eq 8 ]]
if "$PUBLISHER" "$TAG" "$BUNDLE_DIR" "$REPOSITORY" >/dev/null 2>&1; then
echo "Published immutable release was accepted as resumable" >&2
exit 1
fi
[[ "$(grep -c '^EDIT$' "$MOCK_STATE/calls.log")" -eq 1 ]]
MISMATCH_STATE="$TEST_ROOT/mismatch-state"
mkdir -p "$MISMATCH_STATE"
printf 'draft\n' > "$MISMATCH_STATE/release-state"
printf '%s\t%s\tsha256:%064d\n' \
"Zapret-KVN-$TAG-arm64-v8a.apk" \
"$(stat -c '%s' "$BUNDLE_DIR/Zapret-KVN-$TAG-arm64-v8a.apk")" \
0 \
> "$MISMATCH_STATE/assets.tsv"
export MOCK_GH_STATE="$MISMATCH_STATE"
if "$PUBLISHER" "$TAG" "$BUNDLE_DIR" "$REPOSITORY" >/dev/null 2>&1; then
echo "Mismatched remote digest was accepted" >&2
exit 1
fi
[[ "$(<"$MISMATCH_STATE/release-state")" == draft ]]
[[ ! -e "$MISMATCH_STATE/calls.log" ]]
echo "Resumable sequential GitHub release publication verified."