| 1 | #!/usr/bin/env bash |
| 2 | # Resolve and authorize the immutable source commit for a Desktop publication. |
| 3 | # The workflow control plane must run from protected main-v2. Stable/RC builds |
| 4 | # use an existing tag on main-v2 history; Preview builds use the exact main-v2 |
| 5 | # commit selected when the protected workflow was dispatched. |
| 6 | set -euo pipefail |
| 7 | |
| 8 | channel="${RELEASE_CHANNEL:?RELEASE_CHANNEL is required}" |
| 9 | tag="${RELEASE_TAG:?RELEASE_TAG is required}" |
| 10 | orchestrated="${IN_ORCHESTRATED:-false}" |
| 11 | orchestrator="${IN_ORCHESTRATOR:-}" |
| 12 | candidate_preparation="${CANDIDATE_PREPARATION:-false}" |
| 13 | candidate_rehearsal="${CANDIDATE_REHEARSAL:-false}" |
| 14 | approved_sha="${APPROVED_SHA:-}" |
| 15 | caller_event="${CALLER_EVENT_NAME:-}" |
| 16 | caller_ref="${CALLER_REF:-}" |
| 17 | caller_ref_protected="${CALLER_REF_PROTECTED:-}" |
| 18 | caller_sha="${CALLER_SHA:-}" |
| 19 | caller_workflow_sha="${CALLER_WORKFLOW_SHA:-}" |
| 20 | release_remote="${RELEASE_REMOTE:-origin}" |
| 21 | require_current_main="${REQUIRE_CURRENT_MAIN:-true}" |
| 22 | verify_checkout="${VERIFY_RELEASE_CHECKOUT:-false}" |
| 23 | |
| 24 | case "$orchestrated" in |
| 25 | true | false) ;; |
| 26 | *) |
| 27 | echo "::error::IN_ORCHESTRATED must be true or false, got: $orchestrated" >&2 |
| 28 | exit 2 |
| 29 | ;; |
| 30 | esac |
| 31 | if [ "$orchestrated" = "true" ]; then |
| 32 | case "$orchestrator" in |
| 33 | stable | preview | candidate | promote) ;; |
| 34 | *) |
| 35 | echo "::error::IN_ORCHESTRATOR must be stable or preview for an orchestrated Desktop release, got: $orchestrator" >&2 |
| 36 | exit 2 |
| 37 | ;; |
| 38 | esac |
| 39 | if [ "$orchestrator" != "$channel" ] && [ "$orchestrator" != candidate ] && [ "$orchestrator" != promote ]; then |
| 40 | echo "::error::the $orchestrator orchestrator cannot authorize a Desktop $channel candidate" >&2 |
| 41 | exit 1 |
| 42 | fi |
| 43 | fi |
| 44 | case "$candidate_preparation" in |
| 45 | true | false) ;; |
| 46 | *) |
| 47 | echo "::error::CANDIDATE_PREPARATION must be true or false, got: $candidate_preparation" >&2 |
| 48 | exit 2 |
| 49 | ;; |
| 50 | esac |
| 51 | if [ "$candidate_preparation" = true ] && { [ "$orchestrated" != true ] || [ "$orchestrator" != candidate ] || [ "$channel" != stable ]; }; then |
| 52 | echo "::error::candidate preparation requires the protected candidate orchestrator" >&2 |
| 53 | exit 1 |
| 54 | fi |
| 55 | case "$candidate_rehearsal" in |
| 56 | true) |
| 57 | [ "$candidate_preparation" = true ] || { echo "::error::rehearsal requires non-publishing candidate preparation" >&2; exit 1; } |
| 58 | ;; |
| 59 | false) ;; |
| 60 | *) echo "::error::CANDIDATE_REHEARSAL must be true or false" >&2; exit 2 ;; |
| 61 | esac |
| 62 | case "$require_current_main" in |
| 63 | true | false) ;; |
| 64 | *) |
| 65 | echo "::error::REQUIRE_CURRENT_MAIN must be true or false, got: $require_current_main" >&2 |
| 66 | exit 2 |
| 67 | ;; |
| 68 | esac |
| 69 | case "$verify_checkout" in |
| 70 | true | false) ;; |
| 71 | *) |
| 72 | echo "::error::VERIFY_RELEASE_CHECKOUT must be true or false, got: $verify_checkout" >&2 |
| 73 | exit 2 |
| 74 | ;; |
| 75 | esac |
| 76 | |
| 77 | stable_tag_pattern='^desktop-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-([0-9A-Za-z-]+)(\.[0-9A-Za-z-]+)*)?)?$' |
| 78 | preview_tag_pattern='^desktop-v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-preview\.(0|[1-9][0-9]*)$' |
| 79 | |
| 80 | case "$channel" in |
| 81 | stable) |
| 82 | if [[ ! "$tag" =~ $stable_tag_pattern ]] || [[ "$tag" =~ $preview_tag_pattern ]]; then |
| 83 | echo "::error::Stable Desktop candidate requires desktop-vMAJOR.MINOR.PATCH[-PRERELEASE], excluding -preview.N: $tag" >&2 |
| 84 | exit 1 |
| 85 | fi |
| 86 | ;; |
| 87 | preview) |
| 88 | if [[ ! "$tag" =~ $preview_tag_pattern ]]; then |
| 89 | echo "::error::Preview Desktop candidate requires desktop-vMAJOR.MINOR.PATCH-preview.N: $tag" >&2 |
| 90 | exit 1 |
| 91 | fi |
| 92 | ;; |
| 93 | *) |
| 94 | echo "::error::Desktop release channel must be stable or preview, got: $channel" >&2 |
| 95 | exit 2 |
| 96 | ;; |
| 97 | esac |
| 98 | |
| 99 | git fetch "$release_remote" main-v2 --tags |
| 100 | main_sha="$(git rev-parse "$release_remote/main-v2^{commit}")" |
| 101 | |
| 102 | if [ "$orchestrated" = "true" ]; then |
| 103 | candidate="$approved_sha" |
| 104 | else |
| 105 | if [ "$caller_event" != "workflow_dispatch" ] || |
| 106 | [ "$caller_ref" != "refs/heads/main-v2" ] || |
| 107 | [ "$caller_ref_protected" != "true" ]; then |
| 108 | echo "::error::standalone Desktop releases must run from protected main-v2" >&2 |
| 109 | exit 1 |
| 110 | fi |
| 111 | if [ "$caller_sha" != "$caller_workflow_sha" ]; then |
| 112 | echo "::error::standalone Desktop workflow SHA is $caller_workflow_sha, expected caller SHA $caller_sha" >&2 |
| 113 | exit 1 |
| 114 | fi |
| 115 | if [ "$channel" = "preview" ]; then |
| 116 | candidate="$caller_sha" |
| 117 | else |
| 118 | candidate="$(git rev-parse "$tag^{commit}")" |
| 119 | fi |
| 120 | fi |
| 121 | |
| 122 | if [[ ! "$candidate" =~ ^[0-9a-f]{40}$ ]]; then |
| 123 | echo "::error::Desktop candidate SHA must be a full commit SHA, got: $candidate" >&2 |
| 124 | exit 1 |
| 125 | fi |
| 126 | if ! git cat-file -e "$candidate^{commit}" 2>/dev/null; then |
| 127 | echo "::error::Desktop candidate commit is unavailable: $candidate" >&2 |
| 128 | exit 1 |
| 129 | fi |
| 130 | if ! git merge-base --is-ancestor "$candidate" "$main_sha"; then |
| 131 | echo "::error::Desktop candidate $candidate is not on main-v2 history at $main_sha" >&2 |
| 132 | exit 1 |
| 133 | fi |
| 134 | |
| 135 | if [ "$channel" = "stable" ]; then |
| 136 | if [ "$candidate_preparation" = true ]; then |
| 137 | if [ "$candidate_rehearsal" != true ] && git show-ref --verify --quiet "refs/tags/$tag"; then |
| 138 | echo "::error::candidate preparation refuses an existing release tag: $tag" >&2 |
| 139 | exit 1 |
| 140 | fi |
| 141 | else |
| 142 | tag_sha="$(git rev-parse "$tag^{commit}")" |
| 143 | if [ "$tag_sha" != "$candidate" ]; then |
| 144 | echo "::error::$tag points to $tag_sha, expected Desktop candidate $candidate" >&2 |
| 145 | exit 1 |
| 146 | fi |
| 147 | fi |
| 148 | elif git show-ref --verify --quiet "refs/tags/$tag"; then |
| 149 | echo "::error::Desktop Preview uses an immutable asset directory, not a Git tag: $tag" >&2 |
| 150 | exit 1 |
| 151 | elif [ "$orchestrated" != "true" ] && [ "$require_current_main" = "true" ] && [ "$candidate" != "$main_sha" ]; then |
| 152 | echo "::error::Desktop Preview must use current main-v2 $main_sha, got: $candidate" >&2 |
| 153 | exit 1 |
| 154 | fi |
| 155 | |
| 156 | if [ "$verify_checkout" = "true" ]; then |
| 157 | head_sha="$(git rev-parse HEAD^{commit})" |
| 158 | if [ "$head_sha" != "$candidate" ]; then |
| 159 | echo "::error::Desktop release checkout is $head_sha, expected candidate $candidate" >&2 |
| 160 | exit 1 |
| 161 | fi |
| 162 | fi |
| 163 | |
| 164 | echo "sha=$candidate" >>"${GITHUB_OUTPUT:-/dev/stdout}" |
| 165 | echo "Desktop candidate verified: channel=$channel tag=$tag sha=$candidate" |
| 166 |