| 1 | #!/usr/bin/env bash |
| 2 | # Validate the exact remote main-v2 candidate, then push the three immutable |
| 3 | # Stable tags atomically. The v* tag activates the protected Stable relay. |
| 4 | set -euo pipefail |
| 5 | |
| 6 | if [ "$#" -ne 1 ] || [[ ! "$1" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then |
| 7 | echo "usage: scripts/release-stable.sh MAJOR.MINOR.PATCH" >&2 |
| 8 | exit 2 |
| 9 | fi |
| 10 | |
| 11 | version="$1" |
| 12 | remote="${RELEASE_REMOTE:-origin}" |
| 13 | script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" |
| 14 | |
| 15 | for command in git gh jq node; do |
| 16 | command -v "$command" >/dev/null || { |
| 17 | echo "required command is unavailable: $command" >&2 |
| 18 | exit 2 |
| 19 | } |
| 20 | done |
| 21 | |
| 22 | candidate="$(git ls-remote --heads "$remote" refs/heads/main-v2 | awk 'NR == 1 { print $1 }')" |
| 23 | if [[ ! "$candidate" =~ ^[0-9a-f]{40}$ ]]; then |
| 24 | echo "cannot resolve $remote/main-v2" >&2 |
| 25 | exit 1 |
| 26 | fi |
| 27 | git fetch --quiet --no-tags "$remote" refs/heads/main-v2 |
| 28 | bash "$script_dir/validate-stable-candidate.sh" "$version" "$candidate" |
| 29 | |
| 30 | tags=("v$version" "npm-v$version" "desktop-v$version") |
| 31 | for tag in "${tags[@]}"; do |
| 32 | if git ls-remote --exit-code --tags --refs "$remote" "refs/tags/$tag" >/dev/null 2>&1; then |
| 33 | echo "release tag already exists and will not be moved: $tag" >&2 |
| 34 | exit 1 |
| 35 | fi |
| 36 | done |
| 37 | |
| 38 | bash "$script_dir/verify-release-push-ci.sh" "$candidate" |
| 39 | |
| 40 | # Include a no-op main-v2 update in the same atomic transaction. If main-v2 |
| 41 | # advanced while CI was running, this refspec becomes a non-fast-forward update |
| 42 | # and the server rejects every tag instead of burning an unreleasable version. |
| 43 | git push --atomic "$remote" \ |
| 44 | "$candidate:refs/heads/main-v2" \ |
| 45 | "$candidate:refs/tags/${tags[0]}" \ |
| 46 | "$candidate:refs/tags/${tags[1]}" \ |
| 47 | "$candidate:refs/tags/${tags[2]}" |
| 48 | |
| 49 | for tag in "${tags[@]}"; do |
| 50 | remote_sha="$(git ls-remote --tags --refs "$remote" "refs/tags/$tag" | awk 'NR == 1 { print $1 }')" |
| 51 | if [ "$remote_sha" != "$candidate" ]; then |
| 52 | echo "$tag resolved to ${remote_sha:-missing}; expected $candidate" >&2 |
| 53 | exit 1 |
| 54 | fi |
| 55 | done |
| 56 | |
| 57 | echo "Stable tags pushed atomically at $candidate: ${tags[*]}" |
| 58 | echo "Release stable will request one release-environment approval." |
| 59 |