name: Release # CLI binary line. Tag namespace `v` triggers this — it builds the # release archives + checksums, publishes the GitHub release, and updates the # Homebrew cask. npm is a SEPARATE line (release-npm.yml, `npm-v*`) so an npm # prerelease can ship without forcing the stable cask, and vice versa; desktop is # `desktop-v*` (release-desktop.yml). A prerelease `v*-rc*` still builds archives # and a prerelease GitHub release, but goreleaser auto-skips the cask upload # (brew has no prerelease channel — see .goreleaser.yaml). on: push: tags: ['v*'] permissions: contents: write # create the release and upload archives jobs: cache-guard: name: cache hit guard runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - run: ./scripts/cache-guard.sh goreleaser: name: archives + checksums + homebrew tap needs: cache-guard runs-on: ubuntu-latest # CLI stable release (archives + GitHub release + Homebrew cask). It does not # claim GitHub's repository-wide Latest badge; that belongs to desktop so the # repository homepage points users at the installable app. Gated on the # `release` environment so only esengine can approve it going public. environment: release steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - uses: goreleaser/goreleaser-action@v7 with: version: '~> v2' args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - name: Attach desktop manifest compatibility asset env: GH_TOKEN: ${{ github.token }} TAG: ${{ github.ref_name }} HAS_R2: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ACCOUNT_ID != '' && secrets.R2_BUCKET != '' }} R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }} R2_BUCKET: ${{ secrets.R2_BUCKET }} run: | set -euo pipefail case "$TAG" in *-*) echo "prerelease $TAG — GitHub latest does not move here; skipping desktop manifest compatibility asset" exit 0 ;; esac if [ "$HAS_R2" != "true" ]; then echo "R2 secrets not configured; skipping desktop manifest compatibility asset" exit 0 fi # dl.reasonix.io serves 403 to GitHub Actions egress IPs (Cloudflare bot # protection), so read the manifest over the authenticated S3 API instead # of the public edge. aws configure set aws_access_key_id "${{ secrets.R2_ACCESS_KEY_ID }}" aws configure set aws_secret_access_key "${{ secrets.R2_SECRET_ACCESS_KEY }}" aws configure set region auto aws s3 cp "s3://${R2_BUCKET}/latest/latest.json" latest.raw.json \ --endpoint-url "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" jq '.download_page = "https://reasonix.io/#start"' latest.raw.json > latest.json jq -e ' ([.platforms[] | (.url, .sig)] | all(type == "string" and startswith("https://dl.reasonix.io/") and (contains("/releases/latest/") | not))) ' latest.json >/dev/null gh release upload "$TAG" latest.json --clobber # The compatibility asset exists for pre-v1.16 desktop updaters that # still poll GitHub's repository-wide latest URL. Desktop releases now # own that Latest badge, but this check still exercises the public fallback # path exactly the way those clients fetch it: anonymously, over the public # edge, with a Go client UA. Unlike dl.reasonix.io (whose bot protection # 403s Actions egress — see the R2 note above), GitHub serves its own # runners, so this can hard-fail. #5826/#5858 shipped a broken update check # for weeks precisely because nothing exercised the public path. Retries # cover the release CDN propagating the freshly uploaded asset. - name: Smoke public compatibility manifest env: TAG: ${{ github.ref_name }} HAS_R2: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ACCOUNT_ID != '' && secrets.R2_BUCKET != '' }} run: | set -euo pipefail case "$TAG" in *-*) echo "prerelease $TAG — no compatibility asset uploaded; skipping" exit 0 ;; esac if [ "$HAS_R2" != "true" ]; then echo "R2 secrets not configured; no compatibility asset uploaded; skipping" exit 0 fi url="https://github.com/${{ github.repository }}/releases/latest/download/latest.json" for attempt in 1 2 3 4 5 6; do if curl -fsSL -A "Go-http-client/2.0" -o /tmp/compat-latest.json "$url"; then jq -e '(.version | type == "string") and (.platforms | type == "object")' /tmp/compat-latest.json >/dev/null echo "public compatibility manifest OK (desktop version $(jq -r .version /tmp/compat-latest.json))" exit 0 fi echo "attempt $attempt failed; retrying in 10s" sleep 10 done echo "::error::public compatibility manifest unreachable at $url" exit 1 # A stable CLI release must never leave the npm line behind: v1.17.5 # shipped as binaries/Homebrew while npm `latest` still pointed at 0.53.2 # (#5822) — every `npm update -g` user was silently downgraded to a # months-old version, and nothing noticed because the npm line # (release-npm.yml, `npm-vX.Y.Z` tags) is triggered independently and the # stable npm tag was simply never pushed. release-npm.yml's own verify # step only guards runs that happen; this guard catches the run that # DIDN'T. # # Two distinct states, two responses (the runs race: both workflows wait # on the release environment independently, and npm dist-tags propagate # asynchronously, so "tag pushed but latest not moved yet" is a NORMAL # mid-release state, not a failure): # - npm-v tag missing -> hard fail. This is the #5822 gap: # nobody pushed the npm release at all. # - tag pushed, latest lagging -> poll briefly, then WARN and pass. # The npm run may still be awaiting its own environment approval; # release-npm.yml's verify step owns asserting the dist-tag lands. - name: Check npm latest dist-tag freshness env: TAG: ${{ github.ref_name }} run: | set -euo pipefail case "$TAG" in *-*) echo "prerelease $TAG — npm latest does not move on prereleases; skipping" exit 0 ;; esac version="${TAG#v}" if ! git ls-remote --exit-code origin "refs/tags/npm-v$version" >/dev/null; then echo "::error::the npm-v$version tag was never pushed — the npm channel is being left behind and 'npm update -g' users will be downgraded to the old 'latest'. Push it: git tag npm-v$version ${TAG} && git push origin npm-v$version (or 'npm dist-tag add reasonix@$version latest' for an already-published version)." exit 1 fi for attempt in 1 2 3 4 5 6; do got="$(npm view reasonix dist-tags.latest 2>/dev/null || true)" if [ -n "$got" ]; then newest="$(printf '%s\n%s\n' "$got" "$version" | sort -V | tail -1)" if [ "$newest" = "$got" ]; then echo "npm latest -> $got (>= $version) OK" exit 0 fi fi echo "npm latest -> ${got:-}, want >= $version (attempt $attempt)" sleep 10 done echo "::warning::npm-v$version is pushed but npm 'latest' is still ${got:-} — the npm publish run is likely awaiting release-environment approval or still propagating. Approve/monitor the release-npm run; its verify step asserts the dist-tag lands." exit 0