diff --git a/integration_test/functions/src/index.ts b/integration_test/functions/src/index.ts index 6865d7dd5..e51536295 100644 --- a/integration_test/functions/src/index.ts +++ b/integration_test/functions/src/index.ts @@ -1,41 +1,32 @@ import { PubSub } from '@google-cloud/pubsub'; import { Request, Response } from 'express'; +import fetch from 'node-fetch'; import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; import * as fs from 'fs'; -import * as https from 'https'; -export * from './pubsub-tests'; -export * from './database-tests'; -export * from './auth-tests'; -export * from './firestore-tests'; -export * from './https-tests'; -export * from './remoteConfig-tests'; -export * from './storage-tests'; -export * from './testLab-tests'; -const numTests = Object.keys(exports).length; // Assumption: every exported function is its own test. +import * as v1 from './v1/index'; +const numTests = Object.keys(v1).filter((k) => + ({}.hasOwnProperty.call(v1[k], '__endpoint')) +).length; +export { v1 }; -import * as utils from './test-utils'; -import * as testLab from './testLab-utils'; +import * as testLab from './v1/testLab-utils'; -import 'firebase-functions'; // temporary shim until process.env.FIREBASE_CONFIG available natively in GCF(BUG 63586213) -import { config } from 'firebase-functions'; const firebaseConfig = JSON.parse(process.env.FIREBASE_CONFIG); -admin.initializeApp(); const REGION = functions.config().functions.test_region; +admin.initializeApp(); -// TODO(klimt): Get rid of this once the JS client SDK supports callable triggers. -function callHttpsTrigger(name: string, data: any, baseUrl) { - return utils.makeRequest( +function callHttpsTrigger(name: string, data: any) { + return fetch( + `https://${REGION}-${firebaseConfig.projectId}.cloudfunctions.net/${name}`, { method: 'POST', - host: REGION + '-' + firebaseConfig.projectId + '.' + baseUrl, - path: '/' + name, headers: { 'Content-Type': 'application/json', }, - }, - JSON.stringify({ data }) + body: JSON.stringify({ data }), + } ); } @@ -43,41 +34,85 @@ async function callScheduleTrigger(functionName: string, region: string) { const accessToken = await admin.credential .applicationDefault() .getAccessToken(); - return new Promise((resolve, reject) => { - const request = https.request( - { - method: 'POST', - host: 'cloudscheduler.googleapis.com', - path: `/v1/projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`, - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${accessToken.access_token}`, - }, + const response = await fetch( + `https://cloudscheduler.googleapis.com/v1/projects/${firebaseConfig.projectId}/locations/us-central1/jobs/firebase-schedule-${functionName}-${region}:run`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken.access_token}`, }, - (response) => { - if (response.statusCode! / 100 != 2) { - reject( - new Error('Failed request with status ' + response.statusCode!) - ); - return; - } - let body = ''; - response.on('data', (chunk) => { - body += chunk; - }); - response.on('end', () => { - console.log(`Successfully scheduled function ${functionName}`); - resolve(body); - }); - } - ); - request.on('error', (err) => { - console.error('Failed to schedule cloud scheduler job with error', err); - reject(err); - }); - request.write('{}'); - request.end(); - }); + } + ); + if (!response.ok) { + throw new Error(`Failed request with status ${response.status}!`); + } + const data = await response.text(); + functions.logger.log(`Successfully scheduled function ${functionName}`, data); + return; +} + +async function updateRemoteConfig( + testId: string, + accessToken: string +): Promise { + await fetch( + `https://firebaseremoteconfig.googleapis.com/v1/projects/${firebaseConfig.projectId}/remoteConfig`, + { + method: 'PUT', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json; UTF-8', + 'Accept-Encoding': 'gzip', + 'If-Match': '*', + }, + body: JSON.stringify({ version: { description: testId } }), + } + ); +} + +function v1Tests(testId: string, accessToken: string) { + return [ + // A database write to trigger the Firebase Realtime Database tests. + admin + .database() + .ref(`dbTests/${testId}/start`) + .set({ '.sv': 'timestamp' }), + // A Pub/Sub publish to trigger the Cloud Pub/Sub tests. + new PubSub() + .topic('pubsubTests') + .publish(Buffer.from(JSON.stringify({ testId }))), + // A user creation to trigger the Firebase Auth user creation tests. + admin + .auth() + .createUser({ + email: `${testId}@fake.com`, + password: 'secret', + displayName: `${testId}`, + }) + .then((userRecord) => { + // A user deletion to trigger the Firebase Auth user deletion tests. + admin.auth().deleteUser(userRecord.uid); + }), + // A firestore write to trigger the Cloud Firestore tests. + admin + .firestore() + .collection('tests') + .doc(testId) + .set({ test: testId }), + // Invoke a callable HTTPS trigger. + callHttpsTrigger('v1-callableTests', { foo: 'bar', testId }), + // A Remote Config update to trigger the Remote Config tests. + updateRemoteConfig(testId, accessToken), + // A storage upload to trigger the Storage tests + admin + .storage() + .bucket() + .upload('/tmp/' + testId + '.txt'), + testLab.startTestRun(firebaseConfig.projectId, testId), + // Invoke the schedule for our scheduled function to fire + callScheduleTrigger('v1-schedule', 'us-central1'), + ]; } export const integrationTests: any = functions @@ -86,12 +121,6 @@ export const integrationTests: any = functions timeoutSeconds: 540, }) .https.onRequest(async (req: Request, resp: Response) => { - // We take the base url for our https call (cloudfunctions.net, txckloud.net, etc) from the request - // so that it changes with the environment that the tests are run in - const baseUrl = req.hostname - .split('.') - .slice(1) - .join('.'); const testId = admin .database() .ref() @@ -101,71 +130,15 @@ export const integrationTests: any = functions .ref(`testRuns/${testId}/timestamp`) .set(Date.now()); const testIdRef = admin.database().ref(`testRuns/${testId}`); - console.log('testId is: ', testId); + functions.logger.info('testId is: ', testId); fs.writeFile('/tmp/' + testId + '.txt', 'test', () => {}); try { - await Promise.all([ - // A database write to trigger the Firebase Realtime Database tests. - admin - .database() - .ref(`dbTests/${testId}/start`) - .set({ '.sv': 'timestamp' }), - // A Pub/Sub publish to trigger the Cloud Pub/Sub tests. - new PubSub() - .topic('pubsubTests') - .publish(Buffer.from(JSON.stringify({ testId }))), - // A user creation to trigger the Firebase Auth user creation tests. - admin - .auth() - .createUser({ - email: `${testId}@fake.com`, - password: 'secret', - displayName: `${testId}`, - }) - .then((userRecord) => { - // A user deletion to trigger the Firebase Auth user deletion tests. - admin.auth().deleteUser(userRecord.uid); - }), - // A firestore write to trigger the Cloud Firestore tests. - admin - .firestore() - .collection('tests') - .doc(testId) - .set({ test: testId }), - // Invoke a callable HTTPS trigger. - callHttpsTrigger('callableTests', { foo: 'bar', testId }, baseUrl), - // A Remote Config update to trigger the Remote Config tests. - admin.credential - .applicationDefault() - .getAccessToken() - .then((accessToken) => { - const options = { - hostname: 'firebaseremoteconfig.googleapis.com', - path: `/v1/projects/${firebaseConfig.projectId}/remoteConfig`, - method: 'PUT', - headers: { - Authorization: 'Bearer ' + accessToken.access_token, - 'Content-Type': 'application/json; UTF-8', - 'Accept-Encoding': 'gzip', - 'If-Match': '*', - }, - }; - const request = https.request(options, (resp) => {}); - request.write(JSON.stringify({ version: { description: testId } })); - request.end(); - }), - // A storage upload to trigger the Storage tests - admin - .storage() - .bucket() - .upload('/tmp/' + testId + '.txt'), - testLab.startTestRun(firebaseConfig.projectId, testId), - // Invoke the schedule for our scheduled function to fire - callScheduleTrigger('schedule', 'us-central1'), - ]); - + const accessToken = await admin.credential + .applicationDefault() + .getAccessToken(); + await Promise.all([...v1Tests(testId, accessToken.access_token)]); // On test completion, check that all tests pass and reply "PASS", or provide further details. - console.log('Waiting for all tests to report they pass...'); + functions.logger.info('Waiting for all tests to report they pass...'); await new Promise((resolve, reject) => { setTimeout(() => reject(new Error('Timeout')), 5 * 60 * 1000); let testsExecuted = 0; @@ -179,7 +152,7 @@ export const integrationTests: any = functions ); return; } - console.log( + functions.logger.info( `${snapshot.key} passed (${testsExecuted} of ${numTests})` ); if (testsExecuted < numTests) { @@ -190,10 +163,10 @@ export const integrationTests: any = functions resolve(); }); }); - console.log('All tests pass!'); + functions.logger.info('All tests pass!'); resp.status(200).send('PASS \n'); } catch (err) { - console.log(`Some tests failed: ${err}`); + functions.logger.info(`Some tests failed: ${err}`, err); resp .status(500) .send( diff --git a/integration_test/functions/src/test-utils.ts b/integration_test/functions/src/test-utils.ts deleted file mode 100644 index eda799a67..000000000 --- a/integration_test/functions/src/test-utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as https from 'https'; - -/** - * Makes an http request asynchronously and returns the response data. - * - * This function wraps the callback-based `http.request()` function with a - * Promise. The returned Promise will be rejected in case the request fails with an - * error, or the response code is not in the 200-299 range. - * - * @param options Request options for the request. - * @param body Optional body to send as part of the request. - * @returns Promise returning the response data as string. - */ -export function makeRequest( - options: https.RequestOptions, - body?: string -): Promise { - return new Promise((resolve, reject) => { - const request = https.request(options, (response) => { - let body = ''; - response.on('data', (chunk) => { - body += chunk; - }); - response.on('end', () => { - if (response.statusCode < 200 || response.statusCode > 299) { - reject(body); - return; - } - resolve(body); - }); - }); - if (body) { - request.write(body); - } - request.on('error', reject); - request.end(); - }); -} diff --git a/integration_test/functions/src/testing.ts b/integration_test/functions/src/testing.ts index 1cb9f7819..1054c7f01 100644 --- a/integration_test/functions/src/testing.ts +++ b/integration_test/functions/src/testing.ts @@ -1,7 +1,7 @@ import * as firebase from 'firebase-admin'; -import { EventContext } from 'firebase-functions'; +import * as functions from 'firebase-functions'; -export type TestCase = (data: T, context?: EventContext) => any; +export type TestCase = (data: T, context?: functions.EventContext) => any; export interface TestCaseMap { [key: string]: TestCase; } @@ -20,7 +20,7 @@ export class TestSuite { return this; } - run(testId: string, data: T, context?: EventContext): Promise { + run(testId: string, data: T, context?: functions.EventContext): Promise { const running: Array> = []; for (const testName in this.tests) { if (!this.tests.hasOwnProperty(testName)) { @@ -30,7 +30,7 @@ export class TestSuite { .then(() => this.tests[testName](data, context)) .then( (result) => { - console.log( + functions.logger.info( `${result ? 'Passed' : 'Failed with successful op'}: ${testName}` ); return { name: testName, passed: !!result }; @@ -47,7 +47,7 @@ export class TestSuite { results.forEach((val) => (sum = sum + val.passed)); const summary = `passed ${sum} of ${running.length}`; const passed = sum === running.length; - console.log(summary); + functions.logger.info(summary); const result = { passed, summary, tests: results }; return firebase .database() diff --git a/integration_test/functions/src/auth-tests.ts b/integration_test/functions/src/v1/auth-tests.ts similarity index 94% rename from integration_test/functions/src/auth-tests.ts rename to integration_test/functions/src/v1/auth-tests.ts index c97c907bc..c82166baf 100644 --- a/integration_test/functions/src/auth-tests.ts +++ b/integration_test/functions/src/v1/auth-tests.ts @@ -1,6 +1,6 @@ import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; -import { expectEq, TestSuite } from './testing'; +import { expectEq, TestSuite } from '../testing'; import UserMetadata = admin.auth.UserRecord; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; @@ -10,7 +10,7 @@ export const createUserTests: any = functions .auth.user() .onCreate((u, c) => { const testId: string = u.displayName; - console.log(`testId is ${testId}`); + functions.logger.info(`testId is ${testId}`); return new TestSuite('auth user onCreate') .it('should have a project as resource', (user, context) => @@ -50,7 +50,7 @@ export const deleteUserTests: any = functions .auth.user() .onDelete((u, c) => { const testId: string = u.displayName; - console.log(`testId is ${testId}`); + functions.logger.info(`testId is ${testId}`); return new TestSuite('auth user onDelete') .it('should have a project as resource', (user, context) => diff --git a/integration_test/functions/src/database-tests.ts b/integration_test/functions/src/v1/database-tests.ts similarity index 85% rename from integration_test/functions/src/database-tests.ts rename to integration_test/functions/src/v1/database-tests.ts index 568d26bf4..040c6786c 100644 --- a/integration_test/functions/src/database-tests.ts +++ b/integration_test/functions/src/v1/database-tests.ts @@ -1,6 +1,6 @@ import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; -import { expectEq, expectMatches, TestSuite } from './testing'; +import { expectEq, expectMatches, TestSuite } from '../testing'; import DataSnapshot = admin.database.DataSnapshot; const testIdFieldName = 'testId'; @@ -11,7 +11,7 @@ export const databaseTests: any = functions .database.ref('dbTests/{testId}/start') .onWrite((ch, ctx) => { if (ch.after.val() === null) { - console.log( + functions.logger.info( 'Event for ' + ctx.params[testIdFieldName] + ' is null; presuming data cleanup, so skipping.' @@ -40,7 +40,7 @@ export const databaseTests: any = functions return expectMatches( url, new RegExp( - `^https://${process.env.GCLOUD_PROJECT}.firebaseio.com/dbTests` + `^https://${process.env.GCLOUD_PROJECT}(-default-rtdb)*.firebaseio.com/dbTests` ) ); }) @@ -50,9 +50,11 @@ export const databaseTests: any = functions }) .it('should have refs resources', (change, context) => - expectEq( + expectMatches( context.resource.name, - `projects/_/instances/${process.env.GCLOUD_PROJECT}/refs/dbTests/${context.params.testId}/start` + new RegExp( + `^projects/_/instances/${process.env.GCLOUD_PROJECT}(-default-rtdb)*/refs/dbTests/${context.params.testId}/start$` + ) ) ) diff --git a/integration_test/functions/src/firestore-tests.ts b/integration_test/functions/src/v1/firestore-tests.ts similarity index 95% rename from integration_test/functions/src/firestore-tests.ts rename to integration_test/functions/src/v1/firestore-tests.ts index 97daadea5..9916e2d3c 100644 --- a/integration_test/functions/src/firestore-tests.ts +++ b/integration_test/functions/src/v1/firestore-tests.ts @@ -1,6 +1,6 @@ import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; -import { expectDeepEq, expectEq, TestSuite } from './testing'; +import { expectDeepEq, expectEq, TestSuite } from '../testing'; import DocumentSnapshot = admin.firestore.DocumentSnapshot; const testIdFieldName = 'documentId'; diff --git a/integration_test/functions/src/https-tests.ts b/integration_test/functions/src/v1/https-tests.ts similarity index 88% rename from integration_test/functions/src/https-tests.ts rename to integration_test/functions/src/v1/https-tests.ts index 18972bd47..103228d7d 100644 --- a/integration_test/functions/src/https-tests.ts +++ b/integration_test/functions/src/v1/https-tests.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions'; -import { expectEq, TestSuite } from './testing'; +import { expectEq, TestSuite } from '../testing'; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; diff --git a/integration_test/functions/src/v1/index.ts b/integration_test/functions/src/v1/index.ts new file mode 100644 index 000000000..6e6717b15 --- /dev/null +++ b/integration_test/functions/src/v1/index.ts @@ -0,0 +1,8 @@ +export * from './pubsub-tests'; +export * from './database-tests'; +export * from './auth-tests'; +export * from './firestore-tests'; +export * from './https-tests'; +export * from './remoteConfig-tests'; +export * from './storage-tests'; +export * from './testLab-tests'; diff --git a/integration_test/functions/src/pubsub-tests.ts b/integration_test/functions/src/v1/pubsub-tests.ts similarity index 97% rename from integration_test/functions/src/pubsub-tests.ts rename to integration_test/functions/src/v1/pubsub-tests.ts index a21c2011a..79f5889c6 100644 --- a/integration_test/functions/src/pubsub-tests.ts +++ b/integration_test/functions/src/v1/pubsub-tests.ts @@ -1,6 +1,6 @@ import * as admin from 'firebase-admin'; import * as functions from 'firebase-functions'; -import { evaluate, expectEq, success, TestSuite } from './testing'; +import { evaluate, expectEq, success, TestSuite } from '../testing'; import PubsubMessage = functions.pubsub.Message; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; diff --git a/integration_test/functions/src/remoteConfig-tests.ts b/integration_test/functions/src/v1/remoteConfig-tests.ts similarity index 95% rename from integration_test/functions/src/remoteConfig-tests.ts rename to integration_test/functions/src/v1/remoteConfig-tests.ts index 3f2cc8993..a9dbd10d7 100644 --- a/integration_test/functions/src/remoteConfig-tests.ts +++ b/integration_test/functions/src/v1/remoteConfig-tests.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions'; -import { expectEq, TestSuite } from './testing'; +import { expectEq, TestSuite } from '../testing'; import TemplateVersion = functions.remoteConfig.TemplateVersion; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; diff --git a/integration_test/functions/src/storage-tests.ts b/integration_test/functions/src/v1/storage-tests.ts similarity index 94% rename from integration_test/functions/src/storage-tests.ts rename to integration_test/functions/src/v1/storage-tests.ts index df3032f78..4259c5a00 100644 --- a/integration_test/functions/src/storage-tests.ts +++ b/integration_test/functions/src/v1/storage-tests.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions'; -import { expectEq, TestSuite } from './testing'; +import { expectEq, TestSuite } from '../testing'; import ObjectMetadata = functions.storage.ObjectMetadata; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; diff --git a/integration_test/functions/src/testLab-tests.ts b/integration_test/functions/src/v1/testLab-tests.ts similarity index 86% rename from integration_test/functions/src/testLab-tests.ts rename to integration_test/functions/src/v1/testLab-tests.ts index 8e064928a..805b988c8 100644 --- a/integration_test/functions/src/testLab-tests.ts +++ b/integration_test/functions/src/v1/testLab-tests.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions'; -import { expectEq, TestSuite } from './testing'; +import { expectEq, TestSuite } from '../testing'; import TestMatrix = functions.testLab.TestMatrix; const REGION = process.env.FIREBASE_FUNCTIONS_TEST_REGION || 'us-central1'; @@ -13,8 +13,6 @@ export const testLabTests: any = functions return new TestSuite('test matrix complete') .it('should have eventId', (snap, context) => context.eventId) - .it('should have timestamp', (snap, context) => context.timestamp) - .it('should have right eventType', (_, context) => expectEq(context.eventType, 'google.testing.testMatrix.complete') ) diff --git a/integration_test/functions/src/testLab-utils.ts b/integration_test/functions/src/v1/testLab-utils.ts similarity index 72% rename from integration_test/functions/src/testLab-utils.ts rename to integration_test/functions/src/v1/testLab-utils.ts index 3dbb7b763..8f0424d98 100644 --- a/integration_test/functions/src/testLab-utils.ts +++ b/integration_test/functions/src/v1/testLab-utils.ts @@ -1,7 +1,5 @@ +import fetch from 'node-fetch'; import * as admin from 'firebase-admin'; -import * as http from 'http'; -import * as https from 'https'; -import * as utils from './test-utils'; interface AndroidDevice { androidModelId: string; @@ -30,10 +28,16 @@ export async function startTestRun(projectId: string, testId: string) { async function fetchDefaultDevice( accessToken: admin.GoogleOAuthAccessToken ): Promise { - const response = await utils.makeRequest( - requestOptions(accessToken, 'GET', '/v1/testEnvironmentCatalog/ANDROID') + const response = await fetch( + `https://${TESTING_API_SERVICE_NAME}/v1/testEnvironmentCatalog/ANDROID`, + { + headers: { + Authorization: 'Bearer ' + accessToken.access_token, + 'Content-Type': 'application/json', + }, + } ); - const data = JSON.parse(response); + const data = await response.json(); const models = data?.androidDeviceCatalog?.models || []; const defaultModels = models.filter( (m) => @@ -58,17 +62,12 @@ async function fetchDefaultDevice( } as AndroidDevice; } -function createTestMatrix( +async function createTestMatrix( accessToken: admin.GoogleOAuthAccessToken, projectId: string, testId: string, device: AndroidDevice -): Promise { - const options = requestOptions( - accessToken, - 'POST', - '/v1/projects/' + projectId + '/testMatrices' - ); +): Promise { const body = { projectId, testSpecification: { @@ -96,21 +95,16 @@ function createTestMatrix( }, }, }; - return utils.makeRequest(options, JSON.stringify(body)); -} - -function requestOptions( - accessToken: admin.GoogleOAuthAccessToken, - method: string, - path: string -): https.RequestOptions { - return { - method, - hostname: TESTING_API_SERVICE_NAME, - path, - headers: { - Authorization: 'Bearer ' + accessToken.access_token, - 'Content-Type': 'application/json', - }, - }; + await fetch( + `https://${TESTING_API_SERVICE_NAME}/v1/projects/${projectId}/testMatrices`, + { + method: 'POST', + headers: { + Authorization: 'Bearer ' + accessToken.access_token, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ); + return; } diff --git a/integration_test/package.json.template b/integration_test/package.json.template index a5259aeb6..07585cbe2 100644 --- a/integration_test/package.json.template +++ b/integration_test/package.json.template @@ -8,10 +8,11 @@ "@google-cloud/pubsub": "^2.10.0", "firebase-admin": "__FIREBASE_ADMIN__", "firebase-functions": "__SDK_TARBALL__", - "lodash": "~4.17.2" + "node-fetch": "^2.6.7" }, "main": "lib/index.js", "devDependencies": { + "@types/node-fetch": "^2.6.1", "typescript": "~4.2.2" }, "engines": { diff --git a/integration_test/run_tests.sh b/integration_test/run_tests.sh index 498a9b0a0..b78150cc2 100755 --- a/integration_test/run_tests.sh +++ b/integration_test/run_tests.sh @@ -85,9 +85,9 @@ function delete_all_functions { # Try to delete, if there are errors it is because the project is already empty, # in that case do nothing. if [[ "${TOKEN}" == "" ]]; then - firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests testLabTests --force --project=$PROJECT_ID || : & + firebase functions:delete integrationTests v1 --force --project=$PROJECT_ID || : & else - firebase functions:delete callableTests createUserTests databaseTests deleteUserTests firestoreTests integrationTests pubsubTests remoteConfigTests testLabTests --force --project=$PROJECT_ID --token=$TOKEN || : & + firebase functions:delete integrationTests v1 --force --project=$PROJECT_ID --token=$TOKEN || : & fi wait announce "Project emptied." @@ -139,13 +139,8 @@ build_sdk delete_all_functions set_region -for version in 10 12 14 16; do - if [[ "$version" -eq 10 ]]; then - admin_sdk="^9.12.0" - else - admin_sdk="^10.0.0" - fi - create_package_json $TIMESTAMP $version $admin_sdk +for version in 12 14 16; do + create_package_json $TIMESTAMP $version "^10.0.0" install_deps announce "Re-deploying the same functions to Node $version runtime ..." deploy