88
99concurrency :
1010 group : ci-${{ github.workflow }}-${{ github.ref }}
11- cancel-in-progress : true
11+ cancel-in-progress : ${{ github.event_name == 'pull_request' }}
1212
1313permissions : {}
1414
5757 contents : read
5858 id-token : write
5959 outputs :
60- new_release_published : ${{ steps.semantic.outputs.new_release_published }}
61- new_release_git_tag : ${{ steps.semantic.outputs.new_release_git_tag }}
62- new_release_version : ${{ steps.semantic.outputs.new_release_version }}
60+ release_draft : ${{ steps.release-target.outputs.draft }}
61+ release_present : ${{ steps.release-target.outputs.present }}
62+ release_published : ${{ steps.release-target.outputs.published }}
63+ release_tag : ${{ steps.release-target.outputs.tag }}
64+ release_version : ${{ steps.release-target.outputs.version }}
6365
6466 steps :
6567 - name : Create release bot token
@@ -126,8 +128,84 @@ jobs:
126128 GIT_COMMITTER_NAME : ${{ steps.release-bot.outputs.app-slug }}[bot]
127129 GIT_COMMITTER_EMAIL : ${{ steps.release-bot-identity.outputs.user_id }}+${{ steps.release-bot.outputs.app-slug }}[bot]@users.noreply.github.com
128130
131+ - name : Resolve exact release target
132+ id : release-target
133+ env :
134+ GH_TOKEN : ${{ steps.release-bot.outputs.token }}
135+ NEW_RELEASE_TAG : ${{ steps.semantic.outputs.new_release_git_tag }}
136+ run : |
137+ set -euo pipefail
138+ git fetch origin main --tags --force
139+
140+ tag="$NEW_RELEASE_TAG"
141+ if [[ -z "$tag" ]]; then
142+ matches=""
143+ while IFS= read -r candidate; do
144+ candidate_commit="$(git rev-parse "${candidate}^{commit}")"
145+ candidate_parent="$(git rev-parse "${candidate}^{commit}^" 2>/dev/null || true)"
146+ if [[ "$candidate_commit" == "$GITHUB_SHA" || "$candidate_parent" == "$GITHUB_SHA" ]]; then
147+ matches="${matches}${candidate}"$'\n'
148+ fi
149+ done < <(git tag --sort=-creatordate | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true)
150+
151+ match_count="$(printf '%s' "$matches" | awk 'NF { count++ } END { print count + 0 }')"
152+ if (( match_count > 1 )); then
153+ echo "multiple release tags match workflow commit $GITHUB_SHA" >&2
154+ exit 1
155+ fi
156+ tag="$(printf '%s' "$matches" | awk 'NF { print; exit }')"
157+ fi
158+
159+ if [[ -z "$tag" ]]; then
160+ echo "present=false" >> "$GITHUB_OUTPUT"
161+ exit 0
162+ fi
163+ if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
164+ echo "unsupported release tag: $tag" >&2
165+ exit 1
166+ fi
167+ if ! git merge-base --is-ancestor "$tag" origin/main; then
168+ echo "release tag is not on origin/main: $tag" >&2
169+ exit 1
170+ fi
171+
172+ release=""
173+ release_error_file="$(mktemp)"
174+ trap 'rm -f "$release_error_file"' EXIT
175+ for attempt in {1..12}; do
176+ if release="$(gh release view "$tag" --json databaseId,isDraft,isPrerelease,tagName 2>"$release_error_file")"; then
177+ break
178+ fi
179+ if (( attempt == 12 )); then
180+ cat "$release_error_file" >&2
181+ echo "release $tag lookup failed after $attempt attempts" >&2
182+ exit 1
183+ fi
184+ sleep 5
185+ done
186+ rm -f "$release_error_file"
187+ trap - EXIT
188+
189+ if [[ "$(jq -r .tagName <<<"$release")" != "$tag" ]]; then
190+ echo "release lookup returned the wrong tag" >&2
191+ exit 1
192+ fi
193+ if [[ "$(jq -r .isPrerelease <<<"$release")" != "false" ]]; then
194+ echo "stable release tag resolved to a prerelease: $tag" >&2
195+ exit 1
196+ fi
197+
198+ is_draft="$(jq -r .isDraft <<<"$release")"
199+ {
200+ echo "present=true"
201+ echo "tag=$tag"
202+ echo "version=${tag#v}"
203+ echo "draft=$is_draft"
204+ echo "published=$([[ "$is_draft" == "false" ]] && echo true || echo false)"
205+ } >> "$GITHUB_OUTPUT"
206+
129207 build-unix-binaries :
130- if : needs.release.outputs.new_release_published == 'true'
208+ if : needs.release.outputs.release_draft == 'true'
131209 name : Build ${{ matrix.os }} release assets
132210 needs :
133211 - release
@@ -161,7 +239,7 @@ jobs:
161239 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
162240 with :
163241 fetch-depth : 0
164- ref : ${{ needs.release.outputs.new_release_git_tag }}
242+ ref : ${{ needs.release.outputs.release_tag }}
165243 persist-credentials : false
166244
167245 - name : Set up Vite+
@@ -182,7 +260,7 @@ jobs:
182260 - name : Package release assets
183261 shell : pwsh
184262 run : |
185- $version = "${{ needs.release.outputs.new_release_version }}"
263+ $version = "${{ needs.release.outputs.release_version }}"
186264 $assetBase = "putio-cli-$version-${{ matrix.asset_os }}-${{ matrix.asset_arch }}"
187265 $releaseDir = ".artifacts/release"
188266 New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
@@ -205,12 +283,13 @@ jobs:
205283 uses : softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
206284 with :
207285 token : ${{ steps.release-bot.outputs.token }}
208- tag_name : ${{ needs.release.outputs.new_release_git_tag }}
286+ tag_name : ${{ needs.release.outputs.release_tag }}
287+ draft : true
209288 files : |
210289 .artifacts/release/*
211290
212291 build-windows-binary :
213- if : needs.release.outputs.new_release_published == 'true'
292+ if : needs.release.outputs.release_draft == 'true'
214293 name : Build windows-latest release assets
215294 needs :
216295 - release
@@ -234,7 +313,7 @@ jobs:
234313 uses : actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
235314 with :
236315 fetch-depth : 0
237- ref : ${{ needs.release.outputs.new_release_git_tag }}
316+ ref : ${{ needs.release.outputs.release_tag }}
238317 persist-credentials : false
239318
240319 - name : Set up Vite+
@@ -255,7 +334,7 @@ jobs:
255334 - name : Package release assets
256335 shell : pwsh
257336 run : |
258- $version = "${{ needs.release.outputs.new_release_version }}"
337+ $version = "${{ needs.release.outputs.release_version }}"
259338 $assetBase = "putio-cli-$version-windows-amd64"
260339 $releaseDir = ".artifacts/release"
261340 New-Item -ItemType Directory -Force -Path $releaseDir | Out-Null
@@ -273,16 +352,109 @@ jobs:
273352 uses : softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
274353 with :
275354 token : ${{ steps.release-bot.outputs.token }}
276- tag_name : ${{ needs.release.outputs.new_release_git_tag }}
355+ tag_name : ${{ needs.release.outputs.release_tag }}
356+ draft : true
277357 files : |
278358 .artifacts/release/*
279359
360+ finalize-release :
361+ if : >-
362+ ${{
363+ !cancelled() &&
364+ needs.release.result == 'success' &&
365+ needs.release.outputs.release_present == 'true' &&
366+ (
367+ needs.release.outputs.release_published == 'true' ||
368+ (
369+ needs.release.outputs.release_draft == 'true' &&
370+ needs.build-unix-binaries.result == 'success' &&
371+ needs.build-windows-binary.result == 'success'
372+ )
373+ )
374+ }}
375+ name : Verify and publish GitHub release
376+ needs :
377+ - release
378+ - build-unix-binaries
379+ - build-windows-binary
380+ runs-on : ubuntu-latest
381+ timeout-minutes : 10
382+ environment :
383+ name : release
384+ deployment : false
385+ permissions :
386+ contents : read
387+ outputs :
388+ release_ready : ${{ steps.finalized.outputs.ready }}
389+
390+ steps :
391+ - name : Create release bot token
392+ id : release-bot
393+ uses : actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
394+ with :
395+ client-id : ${{ vars.PUTIO_RELEASE_BOT_CLIENT_ID }}
396+ private-key : ${{ secrets.PUTIO_RELEASE_BOT_PRIVATE_KEY }}
397+ permission-contents : write
398+
399+ - name : Verify draft asset manifest and publish
400+ if : needs.release.outputs.release_draft == 'true'
401+ env :
402+ GH_TOKEN : ${{ steps.release-bot.outputs.token }}
403+ RELEASE_TAG : ${{ needs.release.outputs.release_tag }}
404+ RELEASE_VERSION : ${{ needs.release.outputs.release_version }}
405+ run : |
406+ set -euo pipefail
407+ release="$(gh release view "$RELEASE_TAG" --json assets,isDraft,isPrerelease)"
408+ [[ "$(jq -r .isDraft <<<"$release")" == "true" ]]
409+ [[ "$(jq -r .isPrerelease <<<"$release")" == "false" ]]
410+ expected="$(printf '%s\n' \
411+ "putio-cli-${RELEASE_VERSION}-darwin-arm64.tar.gz" \
412+ "putio-cli-${RELEASE_VERSION}-darwin-arm64.tar.gz.sha256" \
413+ "putio-cli-${RELEASE_VERSION}-linux-amd64.tar.gz" \
414+ "putio-cli-${RELEASE_VERSION}-linux-amd64.tar.gz.sha256" \
415+ "putio-cli-${RELEASE_VERSION}-windows-amd64.zip" \
416+ "putio-cli-${RELEASE_VERSION}-windows-amd64.zip.sha256" | sort)"
417+ actual="$(jq -r '.assets[].name' <<<"$release" | sort)"
418+ diff -u <(printf '%s\n' "$expected") <(printf '%s\n' "$actual")
419+ gh release edit "$RELEASE_TAG" --draft=false --latest
420+
421+ - name : Verify published release
422+ id : finalized
423+ env :
424+ GH_TOKEN : ${{ steps.release-bot.outputs.token }}
425+ RELEASE_TAG : ${{ needs.release.outputs.release_tag }}
426+ RELEASE_VERSION : ${{ needs.release.outputs.release_version }}
427+ run : |
428+ set -euo pipefail
429+ expected="$(printf '%s\n' \
430+ "putio-cli-${RELEASE_VERSION}-darwin-arm64.tar.gz" \
431+ "putio-cli-${RELEASE_VERSION}-darwin-arm64.tar.gz.sha256" \
432+ "putio-cli-${RELEASE_VERSION}-linux-amd64.tar.gz" \
433+ "putio-cli-${RELEASE_VERSION}-linux-amd64.tar.gz.sha256" \
434+ "putio-cli-${RELEASE_VERSION}-windows-amd64.zip" \
435+ "putio-cli-${RELEASE_VERSION}-windows-amd64.zip.sha256" | sort)"
436+ for attempt in {1..12}; do
437+ release="$(gh release view "$RELEASE_TAG" --json assets,isDraft,isPrerelease)"
438+ actual="$(jq -r '.assets[].name' <<<"$release" | sort)"
439+ if [[ "$(jq -r .isDraft <<<"$release")" == "false" ]] &&
440+ [[ "$(jq -r .isPrerelease <<<"$release")" == "false" ]] &&
441+ diff -u <(printf '%s\n' "$expected") <(printf '%s\n' "$actual"); then
442+ echo "ready=true" >> "$GITHUB_OUTPUT"
443+ exit 0
444+ fi
445+ if (( attempt == 12 )); then
446+ echo "release $RELEASE_TAG did not reach the expected published state" >&2
447+ exit 1
448+ fi
449+ sleep 5
450+ done
451+
280452 update-homebrew-tap :
281- if : needs.release.outputs.new_release_published == 'true'
453+ if : needs.finalize- release.outputs.release_ready == 'true'
282454 name : Update Homebrew tap
283455 needs :
284456 - release
285- - build-unix-binaries
457+ - finalize-release
286458 runs-on : ubuntu-latest
287459 timeout-minutes : 20
288460 environment :
@@ -329,6 +501,7 @@ jobs:
329501 commit_email : ${{ steps.homebrew-release-bot-user.outputs.user_id }}+${{ steps.homebrew-release-bot.outputs.app-slug }}[bot]@users.noreply.github.com
330502 branch : main
331503 formula_folder : Formula
504+ version : ${{ needs.release.outputs.release_tag }}
332505 install : ' bin.install "putio"'
333506 test : |
334507 output = shell_output("#{bin}/putio version")
@@ -337,3 +510,16 @@ jobs:
337510 target_darwin_arm64 : true
338511 target_linux_amd64 : true
339512 skip_checksum : true
513+ ignore_warnings : true
514+
515+ - name : Verify Homebrew tap points at the release
516+ env :
517+ GH_TOKEN : ${{ steps.homebrew-release-bot.outputs.token }}
518+ RELEASE_TAG : ${{ needs.release.outputs.release_tag }}
519+ RELEASE_VERSION : ${{ needs.release.outputs.release_version }}
520+ run : |
521+ set -euo pipefail
522+ formula="$(gh api repos/putdotio/homebrew-tap/contents/Formula/putio-cli.rb --jq .content | base64 --decode)"
523+ grep -Fq "/archive/refs/tags/${RELEASE_TAG}.tar.gz" <<<"$formula"
524+ grep -Fq "/releases/download/${RELEASE_TAG}/putio-cli-${RELEASE_VERSION}-darwin-arm64.tar.gz" <<<"$formula"
525+ grep -Fq "/releases/download/${RELEASE_TAG}/putio-cli-${RELEASE_VERSION}-linux-amd64.tar.gz" <<<"$formula"
0 commit comments