Skip to content

fix: daemon download URL and SPA routing on Render - #69

Merged
aspectrr merged 2 commits into
mainfrom
fix/daemon-download-url-and-spa-redirects
Feb 22, 2026
Merged

fix: daemon download URL and SPA routing on Render#69
aspectrr merged 2 commits into
mainfrom
fix/daemon-download-url-and-spa-redirects

Conversation

@aspectrr

Copy link
Copy Markdown
Owner

Summary

  • Daemon download fix: stepDownloadDaemon() was using /releases/latest/download/fluid-daemon_linux_{arch}.tar.gz which 404s because actual release assets are named fluid-daemon_{version}_linux_{arch}.tar.gz. Now queries GitHub API for the latest tag first, then constructs the correct versioned URL. Uses grep+cut instead of jq to avoid extra dependencies on the remote host.
  • SPA routing fix: Added _redirects file for Render static site hosting so client-side routes (e.g. /docs/) fall back to index.html instead of returning "Not Found".

Test plan

  • cd fluid-cli && make test - all tests pass
  • Verify /docs/ route works on Render after deploy
  • Verify daemon download resolves correctly on a fresh host setup

🤖 Generated with Claude Code

- Resolve 404 on daemon download by querying GitHub API for latest tag
  and constructing the correct versioned asset URL
- Add _redirects file for Render static site hosting so client-side
  routes (e.g. /docs/) fall back to index.html

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 22, 2026 19:12
@claude

claude Bot commented Feb 22, 2026

Copy link
Copy Markdown

PR Review

Two targeted fixes — both address real problems. Here's my feedback:


fluid-cli/internal/setup/steps.go

Potential bug: Commands slice vs. Execute func have different execution semantics

In the Commands slice, two strings are added:

Commands: []string{
    `TAG=$(curl -fsSL https://api.github.com/repos/aspectrr/fluid.sh/releases/latest | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4)`,
    fmt.Sprintf("curl -fsSL -o /tmp/fluid-daemon.tar.gz https://github.com/aspectrr/fluid.sh/releases/download/${TAG}/fluid-daemon_${TAG#v}_linux_%s.tar.gz", arch),
},

If each command in the Commands slice is executed as a separate shell invocation (which is the common behavior for exec-based runners), the $TAG variable set in the first string will not be available in the second. You'd get a malformed URL like .../releases/download//fluid-daemon__linux_amd64.tar.gz.

The Execute func correctly handles this by joining with && in a single shell string — that's the right approach. The Commands field should follow the same pattern, or if Commands is only used for display (e.g., a "what will run" preview), it should either join them into one string or note the dependency clearly.

Recommendation: Either collapse the two Commands entries into one shell string using &&, or verify that Commands is only used for display (and update it to show the combined command for accuracy).


Error handling when no release exists or API fails

If grep finds no match (e.g., no releases published yet, or the API shape changes), $TAG will be an empty string and curl will silently download a 404 page into /tmp/fluid-daemon.tar.gz. The Check function only tests for file existence, so the setup could proceed with a corrupt tarball.

A hardening option:

TAG=$(curl -fsSL https://api.github.com/repos/aspectrr/fluid.sh/releases/latest | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4) && \
[ -n "$TAG" ] && \
curl -fsSL -o /tmp/fluid-daemon.tar.gz "https://github.com/aspectrr/fluid.sh/releases/download/${TAG}/fluid-daemon_${TAG#v}_linux_%s.tar.gz"

GitHub API rate limiting

Unauthenticated GitHub API requests are limited to 60/hour per IP. For users running setup from a shared CI/CD IP or corporate NAT, this could cause intermittent failures. Not a blocker, but worth documenting or considering an authenticated fallback.


${TAG#v} portability

${TAG#v} is a bash parameter expansion — it works on any system where /bin/sh is bash (most Linux distros). On systems where /bin/sh is dash (e.g., some minimal Ubuntu installs), this will fail. If the remote host could have dash as /bin/sh, using VERSION=$(echo "$TAG" | sed 's/^v//') is safer.


web/public/_redirects

The SPA redirect rule is correct and idiomatic for Render:

/*    /index.html   200

No issues here. Works as expected for client-side routing.


Test coverage

The test plan confirms make test passes, but there's no unit test for the daemon download step logic itself. Given the Commands vs Execute discrepancy noted above, a test that mocks the GitHub API response and verifies the constructed URL would catch regressions. Not blocking, but worth a follow-up.


Summary: The Execute fix is correct and the _redirects addition is clean. The main concern is the Commands slice likely not sharing shell state between entries — worth verifying and fixing before merge.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes two issues: (1) corrects the daemon download URL to query GitHub API for the actual release tag and construct the proper versioned URL, and (2) adds SPA routing support for Render static hosting via a _redirects file.

Changes:

  • Fixed daemon download URL by querying GitHub API for latest release tag instead of using non-existent /releases/latest/download path
  • Added _redirects file to support client-side routing in Render deployments

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
web/public/_redirects Adds Render SPA routing rule to fallback to index.html for all routes
fluid-cli/internal/setup/steps.go Updates stepDownloadDaemon to fetch latest tag from GitHub API and construct correct versioned download URL

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread fluid-cli/internal/setup/steps.go Outdated
Comment on lines +78 to +79
`TAG=$(curl -fsSL https://api.github.com/repos/aspectrr/fluid.sh/releases/latest | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4)`,
fmt.Sprintf("curl -fsSL -o /tmp/fluid-daemon.tar.gz https://github.com/aspectrr/fluid.sh/releases/download/${TAG}/fluid-daemon_${TAG#v}_linux_%s.tar.gz", arch),

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Commands field is used for display purposes in the UI (see fluid-cli/internal/tui/onboarding.go:1605), but these commands won't work correctly if run separately since line 79 depends on the TAG variable set in line 78. Each command in the array is displayed independently. Consider combining them into a single command string with && to show the correct sequence, similar to the Execute function implementation on line 88.

Suggested change
`TAG=$(curl -fsSL https://api.github.com/repos/aspectrr/fluid.sh/releases/latest | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4)`,
fmt.Sprintf("curl -fsSL -o /tmp/fluid-daemon.tar.gz https://github.com/aspectrr/fluid.sh/releases/download/${TAG}/fluid-daemon_${TAG#v}_linux_%s.tar.gz", arch),
fmt.Sprintf(
`TAG=$(curl -fsSL https://api.github.com/repos/aspectrr/fluid.sh/releases/latest | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4) && VERSION=${TAG#v} && curl -fsSL -o /tmp/fluid-daemon.tar.gz "https://github.com/aspectrr/fluid.sh/releases/download/${TAG}/fluid-daemon_${VERSION}_linux_%s.tar.gz"`,
arch,
),

Copilot uses AI. Check for mistakes.
Comment thread fluid-cli/internal/setup/steps.go Outdated
arch := runtime.GOARCH
cmd := fmt.Sprintf(
"curl -fsSL -o /tmp/fluid-daemon.tar.gz https://github.com/aspectrr/fluid.sh/releases/latest/download/fluid-daemon_linux_%s.tar.gz",
`TAG=$(curl -fsSL https://api.github.com/repos/aspectrr/fluid.sh/releases/latest | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4) && VERSION=${TAG#v} && curl -fsSL -o /tmp/fluid-daemon.tar.gz "https://github.com/aspectrr/fluid.sh/releases/download/${TAG}/fluid-daemon_${VERSION}_linux_%s.tar.gz"`,

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shell command doesn't validate whether the GitHub API call succeeded or returned valid data. If the API is rate-limited, down, or returns an error, TAG will be empty and the subsequent download URL will be malformed. Consider adding error checking such as checking if TAG is non-empty before proceeding, or using curl's --fail flag combined with checking the exit code before the download step.

Copilot uses AI. Check for mistakes.
Comment thread fluid-cli/internal/setup/steps.go Outdated
arch := runtime.GOARCH
cmd := fmt.Sprintf(
"curl -fsSL -o /tmp/fluid-daemon.tar.gz https://github.com/aspectrr/fluid.sh/releases/latest/download/fluid-daemon_linux_%s.tar.gz",
`TAG=$(curl -fsSL https://api.github.com/repos/aspectrr/fluid.sh/releases/latest | grep -o '"tag_name":"[^"]*"' | head -1 | cut -d'"' -f4) && VERSION=${TAG#v} && curl -fsSL -o /tmp/fluid-daemon.tar.gz "https://github.com/aspectrr/fluid.sh/releases/download/${TAG}/fluid-daemon_${VERSION}_linux_%s.tar.gz"`,

Copilot AI Feb 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitHub API can be rate-limited (60 requests per hour for unauthenticated requests). Consider handling this scenario gracefully or documenting the limitation. Users running setup multiple times in quick succession might encounter rate limiting errors. Alternative approaches could include: 1) caching the latest tag locally, 2) providing a fallback mechanism, or 3) detecting rate limit errors and providing a helpful error message to users.

Copilot uses AI. Check for mistakes.
- Share single downloadCmd between Commands (display) and Execute
- Add [ -n "$TAG" ] guard to fail early if GitHub API returns no tag
- Remove redundant arch redeclaration in Execute closure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Feb 22, 2026

Copy link
Copy Markdown

Code Review

_redirects (web/public/_redirects)

Simple and correct. The /* /index.html 200 catchall is the standard Render SPA routing fix. No issues.


stepDownloadDaemon() (fluid-cli/internal/setup/steps.go)

The intent is correct — the old /releases/latest/download/ URL 404s because release assets use versioned names. However there are a few issues:

Bug: grep pattern won't match GitHub's JSON format

The GitHub API returns formatted JSON with a space after the colon:

"tag_name": "v1.2.3"

The current pattern '"tag_name":"[^"]*"' has no space after the colon and will fail to match, leaving $TAG empty. The command then silently exits with code 1.

Fix — allow optional space in the pattern:

grep -o '"tag_name": *"[^"]*"'

Or strip whitespace from the response before grepping:

curl -fsSL ... | tr -d ' \t' | grep -o '"tag_name":"[^"]*"'

Silent failure when TAG resolution fails

[ -n "$TAG" ] && VERSION=... exits 1 when $TAG is empty but writes nothing to stderr. The user sees:

download failed (exit 1):

Better to emit an explicit message:

[ -n "$TAG" ] || { echo "Failed to resolve latest release tag from GitHub API" >&2; exit 1; }

Commands display field now shows the full one-liner

The Commands field is rendered in the TUI step display. The old value was a concise, readable curl URL. Now it is a ~220-character shell script. Consider a short human-readable placeholder:

Commands: []string{
    "# fetch latest tag from GitHub API",
    fmt.Sprintf("curl .../fluid-daemon_VERSION_linux_%s.tar.gz", arch),
},

Minor: unauthenticated GitHub API rate limit

Unauthenticated requests are capped at 60/hour per IP. Repeated setup runs (e.g. CI or re-provisioning) could hit this. Not blocking, just worth noting.


Summary

_redirects ✅ Correct
grep pattern space bug ❌ TAG will always be empty; all installs will fail
Silent failure on empty TAG ⚠️ Poor error UX
Commands display readability ⚠️ Minor regression

The grep pattern bug is the main concern — it will break every install attempt with the new code path.

@aspectrr
aspectrr merged commit 116946d into main Feb 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants