firebase / firebase/firebase-tools

NPM advertises each release before its binaries are downloadable, breaking `curl -sL firebase.tools | bash`

Open
#11,039 0 comments 0 reactions 1 assignee Claimed by @joehan View on GitHub
type: bug
Dominant language
TypeScript
Stars
4.5k
Forks
1.3k
Avg merge
1d 12h
Merged PRs (30d)
84

Description

### [REQUIRED] Environment info

**firebase-tools:** the install script served at https://firebase.tools, checked 2026-09-04 (installing 15.29.0). Reports of this go back to 15.0.0.

**Platform:** macOS 25.6 (reproduced here), also reported on Linux CI (CircleCI, Azure DevOps)

### [REQUIRED] Test case

`curl -sL https://firebase.tools | bash`, run while the newest version on NPM does not yet have a downloadable GitHub release asset. That window opens on every release, so this is timing dependent rather than version specific.

### [REQUIRED] Steps to reproduce

The mechanism, which needs no waiting. `v99.99.99` stands in for any version whose asset is not downloadable yet:

```sh
curl -o /tmp/fb -L --progress-bar https://firebase.tools/bin/macos/v99.99.99
chmod +rx /tmp/fb && /tmp/fb --version
# /tmp/fb: line 1: Not: command not found
```

`/bin/:os/:version` 302s to the release asset on github.com, and a missing asset answers with a 9 byte body of `Not Found`, which is what ends up on disk.

To see the full script do it, run it with an isolated `HOME` and `FIREBASE_BINARY` so your real install is not touched, with the version pinned to one that has no assets:

```sh
mkdir -p /tmp/lab/bin /tmp/lab/home
printf '#!/bin/bash\ncase "$1" in\n --version) echo 15.0.0-fake;;\n --tool:setup-check) echo "{\\"bins\\":{}}";;\nesac\n' > /tmp/lab/bin/firebase
chmod +x /tmp/lab/bin/firebase
curl -sL https://firebase.tools | sed 's/^LATEST_NPM_VERSION=.*/LATEST_NPM_VERSION=99.99.99/' \
| env HOME=/tmp/lab/home FIREBASE_BINARY=/tmp/lab/bin/firebase upgrade=true analytics=false bash
cat /tmp/lab/bin/firebase # Not Found
```

### [REQUIRED] Expected behavior

A failed download aborts, and an existing install is left alone.

### [REQUIRED] Actual behavior

The HTTP error body is installed as the binary and marked executable, so the CLI is broken until the user reinstalls:

```
-- Downloading binary from https://firebase.tools/bin/macos/v15.28.2
-- Setting permissions on binary... /usr/local/bin/firebase
/usr/local/bin/firebase: line 1: Not: command not found
Something went wrong, firebase has not been installed.
```

`which firebase` still resolves, and `firebase --help` keeps failing the same way, because the previous working binary was overwritten.

### Root cause

Two things combine, and either one alone would be harmless.

**1. A version is advertised on NPM before its binary is downloadable.**

- `scripts/publish.sh:140-141` publishes to NPM.
- `scripts/publish/cloudbuild.yaml:114` then waits for the package to appear on NPM.
- `scripts/publish.sh:163-164` creates the GitHub release with `hub release create --draft`.
- `scripts/publish/cloudbuild.yaml:150-158` builds the standalone binaries, uploads them to that draft, and prints "Please review the draft release notes ... If it looks good, publish it".

So the assets only become publicly downloadable when someone publishes the draft by hand, which is after NPM already advertises the version. The install script takes its version from NPM (`registry.npmjs.org/firebase-tools/latest`) and downloads it from the release asset, so it asks for a version that is not there yet.

Measuring NPM publish time against `max(release published_at, macos asset created_at)` for 76 releases since January 2025:

- every single one reached NPM before its binary was downloadable
- median window 19 minutes, p90 2.1 hours
- 11 releases over 1 hour, longest 90.7 hours (v15.9.0)
- v15.28.2 was 20.4 hours, v15.23.0 16.5 hours

(A further 21 releases are excluded because their asset `created_at` is more than an hour after the release was published, which I cannot tell apart from a delete and re-upload.)

**2. The install script installs whatever `curl` left behind.**

```sh
curl -o "/tmp/firebase_standalone.tmp" -L --progress-bar $DOWNLOAD_URL
```

There is no `--fail` and the exit code is not checked, so an error response is saved, moved over `$FIREBASE_BINARY` and given `chmod +rx`. The `--version` check that notices runs after the move, which is why a working install is lost. This script is served from firebase.tools and is not in this repo.

### This is what the recurring install reports are

Four reports land inside a measured window:

- #10991 (15.28.2, filed 2h22m into a 20.4h window)
- #10787 (15.23.0, filed 3h29m into a 16.5h window)
- #10031 (15.9.0, inside a 90.7h window)
- #9607 (15.0.0, filed 53m after the NPM publish)

The logs in #10991 and #10787 are the clearest evidence: both show the previous version installing successfully, then the new version failing minutes or hours later on the same machine.

#10031 is the same race with a different symptom. Before the script switched to a version pinned URL it used `/bin/:os/latest`, which resolves to the newest *published* release, so during v15.9.0's window it silently installed 15.8.0 while NPM said 15.9.0. #9607 shows `/latest` producing this exact `Not: command not found` failure too, so the underlying problem predates the versioned URL. The version pinned URL widened the exposure, because it has to wait for the release to be published rather than just for the upload to finish.

#10722 was closed after a missing `v` in the versioned URL was fixed, which was a real and separate bug. The window remains.

### Related issues

Both halves of this have open issues already, so this is filed as the shared root cause rather than as another instance. If you would rather keep it in one place, closing this as a duplicate of either is completely fine.

- **#7868** tracks the binaries not being uploaded for the latest release (#7865, #7648, #8886). The two 2026 cases below are a different mechanism: the binaries were uploaded within minutes, and the release was left as a draft. Stopping the release on a missing build, or alerting on one, would not have caught either.
- **#8931** asks for the script to abort instead of installing a failed download. That reporter hit a truncated transfer rather than a 404, but it is the same fail-open behaviour described in part 2 below.
- Closed instance reports: #9607, #10031, #10727, #10787, #10991, and #10722, which was closed after a missing `v` in the download URL was fixed. That was a real and separate bug, and the window described here is what remains.

### Suggested fix

The draft release looks deliberate, since the build asks for the notes to be reviewed before publishing, so the ordering is the thing to change rather than the review gate:

1. **Pipeline** (this repo, and the part #7868 is closest to): publish to NPM last, after the release is published, so nothing advertises a version before its binary is downloadable. Reviewing the notes still gates the release.
2. **Script** (#8931): add `--fail` and check the exit code, so a failed download exits non-zero and leaves any existing binary in place. This does not close the window, but it turns a broken CLI into a retryable error. I have this patched and tested locally and am happy to send it if that helps, though the script is not in this repo.

Worth noting for anything that retries or falls back: pointing the script at `/bin/:os/latest` on failure would undo the NPM pin added in the same change, since `latest` follows GitHub rather than NPM.

### What I verified how

Run, not read: the download and install behaviour, including the full script reproduction above, the `Not Found` body, and that an existing install is replaced. The release and NPM timestamps come from the GitHub and NPM APIs.

Read, not run: the pipeline ordering, from `publish.sh` and `cloudbuild.yaml` on `main`. I could not exercise a download against a genuinely unpublished draft release, so that step rests on the timestamps and the scripts rather than on an observed request.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.