@@ -3,9 +3,10 @@ name: Promote release
33# Platform build workflows (windows-installer, linux-installer,
44# release-pythinker-cli) create the GitHub Release as a PRERELEASE so it stays
55# out of the date-based /releases/latest endpoint (which ignores make_latest)
6- # until every asset has uploaded. This workflow waits for all expected assets,
7- # then clears `prerelease` and marks the release latest — the single point
8- # where a version becomes resolvable by the install scripts and in-app updater.
6+ # until every install channel is ready. This workflow waits for exact release
7+ # assets, PyPI, and the Homebrew formula, then clears `prerelease` and marks the
8+ # release latest — the single point where a version becomes resolvable by the
9+ # install scripts and in-app updater.
910#
1011# It runs on the tag push (not `release: published`, which a GITHUB_TOKEN-created
1112# release never fires) so promotion always happens. workflow_dispatch allows a
@@ -55,24 +56,44 @@ jobs:
5556 fi
5657 echo "tag=$tag" >> "$GITHUB_OUTPUT"
5758
58- - name : Wait for all release assets
59+ - name : Wait for install-channel readiness
5960 env :
6061 GH_TOKEN : ${{ github.token }}
6162 TAG : ${{ steps.tag.outputs.tag }}
6263 REPO : ${{ github.repository }}
6364 run : |
6465 set -euo pipefail
65- required=(
66- "PythinkerSetup-"
67- "_amd64.deb"
68- "_arm64.deb"
69- ".x86_64.rpm"
70- ".aarch64.rpm"
71- "x86_64-unknown-linux-gnu.tar.gz"
72- "aarch64-unknown-linux-gnu.tar.gz"
73- "aarch64-apple-darwin.tar.gz"
74- "x86_64-apple-darwin.tar.gz"
66+ version="${TAG#v}"
67+ required_assets=(
68+ "PythinkerSetup-${version}.exe"
69+ "PythinkerSetup-${version}.exe.sha256"
70+ "pythinker-code_${version}_amd64.deb"
71+ "pythinker-code_${version}_amd64.deb.sha256"
72+ "pythinker-code_${version}_arm64.deb"
73+ "pythinker-code_${version}_arm64.deb.sha256"
74+ "pythinker-code-${version}.x86_64.rpm"
75+ "pythinker-code-${version}.x86_64.rpm.sha256"
76+ "pythinker-code-${version}.aarch64.rpm"
77+ "pythinker-code-${version}.aarch64.rpm.sha256"
78+ "pythinker-${version}-x86_64-unknown-linux-gnu.tar.gz"
79+ "pythinker-${version}-x86_64-unknown-linux-gnu.tar.gz.sha256"
80+ "pythinker-${version}-aarch64-unknown-linux-gnu.tar.gz"
81+ "pythinker-${version}-aarch64-unknown-linux-gnu.tar.gz.sha256"
82+ "pythinker-${version}-aarch64-apple-darwin.tar.gz"
83+ "pythinker-${version}-aarch64-apple-darwin.tar.gz.sha256"
84+ "pythinker-${version}-x86_64-apple-darwin.tar.gz"
85+ "pythinker-${version}-x86_64-apple-darwin.tar.gz.sha256"
86+ "pythinker-${version}-x86_64-unknown-linux-gnu-onedir.tar.gz"
87+ "pythinker-${version}-x86_64-unknown-linux-gnu-onedir.tar.gz.sha256"
88+ "pythinker-${version}-aarch64-unknown-linux-gnu-onedir.tar.gz"
89+ "pythinker-${version}-aarch64-unknown-linux-gnu-onedir.tar.gz.sha256"
90+ "pythinker-${version}-aarch64-apple-darwin-onedir.tar.gz"
91+ "pythinker-${version}-aarch64-apple-darwin-onedir.tar.gz.sha256"
92+ "pythinker-${version}-x86_64-apple-darwin-onedir.tar.gz"
93+ "pythinker-${version}-x86_64-apple-darwin-onedir.tar.gz.sha256"
7594 )
95+ pypi_url="https://pypi.org/pypi/pythinker-code/${version}/json"
96+ homebrew_formula_url="https://raw.githubusercontent.com/TechMatrix-labs/homebrew-pythinker/main/Formula/pythinker-code.rb"
7697 # The budget must comfortably exceed the slowest platform build, since
7798 # this job runs on the tag push in parallel with them. The long pole is
7899 # linux-installer's emulated arm64 .deb/.rpm step: on the 0.26.0 release
@@ -83,26 +104,51 @@ jobs:
83104 max_attempts=80
84105 poll_interval=30
85106 budget_min=$(( max_attempts * poll_interval / 60 ))
86- echo "Polling for all release assets on $TAG (up to ${budget_min}m)..."
87- all_present =false
107+ echo "Polling install-channel readiness for $TAG (up to ${budget_min}m)..."
108+ all_ready =false
88109 for i in $(seq 1 "$max_attempts"); do
89- assets=$(gh api "repos/$REPO/releases/tags/$TAG" --jq '[.assets[].name] | join(" ")' 2>/dev/null || echo "")
90- all_present=true
91- for p in "${required[@]}"; do
92- if [[ "$assets" != *"$p"* ]]; then
93- all_present=false
94- break
110+ assets_json=$(gh api "repos/$REPO/releases/tags/$TAG" --jq '[.assets[].name]' 2>/dev/null || printf '[]')
111+ missing_assets=()
112+ for asset in "${required_assets[@]}"; do
113+ if ! jq -e --arg name "$asset" 'index($name)' <<<"$assets_json" >/dev/null; then
114+ missing_assets+=("$asset")
95115 fi
96116 done
97- if [[ "$all_present" == "true" ]]; then
98- echo "All assets present (attempt $i)"
117+
118+ pypi_ready=false
119+ if curl -fsSL --retry 2 --retry-delay 2 -o /dev/null "$pypi_url"; then
120+ pypi_ready=true
121+ fi
122+
123+ homebrew_ready=false
124+ formula_text=$(curl -fsSL --retry 2 --retry-delay 2 "$homebrew_formula_url" 2>/dev/null || true)
125+ if grep -qF "version \"${version}\"" <<<"$formula_text"; then
126+ homebrew_ready=true
127+ fi
128+
129+ if [[ "${#missing_assets[@]}" -eq 0 && "$pypi_ready" == "true" && "$homebrew_ready" == "true" ]]; then
130+ all_ready=true
131+ echo "All install channels ready (attempt $i)"
99132 break
100133 fi
101- echo "Attempt $i/$max_attempts: assets not ready, retrying in ${poll_interval}s..."
102- sleep "$poll_interval"
134+
135+ echo "Attempt $i/$max_attempts: install channels not ready."
136+ if [[ "${#missing_assets[@]}" -gt 0 ]]; then
137+ printf 'Missing release assets: %s\n' "${missing_assets[*]}"
138+ fi
139+ if [[ "$pypi_ready" != "true" ]]; then
140+ echo "PyPI is not serving ${version} yet: $pypi_url"
141+ fi
142+ if [[ "$homebrew_ready" != "true" ]]; then
143+ echo "Homebrew tap formula is not at ${version} yet: $homebrew_formula_url"
144+ fi
145+ if [[ "$i" -lt "$max_attempts" ]]; then
146+ echo "Retrying in ${poll_interval}s..."
147+ sleep "$poll_interval"
148+ fi
103149 done
104- if [[ "$all_present " != "true" ]]; then
105- echo "::error::Release assets not fully uploaded after ${budget_min} minutes"
150+ if [[ "$all_ready " != "true" ]]; then
151+ echo "::error::Install channels were not fully ready after ${budget_min} minutes"
106152 exit 1
107153 fi
108154
0 commit comments