| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | repo_root="$(git rev-parse --show-toplevel)" |
| 5 | release_script="$repo_root/scripts/release-stable.sh" |
| 6 | test_root="$(mktemp -d "${TMPDIR:-/tmp}/reasonix-stable-tag-test.XXXXXX")" |
| 7 | cleanup() { |
| 8 | case "$test_root" in |
| 9 | */reasonix-stable-tag-test.*) rm -rf -- "$test_root" ;; |
| 10 | *) echo "refusing to clean unexpected test directory: $test_root" >&2 ;; |
| 11 | esac |
| 12 | } |
| 13 | trap cleanup EXIT |
| 14 | |
| 15 | mkdir -p "$test_root/bin" |
| 16 | cat >"$test_root/bin/gh" <<'EOF' |
| 17 | #!/usr/bin/env bash |
| 18 | set -euo pipefail |
| 19 | if [ -n "${FAKE_ADVANCE_WORK:-}" ] && [ ! -e "$FAKE_ADVANCE_WORK/.advanced-by-fake-gh" ]; then |
| 20 | : >"$FAKE_ADVANCE_WORK/.advanced-by-fake-gh" |
| 21 | git -C "$FAKE_ADVANCE_WORK" add .advanced-by-fake-gh |
| 22 | git -C "$FAKE_ADVANCE_WORK" commit -q -m "advance main during CI" |
| 23 | git -C "$FAKE_ADVANCE_WORK" push -q origin main-v2 |
| 24 | fi |
| 25 | if [ "${FAKE_GH_CONCLUSION:-success}" = pending ]; then |
| 26 | printf '[{"headSha":"%s","status":"in_progress","conclusion":""}]\n' "$FAKE_CANDIDATE" |
| 27 | else |
| 28 | printf '[{"headSha":"%s","status":"completed","conclusion":"%s"}]\n' \ |
| 29 | "$FAKE_CANDIDATE" "${FAKE_GH_CONCLUSION:-success}" |
| 30 | fi |
| 31 | EOF |
| 32 | chmod +x "$test_root/bin/gh" |
| 33 | |
| 34 | make_remote() { |
| 35 | local name="$1" |
| 36 | local work="$test_root/$name-work" |
| 37 | local remote="$test_root/$name.git" |
| 38 | git init -q --bare "$remote" |
| 39 | git init -q -b main-v2 "$work" |
| 40 | git -C "$work" config user.name test |
| 41 | git -C "$work" config user.email test@example.invalid |
| 42 | mkdir -p "$work/release-notes" |
| 43 | jq '.releases |= map(select(.version != "1.19.2"))' \ |
| 44 | "$repo_root/release-notes/releases.json" >"$work/release-notes/releases.json" |
| 45 | git -C "$work" add release-notes/releases.json |
| 46 | git -C "$work" commit -q -m base |
| 47 | cp "$repo_root/release-notes/releases.json" "$work/release-notes/releases.json" |
| 48 | git -C "$work" add release-notes/releases.json |
| 49 | git -C "$work" commit -q -m "reviewed release notes" |
| 50 | git -C "$work" remote add origin "$remote" |
| 51 | git -C "$work" push -q origin main-v2 |
| 52 | printf '%s\n' "$work" |
| 53 | } |
| 54 | |
| 55 | success_work="$(make_remote success)" |
| 56 | success_sha="$(git -C "$success_work" rev-parse HEAD)" |
| 57 | ( |
| 58 | cd "$success_work" |
| 59 | PATH="$test_root/bin:$PATH" FAKE_CANDIDATE="$success_sha" \ |
| 60 | RELEASE_CI_WAIT_SECONDS=0 RELEASE_REMOTE=origin \ |
| 61 | "$release_script" 1.19.2 |
| 62 | ) |
| 63 | for tag in v1.19.2 npm-v1.19.2 desktop-v1.19.2; do |
| 64 | [ "$(git ls-remote --tags --refs "$test_root/success.git" "refs/tags/$tag" | awk 'NR == 1 { print $1 }')" = "$success_sha" ] |
| 65 | done |
| 66 | |
| 67 | # Advancing main after the atomic tag transaction must not invalidate the |
| 68 | # immutable Notes candidate selected by the protected relay. |
| 69 | git clone -q -b main-v2 "$test_root/success.git" "$test_root/success-control" |
| 70 | git -C "$test_root/success-control" config user.name test |
| 71 | git -C "$test_root/success-control" config user.email test@example.invalid |
| 72 | git -C "$test_root/success-control" commit --allow-empty -q -m "advance after tags" |
| 73 | git -C "$test_root/success-control" push -q origin main-v2 |
| 74 | ( |
| 75 | cd "$test_root/success-control" |
| 76 | GITHUB_OUTPUT="$test_root/success-control.out" RELEASE_REMOTE=origin \ |
| 77 | RELEASE_TAG=v1.19.2 ALLOW_STABLE_RECOVERY=false \ |
| 78 | "$repo_root/scripts/resolve-stable-release.sh" |
| 79 | ) |
| 80 | grep -Eq '^sha='"$success_sha"'$' "$test_root/success-control.out" |
| 81 | ( |
| 82 | cd "$success_work" |
| 83 | GITHUB_OUTPUT="$test_root/stale-control.out" RELEASE_REMOTE=origin \ |
| 84 | RELEASE_TAG=v1.19.2 ALLOW_STABLE_RECOVERY=false \ |
| 85 | "$repo_root/scripts/resolve-stable-release.sh" |
| 86 | ) |
| 87 | grep -Eq '^sha='"$success_sha"'$' "$test_root/stale-control.out" |
| 88 | |
| 89 | failure_work="$(make_remote failure)" |
| 90 | failure_sha="$(git -C "$failure_work" rev-parse HEAD)" |
| 91 | if ( |
| 92 | cd "$failure_work" |
| 93 | PATH="$test_root/bin:$PATH" FAKE_CANDIDATE="$failure_sha" FAKE_GH_CONCLUSION=failure \ |
| 94 | RELEASE_CI_WAIT_SECONDS=0 RELEASE_REMOTE=origin \ |
| 95 | "$release_script" 1.19.2 |
| 96 | ); then |
| 97 | echo "failed CI unexpectedly created release tags" >&2 |
| 98 | exit 1 |
| 99 | fi |
| 100 | [ -z "$(git ls-remote --tags --refs "$test_root/failure.git" 'refs/tags/*')" ] |
| 101 | |
| 102 | # A later code commit is not the reviewed Notes merge and must never become the |
| 103 | # release candidate merely because it contains the older reviewed record. |
| 104 | stale_notes_work="$(make_remote stale-notes)" |
| 105 | git -C "$stale_notes_work" commit --allow-empty -q -m "later product change" |
| 106 | git -C "$stale_notes_work" push -q origin main-v2 |
| 107 | stale_notes_sha="$(git -C "$stale_notes_work" rev-parse HEAD)" |
| 108 | if ( |
| 109 | cd "$stale_notes_work" |
| 110 | PATH="$test_root/bin:$PATH" FAKE_CANDIDATE="$stale_notes_sha" \ |
| 111 | RELEASE_CI_WAIT_SECONDS=0 RELEASE_REMOTE=origin \ |
| 112 | "$release_script" 1.19.2 |
| 113 | ); then |
| 114 | echo "candidate after the reviewed Notes commit unexpectedly created release tags" >&2 |
| 115 | exit 1 |
| 116 | fi |
| 117 | [ -z "$(git ls-remote --tags --refs "$test_root/stale-notes.git" 'refs/tags/*')" ] |
| 118 | |
| 119 | # If main advances while exact-SHA CI is being checked, the no-op main-v2 |
| 120 | # refspec makes the atomic push reject all three tags. |
| 121 | race_work="$(make_remote race)" |
| 122 | race_sha="$(git -C "$race_work" rev-parse HEAD)" |
| 123 | git clone -q -b main-v2 "$test_root/race.git" "$test_root/race-advance" |
| 124 | git -C "$test_root/race-advance" config user.name test |
| 125 | git -C "$test_root/race-advance" config user.email test@example.invalid |
| 126 | if ( |
| 127 | cd "$race_work" |
| 128 | PATH="$test_root/bin:$PATH" FAKE_CANDIDATE="$race_sha" \ |
| 129 | FAKE_ADVANCE_WORK="$test_root/race-advance" RELEASE_CI_WAIT_SECONDS=0 \ |
| 130 | RELEASE_REMOTE=origin "$release_script" 1.19.2 |
| 131 | ); then |
| 132 | echo "main-v2 advance during CI unexpectedly created release tags" >&2 |
| 133 | exit 1 |
| 134 | fi |
| 135 | [ -z "$(git ls-remote --tags --refs "$test_root/race.git" 'refs/tags/*')" ] |
| 136 | [ "$(git ls-remote "$test_root/race.git" refs/heads/main-v2 | awk 'NR == 1 { print $1 }')" != "$race_sha" ] |
| 137 | |
| 138 | occupied_work="$(make_remote occupied)" |
| 139 | occupied_sha="$(git -C "$occupied_work" rev-parse HEAD)" |
| 140 | git -C "$occupied_work" tag v1.19.2 |
| 141 | git -C "$occupied_work" push -q origin v1.19.2 |
| 142 | if ( |
| 143 | cd "$occupied_work" |
| 144 | PATH="$test_root/bin:$PATH" FAKE_CANDIDATE="$occupied_sha" \ |
| 145 | RELEASE_CI_WAIT_SECONDS=0 RELEASE_REMOTE=origin \ |
| 146 | "$release_script" 1.19.2 |
| 147 | ); then |
| 148 | echo "occupied release identity unexpectedly passed" >&2 |
| 149 | exit 1 |
| 150 | fi |
| 151 | [ -z "$(git ls-remote --tags --refs "$test_root/occupied.git" refs/tags/npm-v1.19.2 refs/tags/desktop-v1.19.2)" ] |
| 152 | |
| 153 | if "$release_script" 1.19.2-preview.1 >/dev/null 2>&1; then |
| 154 | echo "Preview version unexpectedly passed the Stable tag helper" >&2 |
| 155 | exit 1 |
| 156 | fi |
| 157 | |
| 158 | echo "stable release tag helper tests: PASS" |
| 159 |