Skip to content

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

Description

@IzaakGough

[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:

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:

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.

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:

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.

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 [binary-publish-script] Enhance publish script reliability #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 (Install script should abort on network failure #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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions