Z tego przewodnika dowiesz się, jak przenieść rozszerzenia z wycofanego Firebase Extensions środowiska do funkcji, którą użytkownicy instalują i wdrażają w swojej bazie kodu Cloud Functions for Firebase (2 generacji).
Jest to zalecana ścieżka migracji. Firebase będzie prowadzić listę rozszerzeń z oficjalnymi odpowiednikami w npm. Z tego przewodnika dowiesz się, jak utworzyć własne rozszerzenie.
W tym przewodniku jako przykład używamy rozszerzenia Stream Firestore to BigQuery (firestore-bigquery-export). Każda sekcja kończy się przykładem, który pokazuje, jak to rozszerzenie wyglądało przed migracją i jak wygląda po niej jako pakiet @firebase/firestore-bigquery-export.
Zarejestruj się, aby uzyskać więcej informacji i pomocy dotyczącej migracji rozszerzeń
Jeśli masz pytania dotyczące migracji z Firebase Extensions, możesz skontaktować się z nami pod adresem firebase-extensions-migrator-support-external@google.com. Będziemy też wysyłać e-maile do tej grupy, gdy będziemy aktualizować przewodnik o dodatkowe informacje na temat pakowania, testowania i dystrybucji funkcji 2 generacji.
Aby dołączyć do tej grupy, wyślij wiadomość na adres firebase-extensions-migrator-support-external+subscribe@google.com. Otrzymasz e-maila z prośbą o dołączenie. Musisz odpowiedzieć na tego e-maila, a nie kliknąć przycisku „Dołącz do tej grupy”.
Zanim zaczniesz
Aby przeprowadzić tę migrację, użyjesz tych funkcji Cloud Functions:
Konfiguracja z parametrami. Każdy parametr zadeklarowany w
extension.yamlstaje się zdefiniowanym parametrem w kodzie pakietu.Deklaratywne role uprawnień i wymagane interfejsy API. Każda rola zadeklarowana w
extension.yamlstaje się wywołaniemrequiresRole(...), a każdy interfejs API staje się wywołaniemrequiresAPI(...)w kodzie funkcji. Podczas wdrażania interfejs Firebase CLI przyznaje zadeklarowane role zarządzanemu kontu usługi środowiska wykonawczego i włącza zadeklarowane interfejsy API w Twoim imieniu.Zdarzenia cyklu życia w przypadku baz kodu Cloud Functions. Cloud Functions bazy kodu obsługują teraz zdarzenia cyklu życia analogiczne do Firebase Extensions. Zadeklaruj konfigurację podczas instalacji i aktualizacji za pomocą haków cyklu życia
afterFirstDeploy(...)iafterRedeploy(...). Zastępują onelifecycleEventszadeklarowane wextension.yaml.
Sprawdź rozszerzenie
Zacznij od sprawdzenia rozszerzenia: pełnej listy wszystkich elementów, które rozszerzenie deklaruje, dostarcza i dokumentuje. Dzięki temu każde działanie będzie miało zdefiniowane miejsce docelowe w funkcji 2 generacji i nic nie zostanie utracone podczas migracji.
Sprawdź każdy z tych elementów i zanotuj, co znajdziesz:
extension.yaml, który deklaruje parametry, funkcje, zdarzenia, role uprawnień, wymagane interfejsy API, obiekty tajne i haki cyklu życia.functions/, który zawiera kod funkcji, zależności, konfigurację kompilacji, wyzwalacze i funkcje kolejki zadań.README.md,PREINSTALL.mdiPOSTINSTALL.md, które zawierają instrukcje konfiguracji, ostrzeżenia i informacje o rozliczeniach.scripts/, który zawiera narzędzia do importu, uzupełniania, uprawnień, naprawy lub migracji oraz inne narzędzia dostarczane razem z rozszerzeniem.
Następnie w przypadku każdego elementu w
extension.yaml zdecyduj, gdzie ma się on
znaleźć w pakiecie npm:
Przekształć konfigurację użytkownika w Cloud Functions parametry (sekcja 5).
Przekształć obiekty tajne w obiekty tajne Cloud Functions (sekcja 6).
Przekształć role uprawnień w deklaracje
requiresRole(...)(sekcja 8).Przekształć wymagane interfejsy API Google w deklaracje
requiresAPI(...)(sekcja 8).Przekształć haki instalacji i aktualizacji w deklaracje
afterFirstDeploy(...)iafterRedeploy(...)(sekcja 8).
Przykład: Stream Firestore to BigQuery
Odczytanie firestore-bigquery-export/extension.yaml i functions/ daje ten spis:
| W extension.yaml | Liczba / wartość | Gdzie się znajduje |
|---|---|---|
| params | 25 (COLLECTION_PATH, DATASET_ID, TABLE_ID, DATASET_LOCATION, VIEW_TYPE, …) | Cloud Functions parametry (sekcja 5) |
| apis | bigquery.googleapis.com | requiresAPI(...) (sekcja 7) |
| roles | bigquery.dataEditor, datastore.user, bigquery.user | requiresRole(...) (sekcja 7) |
| resources | 1 wyzwalacz zdarzeń (fsexportbigquery) + funkcje kolejki zadań (initBigQuerySync, setupBigQuerySync) | Wyeksportowane funkcje pakietu (sekcja 3) |
| lifecycleEvents | onInstall → initBigQuerySync; onUpdate / onConfigure → setupBigQuerySync | afterFirstDeploy / afterRedeploy (sekcja 9) |
| scripts/ | import/ (backfill), gen-schema-view/ | Zachowane jako skrypty (poza zakresem) |
Rozszerzenie nie deklaruje żadnych parametrów typu secret, więc w sekcji 6 tego przewodnika nie ma nic do przeniesienia. Wyzwalacz zdarzeń jest już 2 generacji, a tylko funkcje kolejki zadań są nadal 1 generacji (istotne w sekcji 3).
Aktualizowanie pliku package.json
Zaktualizuj plik package.json rozszerzenia. Jeśli migrujesz 1 rozszerzenie, może to być główny plik package.json. Jeśli migrujesz wiele rozszerzeń w jednym repozytorium, każde rozszerzenie powinno mieć własny pakiet.
Minimalne wersje pakietu SDK. Zadeklaruj firebase-functions >= 7.3 i firebase-admin >= 14.2.0 jako zależności. Zadeklaruj też swoją wersję firebase-functions jako zależność równorzędną.
{
"name": "<package-name>",
"version": "1.0.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"default": "./lib/index.js"
}
},
"engines": {
"node": ">=22"
},
"peerDependencies": {
"firebase-functions": "^7.3.0"
},
"dependencies": {
"firebase-functions": "^7.3.0",
"firebase-admin": "^14.2.0"
}
}
Oprócz zwykłej zależności zadeklaruj firebase-functions jako zależność równorzędną, aby projekt Cloud Functions użytkowników miał tę samą wersję pakietu SDK, w której napisano Twoją bibliotekę.
Przykład: Stream Firestore to BigQuery
Przed. Plik functions/package.json rozszerzenia jest prywatny, zawiera nazwę identyfikatora rozszerzenia i deklaruje firebase-functions jako bezpośrednią zależność:
{
"name": "firestore-bigquery-export",
"main": "lib/index.js",
"private": true,
"dependencies": {
"@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
"firebase-admin": "^14.2.0",
"firebase-functions": "^6.3.2"
}
}
Po. Pakiet do opublikowania: nazwa w zakresie, mapa eksportów i firebase-functions przeniesione do peerDependencies:
{
"name": "@firebase/firestore-bigquery-export",
"version": "0.1.0",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" }, },
"engines": { "node": ">=22" },
"peerDependencies": { "firebase-functions": "^7.3.0" },
"dependencies": {
"@firebaseextensions/firestore-bigquery-change-tracker": "^2.0.4",
"firebase-admin": "^14.2.0",
"firebase-functions": "^7.3.0"
}
}
Uaktualnianie funkcji z 1 generacji do 2 generacji
Jeśli Twoje rozszerzenie nadal eksportuje funkcje 1 generacji, przekonwertuj każdą funkcję na jej odpowiednik 2 generacji. Importuj z modułów firebase-functions/... i przekazuj ustawienia środowiska wykonawczego w opcjach funkcji.
Możesz zminimalizować nakład pracy związany z przepisywaniem kodu dzięki destrukcji zdarzeń z poprawkami w 2 generacji i uniknąć przepisywania logiki funkcji ponieważ pakiet SDK 2 generacji udostępnia teraz parametry V1 jako pola w obiekcie zdarzenia. Dzięki temu możesz używać parametrów zdestrukturyzowanych lub nazwanych i zachować logikę biznesową bez zmian.
Przed. 1 generacja:
import * as functions from "firebase-functions/v1";
export const sync = functions.firestore
.document("{collectionId}/{documentId}")
.onWrite(async (change, context) => {
await handleWrite(change.before, change.after, context.params);
});
Po. 2 generacja:
import { onDocumentWritten } from "firebase-functions/firestore";
export const syncV2 = onDocumentWritten(
{ document: "{collectionId}/{documentId}" },
async ({change,context}) =>
await handleWrite(change.before, change.after, context.params);
);
Pełną listę różnic między funkcjami 1 generacji i 2 generacji znajdziesz w Cloud Functions porównaniu wersji Cloud Functions.
Konwertowanie parametrów i obiektów tajnych rozszerzenia
Konwertowanie parametrów
Każdy parametr zadeklarowany w extension.yaml staje się Cloud Functions
parametrem.
Konwertuj bezpośrednie odczyty środowiska:
const collectionPath = process.env.COLLECTION_PATH;
na Cloud Functions parametry:
import { defineString } from "firebase-functions/params";
import { onDocumentWritten} from "firebase-functions/firestore";
const collectionPath = defineString("COLLECTION_PATH");
// Pass the param directly when used as a placeholder (e.g. trigger path)
export const sync = onDocumentWritten(
{ document: collectionPath },
async (event) => {
// Call .value() to read the string inside a handler
const path = collectionPath.value();
await handleWrite(path, event);
}
);
Aby odczytać ciąg znaków wewnątrz obsługi, użyj collectionPath.value(). Użyj
collectionPath bezpośrednio tam, gdzie oczekiwany jest obiekt zastępczy, np. ścieżka wyzwalacza funkcji
.
Interfejs Firebase CLI wykrywa parametry i odczytuje ich wartości z .env, .env.projectId, lub wyświetla użytkownikom prośbę o podanie wartości podczas wdrażania. Zachowaj te same nazwy parametrów, aby wartości z istniejącej instalacji zostały przeniesione.
Ważne jest, aby nie zmieniać nazw parametrów zadeklarowanych w kodzie w ogóle. Migracja rozszerzenia automatycznie zachowa istniejącą wartość parametru użytkownika, ale tylko wtedy, gdy nazwy pozostaną bez zmian.
Przykład: Stream Firestore to BigQuery
Przed. Parametr zadeklarowany w extension.yaml, odczytywany jako surowa zmienna środowiskowa w config.ts:
# extension.yaml
- param: COLLECTION_PATH
label: Collection path
type: string
required: true
// functions/src/config.ts
collectionPath: process.env.COLLECTION_PATH,
Po. Jeden defineString; interfejs CLI wykrywa go i odczytuje z .env:
// src/config.ts
import { defineString } from "firebase-functions/params";
collectionPath: defineString("COLLECTION_PATH", {
label: "Collection path",
// We now support "nonEmpty: true" to ensure a value other than the empty string
// is entered, analagous to "required: true" in extensions.yaml
input: { text: { nonEmpty: true} }
}),
Nazwa parametru nie została zmieniona, więc istniejący plik .env nadal działa.
Konwertowanie obiektów tajnych
W extension.yaml deklarujesz obiekty tajne za pomocą type: secret. Środowisko wykonawcze rozszerzeń przechowuje je i wiąże, dzięki czemu kod rozszerzenia może bezpośrednio odczytywać process.env.PARAM_NAME. W typowej bazie kodu Cloud Functions deklarujesz i wiżążesz każdy obiekt tajny osobno:
import { defineSecret } from "firebase-functions/params";
import { onRequest } from "firebase-functions/https";
const apiKey = defineSecret("API_KEY");
export const fn = onRequest({ secrets: [apiKey] }, handler);
Gdy rozszerzenie zostanie przeniesione do pakietu/zestawu npm, odwołania do obiektów tajnych będą zarządzane w pliku .env użytkownika. Ważne jest, aby nie
zmieniać nazw obiektów tajnych zadeklarowanych w kodzie w ogóle. Podczas migracji obiekty tajne użytkownika zostaną odpowiednio przeniesione.
Przykład: Trigger Email From Cloud Firestore
Przed. MAIL_COLLECTION i SMTP_PASSWORD odczytywane jako surowe zmienne środowiskowe w config.ts:
# extension.yaml
- param: MAIL_COLLECTION
label: Email documents collection
type: string
default: mail
required: true
- param: SMTP_PASSWORD
label: SMTP password
type: secret
// functions/src/config.ts
mailCollection: process.env.MAIL_COLLECTION,
smtpPassword: process.env.SMTP_PASSWORD,
Po. Jeden defineString i jeden defineSecret; interfejs CLI wykrywa oba i odczytuje z .env:
import { defineString, defineSecret } from "firebase-functions/params";
import { onDocumentWritten } from "firebase-functions/firestore";
const mailCollection = defineString("MAIL_COLLECTION",{ label: "Email documents collection",
default: "mail"
});
const smtpPassword = defineSecret("SMTP_PASSWORD", { label: "SMTP password" });
export const processQueue = onDocumentWritten(
{ document: `${mailCollection}/{documentId}`, secrets: [smtpPassword] },
async (event) => {
const collection = mailCollection.value();
const password = smtpPassword.value();
// ...
}
);
Migrowanie wewnętrznych wywołań kolejki zadań
Niektóre rozszerzenia dodają zadania do własnych kolejek zadań z poziomu kodu funkcji za pomocą Firebase Admin SDK. Różni się to od odbierania wysłanego zadania (omówionego w sekcjach Uaktualnianie funkcji i Konwertowanie haków cyklu życia). W tym przypadku Twój kod jest producentem wywołującym queue.enqueue(...).
W poprzednich wersjach Admin SDK rozszerzenia musiały przekazywać własny identyfikator instancji rozszerzenia jako drugi parametr, aby kierować do funkcji kolejki zadań w tym samym rozszerzeniu. Od wersji `firebase-admin` 14.2.0 nie jest to wymagane ani zalecane. Interfejs Task Queue API będzie teraz domyślnie kierować do kolejek zadań w tym samym kontekście (np. rozszerzeniu). Zalecamy usunięcie tego parametru z kodu zarówno w przypadku rozszerzenia, jak i funkcji autonomicznych. Usunięcie tego parametru zapewnia przenośność i zgodność z przyszłymi wersjami.
Wszystko inne dotyczące wywołania enqueue – ścieżka zasobu locations/region/functions/name, ładunek zadania i logika ponawiania – pozostaje bez zmian.
Więcej informacji o dodawaniu funkcji do kolejki za pomocą Cloud Tasks znajdziesz w artykule /docs/functions/task-functions.
Przed. Rozszerzenie 1 generacji
import { getFunctions } from "firebase-admin/functions";
const queue = getFunctions().taskQueue(
`locations/${config.location}/functions/syncBigQuery`,
process.env.EXT_INSTANCE_ID, // extension instance ID, injected by the runtime
);
await queue.enqueue(taskData);
Po. Rozszerzenie 2 generacji
import { getFunctions } from "firebase-admin/functions";
import { region } from "firebase-functions/params";
const queue = getFunctions().taskQueue(
`locations/${region.value()}/functions/syncBigQuery`);
await queue.enqueue(taskData);
Jeśli wywołanie enqueue kieruje do bazy kodu z prefiksem, wykryta nazwa funkcji też ma prefiks (np. orders-syncBigQuery).
Deklarowanie wymaganych interfejsów API i ról uprawnień
Przenieś wymagania rozszerzenia dotyczące uprawnień i interfejsów API z extension.yaml do kodu:
import { requiresAPI, requiresRole } from "firebase-functions"
requiresAPI("bigquery.googleapis.com", "Needed to write changelog rows");
requiresRole("roles/bigquery.dataEditor");
requiresRole("roles/bigquery.user");
Dzięki deklaratywnemu zabezpieczeniu interfejs Firebase CLI tworzy lub aktualizuje zarządzane konto usługi środowiska wykonawczego dla bazy kodu i przyznaje mu sumę wszystkich zadeklarowanych ról. Poinformuj użytkowników, że wszystkie funkcje w bazie kodu działają z tymi rolami, chyba że ostateczny interfejs API obsługuje węższy model.
Przykład: Stream Firestore to BigQuery
Przed. Zadeklarowane w extension.yaml; środowisko wykonawcze rozszerzeń włączyło interfejs API i przyznało role zarządzanemu kontu:
apis:
- apiName: bigquery.googleapis.com
roles:
- role: bigquery.dataEditor
- role: datastore.user
- role: bigquery.user
Po. Zadeklarowane w kodzie za pomocą requiresAPI i requiresRole:
import { requiresAPI, requiresRole } from "firebase-functions/";
requiresAPI("bigquery.googleapis.com",
"Needed to write changelog rows and views");
requiresRole("roles/biguqery.dataEditor");
requiresRole("roles/datastore.user");
requiresRole("roles/bigquery.user");
Konwertowanie haków cyklu życia
Jeśli Twoje rozszerzenie wywołuje getExtensions().runtime(), np.
setProcessingState lub setFatalError, usuń te wywołania, ponieważ w przypadku wywołania z normalnie wdrożonej funkcji 2 generacji spowodują one
błąd. Stan cyklu życia jest teraz określany przez afterFirstDeploy i afterRedeploy, gdzie to śledzenie stanu nie jest używane.
Firebase Extensions może uruchamiać konfigurację, gdy użytkownik instaluje, aktualizuje lub ponownie konfiguruje rozszerzenie. W pakiecie npm zadeklaruj w kodzie równoważne działania cyklu życia.
W przypadku konfiguracji jednorazowej:
import { afterFirstDeploy } from "firebase-functions/lifecycle";
import { onTaskDispatched } from "firebase-functions/tasks";
export const runInitialSetup = onTaskDispatched(async (request) => {
await initializeResources(request.data);
});
afterFirstDeploy({
task: {
function: "runInitialSetup",
body: {}
}
});
W przypadku aktualizacji konfiguracji lub kodu:
import { afterRedeploy } from "firebase-functions/lifecycle";
afterRedeploy({
task: {
function: "runInitialSetup",
body: { reconcile: true }
}
});
Spraw, aby działania cyklu życia były idempotentne. Użytkownicy mogą ręcznie uruchomić je ponownie, jeśli wysyłka lub wykonanie się nie powiedzie:
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME
firebase functions:lifecycle:run afterRedeploy CODEBASE_NAME
Przykład: Stream Firestore to BigQuery
Przed. lifecycleEvents w extension.yaml, sterowane przez środowisko wykonawcze rozszerzeń:
lifecycleEvents:
onInstall:
function: initBigQuerySync
processingMessage: Configuring BigQuery Sync.
onUpdate:
function: setupBigQuerySync
processingMessage: Configuring BigQuery Sync
onConfigure:
function: setupBigQuerySync
processingMessage: Configuring BigQuery Sync
Po. Zadeklarowane w kodzie; zadanie udostępnia BigQuery podczas pierwszego wdrożenia:
import { afterFirstDeploy, afterRedeploy } from "firebase-functions/lifecycle";
afterFirstDeploy({ task: { function: "initBigQuerySync" } });
afterRedeploy({ task: { function: "setupBigQuerySync" } });
Udostępnianie jest idempotentne, więc ponowne uruchomienie powoduje uzgodnienie zbioru danych, tabeli i widoków. Użytkownicy mogą ręcznie uruchomić ponownie za pomocą firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME.
Dokumentowanie konfiguracji dla użytkowników
Napisz plik
READMEpakietu, który zawiera co najmniej te informacje:Wartości
.envwymagane przez pakiet.Obiekty tajne wymagane przez pakiet oraz sposób przenoszenia istniejących wartości obiektów tajnych.
Role uprawnień zadeklarowane przez pakiet za pomocą
requiresRole(...).Interfejsy API Google, które pakiet włącza lub wymaga.
Haki cyklu życia zadeklarowane przez pakiet oraz sposób ich ręcznego ponownego uruchamiania.
Informacje o rozliczeniach.
Co się zmieniło w porównaniu z oryginalnym rozszerzeniem.
Przykład: Stream Firestore to BigQuery
Plik README pakietu zawiera konkretną tabelę „Co się zmieniło”:
| Potencjalny problem | Jako rozszerzenie | Jako @firebase/firestore-bigquery-export |
|---|---|---|
| Konfiguracja | Parametry rozszerzenia | Parametry funkcji za pomocą .env |
| Uprawnienia | Przyznane przez rozszerzenia | requiresRole(...), zastosowane podczas wdrażania |
| Udostępniam | Zadanie cyklu życia przez rozszerzenia | Zadanie afterFirstDeploy / afterRedeploy |
| Nazwy funkcji | ext-instanceId-fsexportbigquery | fsexportbigquery (opcjonalnie z prefiksem) |
Testowanie funkcji 2 generacji
Powinna być teraz dostępna funkcja 2 generacji, która po wdrożeniu będzie działać identycznie jak nowa instalacja rozszerzenia. Ostatnim krokiem jest sprawdzenie i naprawienie wszelkich problemów, które mogły się pojawić po drodze.
Upewnij się, że używasz
firebase-tools
>= 15.24.0, i wdróż przekonwertowaną funkcję 2 generacji w projekcie testowym
z odpowiednimi zasobami, aby przetestować jej działanie. Jeśli masz już skonfigurowany projekt testowy do testowania rozszerzenia, użyj tego polecenia:
firebase deploy --only functions
Po wpisaniu tego polecenia wypełnij wyświetlony kreator, podając wartości parametrów w taki sam sposób, jak w przypadku wypełniania formularza instalacji w konsoli Firebase.
Przykład: Stream Firestore to BigQuery
Sprawdzamy synchronizację Cloud Firestore z BigQuery od początku do końca:
- W konsoli Cloud Firestore utwórz zbiór, który został ustawiony jako COLLECTION_PATH (użytkownicy), jeśli jeszcze nie istnieje.
- Utwórz dokument o nazwie bigquery-mirror-test zawierający dowolne pola z dowolnymi wartościami.
- W konsoli BigQuery utwórz zapytanie do tabeli surowego dziennika zmian. Powinien on zawierać jeden wiersz rejestrujący utworzenie dokumentu:
SELECT * FROM `PROJECT_ID.analytics.users_raw_changelog`
- Utwórz zapytanie do najnowszego widoku, który powinien zwrócić najnowsze zdarzenie zmiany dla
jedynego obecnego dokumentu:
bigquery-mirror-test
SELECT * FROM `PROJECT_ID.analytics.users_raw_latest`
- Usuń dokument
bigquery-mirror-testw Cloud Firestore. Zniknie on z najnowszego widoku, a do tabeli surowego dziennika zmian zostanie dodane zdarzenieDELETE.
Pełną historię pojedynczego dokumentu możesz sprawdzić za pomocą tego polecenia:
SELECT *
FROM `PROJECT_ID.analytics.users_raw_changelog`
WHERE document_name = "bigquery-mirror-test"
ORDER BY timestamp ASC
Różnice w porównaniu z testowaniem rozszerzenia:
- Wyzwalacz wdraża się jako
fsexportbigquery(opcjonalnie z prefiksem bazy kodu), a nieext-<instanceId>-fsexportbigquery. Poszukaj tej nazwy w Cloud Functions panelu i logach. - Twój kod będzie teraz działać w Firebase Local Emulator Suite jako zwykłe
funkcje. Wartości parametrów do użycia w emulatorze możesz ustawić za pomocą
.env.local. Możesz też testować jednostkowo kod za pomocą pakietu firebase-functions-test SDK zgodnie z opisem w artykule Testowanie jednostkowe Cloud Functions - Udostępnianie nie jest już sterowane przez środowisko wykonawcze rozszerzeń. Jeśli po wdrożeniu brakuje tabeli dziennika zmian, ręcznie uruchom ponownie zadanie konfiguracji:
firebase functions:lifecycle:run afterFirstDeploy CODEBASE_NAME. Zadanie jest idempotentne, więc ponowne uruchomienie powoduje uzgodnienie zbioru danych, tabeli i widoków. - Wartości parametrów pochodzą z
.env, a nie z formularza instalacji, więc ponowne uruchomieniefirebase deployjest nieinteraktywne, gdy.envjest gotowy.