Skip to content

Commit 17a90d7

Browse files
ralyodioclaude
andauthored
ci(release): publish a release as Latest only after every asset uploaded; install.sh falls back to an earlier release (#58)
softprops/action-gh-release created the release public and Latest on the first platform's upload, so for ~3 minutes after each tag releases/latest/download/<artifact> 404'd for the platforms still building and `curl -fsSL https://c0mpute.com/install.sh | sh` failed. release.yml: a `draft` job creates the release as a draft (or reuses it), the five platform jobs upload into the draft with `gh release upload --clobber`, and `publish` (needs all five) checks every tarball/zip and its .sha256 is on the release, then `gh release edit --draft=false --latest` and checks each asset downloads. A failed platform leaves Latest on the previous release. workflow_dispatch takes a tag input. install.sh: when C0MPUTE_VERSION=latest and the asset is not 200, walk the six most recent published releases (GitHub API, no jq) and install the first that has this platform's asset. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent afa9d55 commit 17a90d7

2 files changed

Lines changed: 92 additions & 8 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ name: release
55
# aarch64, packages each as a tarball (zip on Windows), generates a
66
# SHA-256 checksum, and uploads the lot to a GitHub Release.
77
#
8+
# The release is created as a DRAFT first, every platform uploads into the
9+
# draft, and only when all five uploads succeeded does the `publish` job make
10+
# it public and mark it Latest. Before this, softprops/action-gh-release
11+
# published + marked Latest on the first upload, so for ~3 minutes after a
12+
# tag `releases/latest/download/<artifact>` 404'd for every platform still
13+
# building, and `curl -fsSL https://c0mpute.com/install.sh | sh` failed.
14+
# A failed platform leaves the draft unpublished (Latest stays on the
15+
# previous release); fix and re-run the failed jobs.
16+
#
817
# Stable install URL: https://c0mpute.com/releases/latest/<artifact>
918
# redirects to https://github.com/profullstack/c0mpute/releases/latest/download/<artifact>
1019
# (configured in apps/web/next.config.mjs).
@@ -14,13 +23,38 @@ on:
1423
tags:
1524
- "v*"
1625
workflow_dispatch:
26+
inputs:
27+
tag:
28+
description: "Existing tag to (re)build and publish, e.g. v0.2.33"
29+
required: true
1730

1831
permissions:
1932
contents: write
2033

34+
env:
35+
TAG: ${{ inputs.tag || github.ref_name }}
36+
GH_REPO: ${{ github.repository }}
37+
2138
jobs:
39+
draft:
40+
name: create draft release
41+
runs-on: ubuntu-latest
42+
steps:
43+
- name: Create the release as a draft (or reuse it)
44+
env:
45+
GH_TOKEN: ${{ github.token }}
46+
run: |
47+
set -eu
48+
case "$TAG" in v*) ;; *) echo "not a release tag: $TAG" >&2; exit 1 ;; esac
49+
if gh release view "$TAG" --json isDraft >/dev/null 2>&1; then
50+
echo "release $TAG exists (draft=$(gh release view "$TAG" --json isDraft -q .isDraft)); uploads will replace its assets"
51+
else
52+
gh release create "$TAG" --draft --verify-tag --generate-notes --title "$TAG"
53+
fi
54+
2255
release:
2356
name: ${{ matrix.platform }}
57+
needs: draft
2458
runs-on: ${{ matrix.os }}
2559
strategy:
2660
fail-fast: false
@@ -52,6 +86,8 @@ jobs:
5286

5387
steps:
5488
- uses: actions/checkout@v5
89+
with:
90+
ref: ${{ env.TAG }}
5591

5692
- name: Install Rust toolchain
5793
uses: dtolnay/rust-toolchain@stable
@@ -94,11 +130,34 @@ jobs:
94130
"ARTIFACT=$artifact" | Out-File -Append -Encoding ascii $env:GITHUB_ENV
95131
"ARTIFACT_SHA=$artifact.sha256" | Out-File -Append -Encoding ascii $env:GITHUB_ENV
96132
97-
- name: Upload to GitHub Release
98-
uses: softprops/action-gh-release@v2
99-
with:
100-
files: |
101-
${{ env.ARTIFACT }}
102-
${{ env.ARTIFACT_SHA }}
103-
fail_on_unmatched_files: true
104-
generate_release_notes: true
133+
- name: Upload to the draft release
134+
shell: bash
135+
env:
136+
GH_TOKEN: ${{ github.token }}
137+
run: gh release upload "$TAG" "$ARTIFACT" "$ARTIFACT_SHA" --clobber
138+
139+
publish:
140+
name: publish + mark latest
141+
# Runs only when every platform job succeeded (needs + default success()).
142+
needs: release
143+
runs-on: ubuntu-latest
144+
steps:
145+
- name: Check every asset is on the release, then publish it as Latest
146+
env:
147+
GH_TOKEN: ${{ github.token }}
148+
run: |
149+
set -eu
150+
want="c0mpute-linux-x86_64.tar.gz c0mpute-linux-aarch64.tar.gz c0mpute-darwin-x86_64.tar.gz c0mpute-darwin-aarch64.tar.gz c0mpute-windows-x86_64.zip"
151+
have=$(gh release view "$TAG" --json assets -q '.assets[].name')
152+
for a in $want; do
153+
for f in "$a" "$a.sha256"; do
154+
echo "$have" | grep -qx "$f" || { echo "missing asset $f on $TAG; not publishing" >&2; exit 1; }
155+
done
156+
done
157+
gh release edit "$TAG" --draft=false --latest
158+
# The public download URL must now serve every platform.
159+
for a in $want; do
160+
code=$(curl -s -o /dev/null -w '%{http_code}' -L "https://github.com/$GH_REPO/releases/download/$TAG/$a")
161+
echo "$a -> $code"
162+
[ "$code" = "200" ] || { echo "asset $a not downloadable after publish" >&2; exit 1; }
163+
done

‎scripts/install.sh‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ ensure_bun() {
199199
# c0mpute itself
200200
# ────────────────────────────────────────────────────────────────────────
201201

202+
# Tags of the most recent published releases, newest first (drafts are not
203+
# listed). Empty when GitHub's API is unreachable or rate limited.
204+
recent_release_tags() {
205+
curl -fsSL -H 'Accept: application/vnd.github+json' \
206+
"${C0MPUTE_RELEASES_API:-https://api.github.com/repos/profullstack/c0mpute/releases?per_page=6}" 2>/dev/null \
207+
| sed -n 's/^ *"tag_name": *"\([^"]*\)".*/\1/p' | head -n 6
208+
}
209+
202210
install_c0mpute() {
203211
platform="$1"
204212
target="$C0MPUTE_HOME/bin/c0mpute"
@@ -221,6 +229,23 @@ install_c0mpute() {
221229

222230
say "downloading c0mpute ${C0MPUTE_VERSION}"
223231
http_code=$(curl -sSL -o "$tmp/$artifact" -w '%{http_code}' "$url" 2>/dev/null || echo "000")
232+
if [ "$http_code" != "200" ] && [ "$C0MPUTE_VERSION" = "latest" ]; then
233+
# The newest release can be public before its binaries finish uploading
234+
# (or a platform build failed). Walk back through recent releases and
235+
# take the first one that has this platform's asset.
236+
warn "latest c0mpute release has no ${artifact} yet (HTTP ${http_code}); trying the previous releases"
237+
for tag in $(recent_release_tags); do
238+
try_url="${RELEASE_BASE}/${tag}/${artifact}"
239+
code=$(curl -sSL -o "$tmp/$artifact" -w '%{http_code}' "$try_url" 2>/dev/null || echo "000")
240+
if [ "$code" = "200" ]; then
241+
say "using c0mpute ${tag}"
242+
url="$try_url"
243+
sig_url="${url}.minisig"
244+
http_code=200
245+
break
246+
fi
247+
done
248+
fi
224249
if [ "$http_code" != "200" ]; then
225250
rm -rf "$tmp"
226251
cat <<EOF >&2

0 commit comments

Comments
 (0)