From 10ed932c08ecee472748ac885ad84e0b46d33b63 Mon Sep 17 00:00:00 2001 From: tinayuangao Date: Sat, 25 Mar 2017 08:40:25 +0800 Subject: [PATCH 1/7] Add approve functions and use typescript. --- functions/data.ts | 22 ++++++ functions/data_image.ts | 36 ++++++++++ functions/github.ts | 32 +++++++++ functions/image_data.ts | 37 ++++++++++ functions/index.ts | 120 ++++++++++++++++++++++++++++++++ functions/jwt_util.ts | 37 ++++++++++ functions/package.json | 4 +- functions/test_goldens.ts | 36 ++++++++++ functions/tsconfig.json | 31 +++++++++ functions/util/github.ts | 37 ++++++++++ functions/util/jwt.ts | 20 ++++++ tools/gulp/tasks/screenshots.ts | 66 ++++++------------ tools/gulp/util/firebase.ts | 16 ----- 13 files changed, 431 insertions(+), 63 deletions(-) create mode 100644 functions/data.ts create mode 100644 functions/data_image.ts create mode 100644 functions/github.ts create mode 100644 functions/image_data.ts create mode 100644 functions/index.ts create mode 100644 functions/jwt_util.ts create mode 100644 functions/test_goldens.ts create mode 100644 functions/tsconfig.json create mode 100644 functions/util/github.ts create mode 100644 functions/util/jwt.ts diff --git a/functions/data.ts b/functions/data.ts new file mode 100644 index 000000000000..d06fbee95210 --- /dev/null +++ b/functions/data.ts @@ -0,0 +1,22 @@ +import * as firebaseAdmin from 'firebase-admin'; +import {verifySecureTokenAndExecute} from './jwt_util'; + +/** + * Handle data written to temporary folder. Validate the JWT and move the data out of + * temporary folder if the token is valid. + * Move the data to 'screenshot/reports/$prNumber/$path + */ +export function verifyJWTAndUpdateData(event: any, path: string) { + // Only edit data when it is first created. Exit when the data is deleted. + if (event.data.previous.exists() || !event.data.exists()) { + return; + } + + let prNumber = event.params.prNumber; + let data = event.data.val(); + + return verifySecureTokenAndExecute(event).then(() => { + return firebaseAdmin.database().ref().child('screenshot/reports') + .child(prNumber).child(path).set(data); + }); +}; diff --git a/functions/data_image.ts b/functions/data_image.ts new file mode 100644 index 000000000000..61f130443a91 --- /dev/null +++ b/functions/data_image.ts @@ -0,0 +1,36 @@ +import * as firebaseFunctions from 'firebase-functions'; +import {writeFileSync} from 'fs'; +import {verifySecureTokenAndExecute} from './jwt_util'; + +const gcs = require('@google-cloud/storage')(); + +/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */ +const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); + +/** + * Convert data to images. Image data posted to database will be saved as png files + * and upload to screenshot/$prNumber/dataType/$filename + */ +export function convertTestImageDataToFiles(event: any) { + // Only edit data when it is first created. Exit when the data is deleted. + if (event.data.previous.exists() || !event.data.exists()) { + return; + } + + let dataType = event.params.dataType; + let prNumber = event.params.prNumber; + let data = event.data.val(); + let saveFilename = `${event.params.filename}.screenshot.png`; + + if (dataType != 'diff' && dataType != 'test') { + return; + } + + return verifySecureTokenAndExecute(event).then(() => { + let tempPath = `/tmp/${dataType}-${saveFilename}`; + let filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`; + let binaryData = new Buffer(data, 'base64').toString('binary'); + writeFileSync(tempPath, binaryData, 'binary'); + return bucket.upload(tempPath, {destination: filePath}); + }); +}; diff --git a/functions/github.ts b/functions/github.ts new file mode 100644 index 000000000000..5aa890e3e5a2 --- /dev/null +++ b/functions/github.ts @@ -0,0 +1,32 @@ +import * as firebaseAdmin from 'firebase-admin'; +import * as firebaseFunctions from 'firebase-functions'; +import {setGithubStatus} from './util/github'; + +/** Github status update token */ +const token = firebaseFunctions.config().secret.github; + +/** The repo slug. This is used to validate the JWT is sent from correct repo. */ +const repoSlug = firebaseFunctions.config().repo.slug; + +/** Domain to view the screenshots */ +const authDomain = firebaseFunctions.config().firebase.authDomain; + +/** The same of this screenshot testing tool */ +const toolName = firebaseFunctions.config().tool.name; + +export function updateGithubStatus(event: firebaseFunctions.Event) { + if (!event.data.exists() || typeof event.data.val() != 'boolean') { + return; + } + let result = event.data.val() == true; + let prNumber = event.params.prNumber; + return event.data.ref.parent.child('sha').once('value').then((sha: firebaseAdmin.database.DataSnapshot) => { + return setGithubStatus(sha.val(), + result, + toolName, + `${toolName} ${result ? 'passed' : 'failed'}`, + `http://${authDomain}/${prNumber}`, + repoSlug, + token); + }); +} diff --git a/functions/image_data.ts b/functions/image_data.ts new file mode 100644 index 000000000000..c205b3a25fd7 --- /dev/null +++ b/functions/image_data.ts @@ -0,0 +1,37 @@ +import * as firebaseAdmin from 'firebase-admin'; +import * as path from 'path'; +import {readFileSync} from 'fs'; + +const gcs = require('@google-cloud/storage')(); + +const FIREBASE_DATA_GOLDENS = 'screenshot/goldens'; + +/** + * Read golden files under /goldens/ and store the image data to + * database /screenshot/goldens/$filename + */ +export function convertGoldenImagesToData(name: string, resourceState: string, fileBucket: any) { + // The name should always look like "goldens/xxx.png" + let parsedPath = path.parse(name); + // Get the file name. + if (parsedPath.root != '' || parsedPath.dir != 'goldens' || parsedPath.ext != '.png') { + return; + } + + let filenameKey = path.basename(parsedPath.name, '.screenshot'); + let databaseRef = firebaseAdmin.database().ref(FIREBASE_DATA_GOLDENS).child(filenameKey); + + // When a gold image is deleted, also delete the corresponding record in the firebase database. + if (resourceState === 'not_exists') { + return databaseRef.set(null); + } + + let tempFilePath = `/tmp/${parsedPath.base}`; + let bucket = gcs.bucket(fileBucket); + // Download file from bucket. + return bucket.file(name).download({destination: tempFilePath}) + .then(() => { + let data = readFileSync(tempFilePath); + return databaseRef.set(data); + }).catch((error: any) => console.error(`${filenameKey} ${error}`)); +}; diff --git a/functions/index.ts b/functions/index.ts new file mode 100644 index 000000000000..a328b6131423 --- /dev/null +++ b/functions/index.ts @@ -0,0 +1,120 @@ +'use strict'; + +import * as firebaseFunctions from 'firebase-functions'; +import * as firebaseAdmin from 'firebase-admin'; + +import {verifyJWTAndUpdateData} from './data'; +import {convertGoldenImagesToData} from './image_data'; +import {convertTestImageDataToFiles} from './data_image'; +import {copyTestImagesToGoldens} from './test_goldens'; +import {updateGithubStatus} from './github'; + +/** + * Usage: Firebase functions only accept javascript file index.js + * tsc + * firebase deploy --only functions + * + * + * Data and images handling for Screenshot test. + * + * All users can post data to temporary folder. These Functions will check the data with JsonWebToken and + * move the valid data out of temporary folder. + * + * For valid data posted to database /$temp/screenshot/reports/$prNumber/$secureToken, move it to + * /screenshot/reports/$prNumber. + * These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job information + * + * For valid image results written to database /$temp/screenshot/images/$prNumber/$secureToken/, save the image + * data to image files and upload to google cloud storage under location /screenshots/$prNumber + * These are screenshot test result images, and difference images generated from screenshot comparison. + * + * For golden images uploaded to /goldens, read the data from images files and write the data to Firebase database + * under location /screenshot/goldens + * Screenshot tests can only read restricted database data with no credentials, and they cannot access + * Google Cloud Storage. Therefore we copy the image data to database to make it available to screenshot tests. + * + * The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to database/storage. + * All invalid data will be removed. + * The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path. + */ + +// Initailize the admin app +firebaseAdmin.initializeApp(firebaseFunctions.config().firebase); + +/** The valid data types database accepts */ +const dataTypes = ['result', 'sha', 'travis']; + +/** The Json Web Token format. The token is stored in data path. */ +const jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}'; + +/** The temporary folder name for screenshot data that needs to be validated via JWT. */ +const tempFolder = '/untrustedInbox'; + + +/** Untrusted report data for a PR */ +const reportPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/`; +/** Untrusted image data for a PR */ +const imagePath = `${tempFolder}/screenshot/images/{prNumber}/${jwtFormat}/`; +/** Trusted report data for a PR */ +const trustedReportPath = `screenshot/reports/{prNumber}`; + +/** + * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ + * to /screenshot/reports/$prNumber + * Data copied: filenames(image results names), commit(github PR info), + * sha (github PR info), result (true or false for all the tests), travis job number + */ +const testDataPath = `${reportPath}/{dataType}`; +exports.testData = firebaseFunctions.database.ref(testDataPath) + .onWrite((event: any) => { + const dataType = event.params.dataType; + if (dataTypes.includes(dataType)) { + return verifyJWTAndUpdateData(event, dataType); + } +}); + +/** + * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ + * to /screenshot/reports/$prNumber + * Data copied: test result for each file/test with ${filename}. The value should be true or false. + */ +const testResultsPath = `${reportPath}/results/{filename}`; +exports.testResults = firebaseFunctions.database.ref(testResultsPath) + .onWrite((event: any) => { + return verifyJWTAndUpdateData(event, `results/${event.params.filename}`); +}); + +/** + * Copy valid data from database /$temp/screenshot/images/$prNumber/$secureToken/ + * to storage /screenshots/$prNumber + * Data copied: test result images. Convert from data to image files in storage. + */ +const imageDataToFilePath = `${imagePath}/{dataType}/{filename}`; +exports.imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath) + .onWrite(convertTestImageDataToFiles); + +/** + * Copy valid goldens from storage /goldens/ to database /screenshot/goldens/ + * so we can read the goldens without credentials. + */ +exports.goldenImageToData = firebaseFunctions.storage.bucket( + firebaseFunctions.config().firebase.storageBucket).object().onChange((event: any) => { + return convertGoldenImagesToData(event.data.name, event.data.resourceState, event.data.bucket); +}); + +/** + * Copy test result images for PR to Goldens. + * Copy images from /screenshot/$prNumber/test/ to /goldens/ + */ +const approveImagesPath = `${trustedReportPath}/approved`; +exports.approveImages = firebaseFunctions.database.ref(approveImagesPath).onWrite((event: any) => { + return copyTestImagesToGoldens(event.params.prNumber); +}); + +/** + * Update github status. When the result is true, update github commit status to `success`, + * otherwise update github status to `failure`. + * The Github Status Token is set in config.secret.github + */ +const githubStatusPath = `${trustedReportPath}/result`; +exports.githubStatus = firebaseFunctions.database.ref(githubStatusPath).onWrite(updateGithubStatus); diff --git a/functions/jwt_util.ts b/functions/jwt_util.ts new file mode 100644 index 000000000000..c51fe3e76175 --- /dev/null +++ b/functions/jwt_util.ts @@ -0,0 +1,37 @@ +import * as firebaseFunctions from 'firebase-functions'; +import {verifySecureToken} from './util/jwt'; + +/** The repo slug. This is used to validate the JWT is sent from correct repo. */ +const repoSlug = firebaseFunctions.config().repo.slug; + +/** The JWT secret. This is used to validate JWT. */ +const secret = firebaseFunctions.config().secret.key; + +/** + * Extract the Json Web Token from event params. + * In screenshot gulp task the path we use is {jwtHeader}/{jwtPayload}/{jwtSignature}. + * Replace '/' with '.' to get the token. + */ +function getSecureToken(event: firebaseFunctions.Event) { + return `${event.params.jwtHeader}.${event.params.jwtPayload}.${event.params.jwtSignature}`; +}; + +/** + * Verify event params have correct JsonWebToken, and execute callback when the JWT is verified. + * Delete the data if there's an error or the callback is done + */ +export function verifySecureTokenAndExecute(event: firebaseFunctions.Event) { + return new Promise((resolve, reject) => { + const prNumber = event.params.prNumber; + const secureToken = getSecureToken(event); + + return verifySecureToken(secureToken, prNumber, secret, repoSlug).then(() => { + resolve(); + event.data.ref.parent.set(null); + }).catch((error: any) => { + console.error(`Invalid secure token ${secureToken} ${error}`); + event.data.ref.parent.set(null); + reject(); + }); + }); +}; diff --git a/functions/package.json b/functions/package.json index 180b444d89e8..98d66a4fae05 100644 --- a/functions/package.json +++ b/functions/package.json @@ -5,6 +5,8 @@ "@google-cloud/storage": "^0.8.0", "firebase-admin": "^4.1.3", "firebase-functions": "^0.5.2", - "jsonwebtoken": "^7.3.0" + "jsonwebtoken": "^7.3.0", + "request": "^2.81.0", + "typescript": "^2.2.1" } } diff --git a/functions/test_goldens.ts b/functions/test_goldens.ts new file mode 100644 index 000000000000..081f0799887b --- /dev/null +++ b/functions/test_goldens.ts @@ -0,0 +1,36 @@ +import * as firebaseFunctions from 'firebase-functions'; +import * as firebaseAdmin from 'firebase-admin'; +import * as path from 'path'; + +const gcs = require('@google-cloud/storage')(); + +/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */ +const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); + +/** + * Copy files from /screenshot/$prNumber/test/ to goldens/ + * Only copy the files that test result is failure. Passed test images should be the same as + * goldens. + */ +export function copyTestImagesToGoldens(prNumber: string) { + return firebaseAdmin.database().ref(`screenshot/reports/${prNumber}/results`).once('value') + .then((snapshot: firebaseAdmin.database.DataSnapshot) => { + let keys: string[] = []; + let counter = 0; + snapshot.forEach((childSnapshot: firebaseAdmin.database.DataSnapshot) => { + if (childSnapshot.val() == false) { + keys.push(childSnapshot.key); + } + counter ++; + if (counter == snapshot.numChildren()) return true; + }); + return keys; + }).then((keys: string[]) => { + return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then(function (data: any) { + return Promise.all(data[0] + .filter((file: any) => keys.includes(path.basename(file.name, '.screenshot.png'))) + .map((file: any) => file.copy(`goldens/${path.basename(file.name)}`))); + }); + }) + +}; diff --git a/functions/tsconfig.json b/functions/tsconfig.json new file mode 100644 index 000000000000..9c70fe999ef8 --- /dev/null +++ b/functions/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "declaration": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": ["es6", "es2016", "dom"], + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "outDir": "./", + "sourceMap": true, + "target": "es5", + "stripInternal": false, + "baseUrl": "", + "typeRoots": [ + "../node_modules/@types/!(node)" + ] + }, + "files": [ + "data.ts", + "data_image.ts", + "github.ts", + "image_data.ts", + "index.ts", + "jwt_util.ts", + "test_goldens.ts", + "util/github.ts", + "util/jwt.ts" + ] +} diff --git a/functions/util/github.ts b/functions/util/github.ts new file mode 100644 index 000000000000..8ddf8b241c59 --- /dev/null +++ b/functions/util/github.ts @@ -0,0 +1,37 @@ +const request = require('request'); + +/** Function that sets a Github commit status */ +export function setGithubStatus(commitSHA: string, + result: boolean, + name: string, + description: string, + url: string, + repoSlug: string, + token: string) { + let state = result ? 'success' : 'failure'; + + let data = JSON.stringify({ + state: state, + target_url: url, + context: name, + description: description + }); + + let headers = { + "Authorization": `token ${token}`, + "User-Agent": `${name}/1.0`, + "Content-Type": "application/json" + }; + + return new Promise((resolve) => { + request({ + url: `https://api.github.com/repos/${repoSlug}/statuses/${commitSHA}`, + method: 'POST', + form: data, + headers: headers + }, function (error: any, response: any) { + console.log(response.statusCode); + resolve(response.statusCode); + }); + }); +}; diff --git a/functions/util/jwt.ts b/functions/util/jwt.ts new file mode 100644 index 000000000000..8e903a0022eb --- /dev/null +++ b/functions/util/jwt.ts @@ -0,0 +1,20 @@ +import * as jwt from 'jsonwebtoken'; + +export function verifySecureToken(token: string, + prNumber: string, + secret: string, + repoSlug: string) { + return new Promise((resolve, reject) => { + jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => { + if (err) { + reject(err.message || err); + } else if (payload.slug !== repoSlug) { + reject(`jwt slug invalid. expected: ${repoSlug}`); + } else if (payload['pull-request'].toString() !== prNumber) { + reject(`jwt pull-request invalid. expected: ${prNumber}`); + } else { + resolve(payload); + } + }); + }); +}; diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 4d9615231424..32d904672bbf 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -5,50 +5,48 @@ import * as admin from 'firebase-admin'; import * as firebase from 'firebase'; import { openScreenshotsBucket, - openFirebaseScreenshotsDatabase, connectFirebaseScreenshots} from '../util/firebase'; -import {setGithubStatus} from '../util/github'; import {isTravisPushBuild} from '../util/travis-ci'; const imageDiff = require('image-diff'); +const SCREENSHOT_DIR = './screenshots'; +const LOCAL_GOLDENS = path.join(SCREENSHOT_DIR, `golds`); +const LOCAL_DIFFS = path.join(SCREENSHOT_DIR, `diff`); + // Directory to which untrusted screenshot results are temporarily written // (without authentication required) before they are verified and copied to // the final storage location. const TEMP_FOLDER = 'untrustedInbox'; -const SCREENSHOT_DIR = './screenshots'; const FIREBASE_REPORT = `${TEMP_FOLDER}/screenshot/reports`; const FIREBASE_IMAGE = `${TEMP_FOLDER}/screenshot/images`; -const FIREBASE_FILELIST = 'screenshot/filenames'; +const FIREBASE_DATA_GOLDENS = `screenshot/goldens`; +const FIREBASE_STORAGE_GOLDENS = 'goldens'; /** Task which upload screenshots generated from e2e test. */ task('screenshots', () => { let prNumber = process.env['TRAVIS_PULL_REQUEST']; + if (isTravisPushBuild()) { - // Only update golds and filenames for build - let database = openFirebaseScreenshotsDatabase(); - uploadScreenshots() - .then(() => setScreenFilenames(database)) - .then(() => database.goOffline(), () => database.goOffline()); + // Only update goldens for build + return uploadScreenshots(); } else if (prNumber) { let firebaseApp = connectFirebaseScreenshots(); let database = firebaseApp.database(); - return getScreenshotFiles(database) + return updateTravis(database, prNumber) + .then(() => getScreenshotFiles(database)) .then(() => downloadAllGoldsAndCompare(database, prNumber)) .then((results: boolean) => updateResult(database, prNumber, results)) - .then((result: boolean) => updateGithubStatus(prNumber, result)) .then(() => uploadScreenshotsData(database, 'diff', prNumber)) .then(() => uploadScreenshotsData(database, 'test', prNumber)) - .then(() => updateTravis(database, prNumber)) - .then(() => setScreenFilenames(database, prNumber)) .then(() => database.goOffline(), () => database.goOffline()); } }); function updateFileResult(database: firebase.database.Database, prNumber: string, filenameKey: string, result: boolean) { - return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result); + return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result) ; } function updateResult(database: firebase.database.Database, prNumber: string, result: boolean) { @@ -65,7 +63,6 @@ function getPullRequestRef(database: firebase.database.Database | admin.database function updateTravis(database: firebase.database.Database, prNumber: string) { return getPullRequestRef(database, prNumber).update({ - commit: process.env['TRAVIS_COMMIT'], sha: process.env['TRAVIS_PULL_REQUEST_SHA'], travis: process.env['TRAVIS_JOB_ID'], }); @@ -73,16 +70,16 @@ function updateTravis(database: firebase.database.Database, /** Get a list of filenames from firebase database. */ function getScreenshotFiles(database: firebase.database.Database) { - mkdirp(path.join(SCREENSHOT_DIR, `golds`)); - mkdirp(path.join(SCREENSHOT_DIR, `diff`)); + mkdirp(LOCAL_GOLDENS); + mkdirp(LOCAL_DIFFS); - return database.ref('screenshot/goldens').once('value') + return database.ref(FIREBASE_DATA_GOLDENS).once('value') .then((snapshot: firebase.database.DataSnapshot) => { let counter = 0; snapshot.forEach((childSnapshot: firebase.database.DataSnapshot) => { let key = childSnapshot.key; let binaryData = new Buffer(childSnapshot.val(), 'base64').toString('binary'); - writeFileSync(`${SCREENSHOT_DIR}/golds/${key}.screenshot.png`, binaryData, 'binary'); + writeFileSync(`${LOCAL_GOLDENS}/${key}.screenshot.png`, binaryData, 'binary'); counter++; if (counter == snapshot.numChildren()) { return true; @@ -140,7 +137,7 @@ function uploadScreenshotsData(database: firebase.database.Database, /** Download golds screenshots. */ function downloadAllGoldsAndCompare(database: firebase.database.Database, prNumber: string) { - let filenames = getLocalScreenshotFiles(path.join(SCREENSHOT_DIR, `golds`)); + let filenames = getLocalScreenshotFiles(LOCAL_GOLDENS); return Promise.all(filenames.map((filename: string) => { return diffScreenshot(filename, database, prNumber); @@ -151,9 +148,9 @@ function diffScreenshot(filename: string, database: firebase.database.Database, prNumber: string) { // TODO(tinayuangao): Run the downloads and diffs in parallel. filename = path.basename(filename); - let goldUrl = path.join(SCREENSHOT_DIR, `golds`, filename); + let goldUrl = path.join(LOCAL_GOLDENS, filename); let pullRequestUrl = path.join(SCREENSHOT_DIR, filename); - let diffUrl = path.join(SCREENSHOT_DIR, `diff`, filename); + let diffUrl = path.join(LOCAL_DIFFS, filename); let filenameKey = extractScreenshotName(filename); if (existsSync(goldUrl) && existsSync(pullRequestUrl)) { @@ -177,35 +174,12 @@ function diffScreenshot(filename: string, database: firebase.database.Database, } } -/** - * Upload a list of filenames to firebase database as gold. - * This is necessary for control panel since google-cloud is not available to client side. - */ -function setScreenFilenames(database: admin.database.Database | firebase.database.Database, - prNumber?: string) { - let filenames: string[] = getLocalScreenshotFiles(SCREENSHOT_DIR); - let filelistDatabase = prNumber ? - getPullRequestRef(database, prNumber).child('filenames') : - database.ref(FIREBASE_FILELIST); - return filelistDatabase.set(filenames); -} - -/** Updates the Github Status of the given Pullrequest. */ -function updateGithubStatus(prNumber: number, result: boolean) { - setGithubStatus(process.env['TRAVIS_PULL_REQUEST_SHA'], { - result: result, - name: 'Screenshot Tests', - description: `Screenshot Tests ${result ? 'passed' : 'failed'})`, - url: `http://material2-screenshots.firebaseapp.com/${prNumber}` - }); -} - /** Upload screenshots to google cloud storage. */ function uploadScreenshots() { let bucket = openScreenshotsBucket(); let promises = getLocalScreenshotFiles(SCREENSHOT_DIR).map((file: string) => { let fileName = path.join(SCREENSHOT_DIR, file); - let destination = `golds/${file}`; + let destination = `${FIREBASE_STORAGE_GOLDENS}/${file}`; return bucket.upload(fileName, { destination: destination }); }); return Promise.all(promises); diff --git a/tools/gulp/util/firebase.ts b/tools/gulp/util/firebase.ts index 213f284a6e5e..554caeeba100 100644 --- a/tools/gulp/util/firebase.ts +++ b/tools/gulp/util/firebase.ts @@ -39,22 +39,6 @@ export function openScreenshotsBucket() { return gcs.bucket('material2-screenshots.appspot.com'); } -/** Opens a connection to the firebase database for screenshots. */ -export function openFirebaseScreenshotsDatabase() { - // Initialize the Firebase application with firebaseAdmin credentials. - // Credentials need to be for a Service Account, which can be created in the Firebase console. - let screenshotApp = firebaseAdmin.initializeApp({ - credential: firebaseAdmin.credential.cert({ - project_id: 'material2-screenshots', - client_email: 'firebase-adminsdk-t4209@material2-screenshots.iam.gserviceaccount.com', - private_key: decode(process.env['MATERIAL2_SCREENSHOT_FIREBASE_KEY']) - }), - databaseURL: 'https://material2-screenshots.firebaseio.com' - }, 'material2-screenshots'); - - return screenshotApp.database(); -} - /** Decodes a Travis CI variable that is public in favor for PRs. */ export function decode(str: string): string { // In Travis CI the private key will be incorrect because the line-breaks are escaped. From 0ad5540b5e93fc1b4ee6521ced43e3d15cc459ea Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 27 Mar 2017 11:03:11 -0700 Subject: [PATCH 2/7] Address comments --- functions/data_image.ts | 4 +- functions/github.ts | 10 +- functions/index.js | 198 +++++++++----------------------- functions/package.json | 3 +- functions/test_goldens.ts | 14 +-- functions/tsconfig.json | 10 +- functions/util/github.ts | 23 ++-- tools/gulp/tasks/screenshots.ts | 2 +- 8 files changed, 89 insertions(+), 175 deletions(-) diff --git a/functions/data_image.ts b/functions/data_image.ts index 61f130443a91..c9fba2dd739a 100644 --- a/functions/data_image.ts +++ b/functions/data_image.ts @@ -9,7 +9,7 @@ const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); /** * Convert data to images. Image data posted to database will be saved as png files - * and upload to screenshot/$prNumber/dataType/$filename + * and uploaded to screenshot/$prNumber/dataType/$filename */ export function convertTestImageDataToFiles(event: any) { // Only edit data when it is first created. Exit when the data is deleted. @@ -22,7 +22,7 @@ export function convertTestImageDataToFiles(event: any) { let data = event.data.val(); let saveFilename = `${event.params.filename}.screenshot.png`; - if (dataType != 'diff' && dataType != 'test') { + if (dataType !== 'diff' && dataType !== 'test') { return; } diff --git a/functions/github.ts b/functions/github.ts index 5aa890e3e5a2..62217cff081a 100644 --- a/functions/github.ts +++ b/functions/github.ts @@ -22,10 +22,12 @@ export function updateGithubStatus(event: firebaseFunctions.Event) { let prNumber = event.params.prNumber; return event.data.ref.parent.child('sha').once('value').then((sha: firebaseAdmin.database.DataSnapshot) => { return setGithubStatus(sha.val(), - result, - toolName, - `${toolName} ${result ? 'passed' : 'failed'}`, - `http://${authDomain}/${prNumber}`, + { + result: result, + name: toolName, + description: `${toolName} ${result ? 'passed' : 'failed'}`, + url: `http://${authDomain}/${prNumber}` + }, repoSlug, token); }); diff --git a/functions/index.js b/functions/index.js index ec7a0f3ac675..ecd520d9df6a 100644 --- a/functions/index.js +++ b/functions/index.js @@ -1,12 +1,18 @@ 'use strict'; - -const firebaseFunctions = require('firebase-functions'); -const firebaseAdmin = require('firebase-admin'); -const gcs = require('@google-cloud/storage')(); -const jwt = require('jsonwebtoken'); -const fs = require('fs'); - +Object.defineProperty(exports, "__esModule", { value: true }); +var firebaseFunctions = require("firebase-functions"); +var firebaseAdmin = require("firebase-admin"); +var data_1 = require("./data"); +var image_data_1 = require("./image_data"); +var data_image_1 = require("./data_image"); +var test_goldens_1 = require("./test_goldens"); +var github_1 = require("./github"); /** + * Usage: Firebase functions only accept javascript file index.js + * tsc + * firebase deploy --only functions + * + * * Data and images handling for Screenshot test. * * All users can post data to temporary folder. These Functions will check the data with JsonWebToken and @@ -24,167 +30,77 @@ const fs = require('fs'); * under location /screenshot/goldens * Screenshot tests can only read restricted database data with no credentials, and they cannot access * Google Cloud Storage. Therefore we copy the image data to database to make it available to screenshot tests. - * + * * The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to database/storage. * All invalid data will be removed. * The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path. */ - // Initailize the admin app firebaseAdmin.initializeApp(firebaseFunctions.config().firebase); - /** The valid data types database accepts */ -const dataTypes = ['filenames', 'commit', 'result', 'sha', 'travis']; - -/** The repo slug. This is used to validate the JWT is sent from correct repo. */ -const repoSlug = firebaseFunctions.config().repo.slug; - -/** The JWT secret. This is used to validate JWT. */ -const secret = firebaseFunctions.config().secret.key; - -/** The storage bucket to store the images. The bucket is also used by Firebase Storage. */ -const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); - +var dataTypes = ['result', 'sha', 'travis']; /** The Json Web Token format. The token is stored in data path. */ -const jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}'; - +var jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}'; /** The temporary folder name for screenshot data that needs to be validated via JWT. */ -const tempFolder = '/untrustedInbox'; - +var tempFolder = '/untrustedInbox'; +/** Untrusted report data for a PR */ +var reportPath = tempFolder + "/screenshot/reports/{prNumber}/" + jwtFormat + "/"; +/** Untrusted image data for a PR */ +var imagePath = tempFolder + "/screenshot/images/{prNumber}/" + jwtFormat + "/"; +/** Trusted report data for a PR */ +var trustedReportPath = "screenshot/reports/{prNumber}"; /** - * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber + * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ + * to /screenshot/reports/$prNumber * Data copied: filenames(image results names), commit(github PR info), * sha (github PR info), result (true or false for all the tests), travis job number */ -const copyDataPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/{dataType}`; -exports.copyData = firebaseFunctions.database.ref(copyDataPath).onWrite(event => { - const dataType = event.params.dataType; - if (dataTypes.includes(dataType)) { - return verifyAndCopyScreenshotResult(event, dataType); - } +var testDataPath = reportPath + "/{dataType}"; +exports.testData = firebaseFunctions.database.ref(testDataPath) + .onWrite(function (event) { + var dataType = event.params.dataType; + if (dataTypes.includes(dataType)) { + return data_1.verifyJWTAndUpdateData(event, dataType); + } }); - /** - * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber + * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ + * to /screenshot/reports/$prNumber * Data copied: test result for each file/test with ${filename}. The value should be true or false. */ -const copyDataResultPath = `${tempFolder}/screenshot/reports/{prNumber}/${jwtFormat}/results/{filename}`; -exports.copyDataResult = firebaseFunctions.database.ref(copyDataResultPath).onWrite(event => { - return verifyAndCopyScreenshotResult(event, `results/${event.params.filename}`); +var testResultsPath = reportPath + "/results/{filename}"; +exports.testResults = firebaseFunctions.database.ref(testResultsPath) + .onWrite(function (event) { + return data_1.verifyJWTAndUpdateData(event, "results/" + event.params.filename); }); - /** - * Copy valid data from database /$temp/screenshot/images/$prNumber/$secureToken/ to storage /screenshots/$prNumber + * Copy valid data from database /$temp/screenshot/images/$prNumber/$secureToken/ + * to storage /screenshots/$prNumber * Data copied: test result images. Convert from data to image files in storage. */ -const copyImagePath = `${tempFolder}/screenshot/images/{prNumber}/${jwtFormat}/{dataType}/{filename}`; -exports.copyImage = firebaseFunctions.database.ref(copyImagePath).onWrite(event => { - // Only edit data when it is first created. Exit when the data is deleted. - if (event.data.previous.exists() || !event.data.exists()) { - return; - } - - const dataType = event.params.dataType; - const prNumber = event.params.prNumber; - const secureToken = getSecureToken(event); - const saveFilename = `${event.params.filename}.screenshot.png`; - - if (dataType != 'diff' && dataType != 'test') { - return; - } - - return verifySecureToken(secureToken, prNumber).then((payload) => { - const tempPath = `/tmp/${dataType}-${saveFilename}` - const filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`; - const binaryData = new Buffer(event.data.val(), 'base64').toString('binary'); - fs.writeFile(tempPath, binaryData, 'binary'); - return bucket.upload(tempPath, {destination: filePath}).then(() => { - // Clear the data in temporary folder after processed. - return event.data.ref.parent.set(null); - }); - }).catch((error) => { - console.error(`Invalid secure token ${secureToken} ${error}`); - return event.data.ref.parent.set(null); - }); -}); - +var imageDataToFilePath = imagePath + "/{dataType}/{filename}"; +exports.imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath) + .onWrite(data_image_1.convertTestImageDataToFiles); /** * Copy valid goldens from storage /goldens/ to database /screenshot/goldens/ * so we can read the goldens without credentials. */ -exports.copyGoldens = firebaseFunctions.storage.bucket(firebaseFunctions.config().firebase.storageBucket) - .object().onChange(event => { - // The filePath should always l ook like "goldens/xxx.png" - const filePath = event.data.name; - - // Get the file name. - const fileNames = filePath.split('/'); - if (fileNames.length != 2 && fileNames[0] != 'goldens') { - return; - } - const filenameKey = fileNames[1].replace('.screenshot.png', ''); - - // When a gold image is deleted, also delete the corresponding record in the firebase database. - if (event.data.resourceState === 'not_exists') { - return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(null); - } - - // Download file from bucket. - const bucket = gcs.bucket(event.data.bucket); - const tempFilePath = `/tmp/${fileNames[1]}`; - return bucket.file(filePath).download({destination: tempFilePath}).then(() => { - const data = fs.readFileSync(tempFilePath); - return firebaseAdmin.database().ref(`screenshot/goldens/${filenameKey}`).set(data); - }); +exports.goldenImageToData = firebaseFunctions.storage.bucket(firebaseFunctions.config().firebase.storageBucket).object().onChange(function (event) { + return image_data_1.convertGoldenImagesToData(event.data.name, event.data.resourceState, event.data.bucket); }); - /** - * Handle data written to temporary folder. Validate the JWT and move the data out of - * temporary folder if the token is valid. + * Copy test result images for PR to Goldens. + * Copy images from /screenshot/$prNumber/test/ to /goldens/ */ -function verifyAndCopyScreenshotResult(event, path) { - // Only edit data when it is first created. Exit when the data is deleted. - if (event.data.previous.exists() || !event.data.exists()) { - return; - } - - const prNumber = event.params.prNumber; - const secureToken = getSecureToken(event); - const original = event.data.val(); - - return verifySecureToken(secureToken, prNumber).then((payload) => { - return firebaseAdmin.database().ref().child('screenshot/reports') - .child(prNumber).child(path).set(original).then(() => { - // Clear the data in temporary folder after processed. - return event.data.ref.parent.set(null); - }); - }).catch((error) => { - console.error(`Invalid secure token ${secureToken} ${error}`); - return event.data.ref.parent.set(null); - }); -} - +var approveImagesPath = trustedReportPath + "/approved"; +exports.approveImages = firebaseFunctions.database.ref(approveImagesPath).onWrite(function (event) { + return test_goldens_1.copyTestImagesToGoldens(event.params.prNumber); +}); /** - * Extract the Json Web Token from event params. - * In screenshot gulp task the path we use is {jwtHeader}/{jwtPayload}/{jwtSignature}. - * Replace '/' with '.' to get the token. + * Update github status. When the result is true, update github commit status to `success`, + * otherwise update github status to `failure`. + * The Github Status Token is set in config.secret.github */ -function getSecureToken(event) { - return `${event.params.jwtHeader}.${event.params.jwtPayload}.${event.params.jwtSignature}`; -} - -function verifySecureToken(token, prNumber) { - return new Promise((resolve, reject) => { - jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => { - if (err) { - reject(err.message || err); - } else if (payload.slug !== repoSlug) { - reject(`jwt slug invalid. expected: ${repoSlug}`); - } else if (payload['pull-request'].toString() !== prNumber) { - reject(`jwt pull-request invalid. expected: ${prNumber} actual: ${payload['pull-request']}`); - } else { - resolve(payload); - } - }); - }); -} +var githubStatusPath = trustedReportPath + "/result"; +exports.githubStatus = firebaseFunctions.database.ref(githubStatusPath).onWrite(github_1.updateGithubStatus); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/functions/package.json b/functions/package.json index 98d66a4fae05..cd9f629113fe 100644 --- a/functions/package.json +++ b/functions/package.json @@ -6,7 +6,6 @@ "firebase-admin": "^4.1.3", "firebase-functions": "^0.5.2", "jsonwebtoken": "^7.3.0", - "request": "^2.81.0", - "typescript": "^2.2.1" + "request": "^2.81.0" } } diff --git a/functions/test_goldens.ts b/functions/test_goldens.ts index 081f0799887b..337d49c35413 100644 --- a/functions/test_goldens.ts +++ b/functions/test_goldens.ts @@ -15,20 +15,20 @@ const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); export function copyTestImagesToGoldens(prNumber: string) { return firebaseAdmin.database().ref(`screenshot/reports/${prNumber}/results`).once('value') .then((snapshot: firebaseAdmin.database.DataSnapshot) => { - let keys: string[] = []; + let failedFilenames: string[] = []; let counter = 0; snapshot.forEach((childSnapshot: firebaseAdmin.database.DataSnapshot) => { - if (childSnapshot.val() == false) { - keys.push(childSnapshot.key); + if (childSnapshot.val() === false) { + failedFilenames.push(childSnapshot.key); } - counter ++; + counter++; if (counter == snapshot.numChildren()) return true; }); - return keys; - }).then((keys: string[]) => { + return failedFilenames; + }).then((failedFilenames: string[]) => { return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then(function (data: any) { return Promise.all(data[0] - .filter((file: any) => keys.includes(path.basename(file.name, '.screenshot.png'))) + .filter((file: any) => failedFilenames.includes(path.basename(file.name, '.screenshot.png'))) .map((file: any) => file.copy(`goldens/${path.basename(file.name)}`))); }); }) diff --git a/functions/tsconfig.json b/functions/tsconfig.json index 9c70fe999ef8..cb6ad1adf008 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -18,14 +18,6 @@ ] }, "files": [ - "data.ts", - "data_image.ts", - "github.ts", - "image_data.ts", - "index.ts", - "jwt_util.ts", - "test_goldens.ts", - "util/github.ts", - "util/jwt.ts" + "index.ts" ] } diff --git a/functions/util/github.ts b/functions/util/github.ts index 8ddf8b241c59..7976cac8f705 100644 --- a/functions/util/github.ts +++ b/functions/util/github.ts @@ -1,25 +1,30 @@ const request = require('request'); +/** Data that must be specified to set a Github PR status. */ +export type GithubStatusData = { + result: boolean; + name: string; + description: string; + url: string; +}; + /** Function that sets a Github commit status */ export function setGithubStatus(commitSHA: string, - result: boolean, - name: string, - description: string, - url: string, + statusData: GithubStatusData, repoSlug: string, token: string) { - let state = result ? 'success' : 'failure'; + let state = statusData.result ? 'success' : 'failure'; let data = JSON.stringify({ state: state, - target_url: url, - context: name, - description: description + target_url: statusData.url, + context: statusData.name, + description: statusData.description }); let headers = { "Authorization": `token ${token}`, - "User-Agent": `${name}/1.0`, + "User-Agent": `${statusData.name}/1.0`, "Content-Type": "application/json" }; diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 32d904672bbf..902d0bd95a72 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -46,7 +46,7 @@ task('screenshots', () => { function updateFileResult(database: firebase.database.Database, prNumber: string, filenameKey: string, result: boolean) { - return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result) ; + return getPullRequestRef(database, prNumber).child('results').child(filenameKey).set(result); } function updateResult(database: firebase.database.Database, prNumber: string, result: boolean) { From 23545428afa1fee8d0b64eecd59d904ddd41a7a2 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 27 Mar 2017 14:41:11 -0700 Subject: [PATCH 3/7] Add sha to result --- functions/github.ts | 20 +++++++++----------- functions/index.ts | 2 +- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/functions/github.ts b/functions/github.ts index 62217cff081a..3742bbb08c9b 100644 --- a/functions/github.ts +++ b/functions/github.ts @@ -20,15 +20,13 @@ export function updateGithubStatus(event: firebaseFunctions.Event) { } let result = event.data.val() == true; let prNumber = event.params.prNumber; - return event.data.ref.parent.child('sha').once('value').then((sha: firebaseAdmin.database.DataSnapshot) => { - return setGithubStatus(sha.val(), - { - result: result, - name: toolName, - description: `${toolName} ${result ? 'passed' : 'failed'}`, - url: `http://${authDomain}/${prNumber}` - }, - repoSlug, - token); - }); + return setGithubStatus(event.params.sha, + { + result: result, + name: toolName, + description: `${toolName} ${result ? 'passed' : 'failed'}`, + url: `http://${authDomain}/${prNumber}` + }, + repoSlug, + token); } diff --git a/functions/index.ts b/functions/index.ts index a328b6131423..6944bd8d963d 100644 --- a/functions/index.ts +++ b/functions/index.ts @@ -116,5 +116,5 @@ exports.approveImages = firebaseFunctions.database.ref(approveImagesPath).onWrit * otherwise update github status to `failure`. * The Github Status Token is set in config.secret.github */ -const githubStatusPath = `${trustedReportPath}/result`; +const githubStatusPath = `${trustedReportPath}/result/{sha}`; exports.githubStatus = firebaseFunctions.database.ref(githubStatusPath).onWrite(updateGithubStatus); From 0c76b581677dd2b12d69e03ee379cbeb2a8d403a Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 3 Apr 2017 10:49:33 -0700 Subject: [PATCH 4/7] Move files to tools/screenshot-test/functions --- .gitignore | 2 +- functions/data.ts | 22 ---- functions/index.js | 106 ------------------ package.json | 4 + .../screenshot-test/functions}/data_image.ts | 9 +- .../screenshot-test/functions}/github.ts | 1 - .../screenshot-test/functions}/image_data.ts | 6 +- .../screenshot-test/functions}/index.ts | 52 +++++---- .../screenshot-test/functions}/jwt_util.ts | 10 +- .../functions}/test_goldens.ts | 10 +- .../screenshot-test/functions}/tsconfig.json | 10 +- .../screenshot-test/functions}/util/github.ts | 6 +- .../screenshot-test/functions}/util/jwt.ts | 10 +- tools/screenshot-test/functions/util/util.ts | 11 ++ .../functions/verify-and-copy-report.ts | 22 ++++ 15 files changed, 102 insertions(+), 179 deletions(-) delete mode 100644 functions/data.ts delete mode 100644 functions/index.js rename {functions => tools/screenshot-test/functions}/data_image.ts (79%) rename {functions => tools/screenshot-test/functions}/github.ts (95%) rename {functions => tools/screenshot-test/functions}/image_data.ts (84%) rename {functions => tools/screenshot-test/functions}/index.ts (73%) rename {functions => tools/screenshot-test/functions}/jwt_util.ts (73%) rename {functions => tools/screenshot-test/functions}/test_goldens.ts (86%) rename {functions => tools/screenshot-test/functions}/tsconfig.json (58%) rename {functions => tools/screenshot-test/functions}/util/github.ts (89%) rename {functions => tools/screenshot-test/functions}/util/jwt.ts (60%) create mode 100644 tools/screenshot-test/functions/util/util.ts create mode 100644 tools/screenshot-test/functions/verify-and-copy-report.ts diff --git a/.gitignore b/.gitignore index 9fded7174921..24e821d2e946 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ /screenshots # dependencies -/node_modules +node_modules /bower_components # Dart diff --git a/functions/data.ts b/functions/data.ts deleted file mode 100644 index d06fbee95210..000000000000 --- a/functions/data.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as firebaseAdmin from 'firebase-admin'; -import {verifySecureTokenAndExecute} from './jwt_util'; - -/** - * Handle data written to temporary folder. Validate the JWT and move the data out of - * temporary folder if the token is valid. - * Move the data to 'screenshot/reports/$prNumber/$path - */ -export function verifyJWTAndUpdateData(event: any, path: string) { - // Only edit data when it is first created. Exit when the data is deleted. - if (event.data.previous.exists() || !event.data.exists()) { - return; - } - - let prNumber = event.params.prNumber; - let data = event.data.val(); - - return verifySecureTokenAndExecute(event).then(() => { - return firebaseAdmin.database().ref().child('screenshot/reports') - .child(prNumber).child(path).set(data); - }); -}; diff --git a/functions/index.js b/functions/index.js deleted file mode 100644 index ecd520d9df6a..000000000000 --- a/functions/index.js +++ /dev/null @@ -1,106 +0,0 @@ -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -var firebaseFunctions = require("firebase-functions"); -var firebaseAdmin = require("firebase-admin"); -var data_1 = require("./data"); -var image_data_1 = require("./image_data"); -var data_image_1 = require("./data_image"); -var test_goldens_1 = require("./test_goldens"); -var github_1 = require("./github"); -/** - * Usage: Firebase functions only accept javascript file index.js - * tsc - * firebase deploy --only functions - * - * - * Data and images handling for Screenshot test. - * - * All users can post data to temporary folder. These Functions will check the data with JsonWebToken and - * move the valid data out of temporary folder. - * - * For valid data posted to database /$temp/screenshot/reports/$prNumber/$secureToken, move it to - * /screenshot/reports/$prNumber. - * These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job information - * - * For valid image results written to database /$temp/screenshot/images/$prNumber/$secureToken/, save the image - * data to image files and upload to google cloud storage under location /screenshots/$prNumber - * These are screenshot test result images, and difference images generated from screenshot comparison. - * - * For golden images uploaded to /goldens, read the data from images files and write the data to Firebase database - * under location /screenshot/goldens - * Screenshot tests can only read restricted database data with no credentials, and they cannot access - * Google Cloud Storage. Therefore we copy the image data to database to make it available to screenshot tests. - * - * The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to database/storage. - * All invalid data will be removed. - * The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path. - */ -// Initailize the admin app -firebaseAdmin.initializeApp(firebaseFunctions.config().firebase); -/** The valid data types database accepts */ -var dataTypes = ['result', 'sha', 'travis']; -/** The Json Web Token format. The token is stored in data path. */ -var jwtFormat = '{jwtHeader}/{jwtPayload}/{jwtSignature}'; -/** The temporary folder name for screenshot data that needs to be validated via JWT. */ -var tempFolder = '/untrustedInbox'; -/** Untrusted report data for a PR */ -var reportPath = tempFolder + "/screenshot/reports/{prNumber}/" + jwtFormat + "/"; -/** Untrusted image data for a PR */ -var imagePath = tempFolder + "/screenshot/images/{prNumber}/" + jwtFormat + "/"; -/** Trusted report data for a PR */ -var trustedReportPath = "screenshot/reports/{prNumber}"; -/** - * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ - * to /screenshot/reports/$prNumber - * Data copied: filenames(image results names), commit(github PR info), - * sha (github PR info), result (true or false for all the tests), travis job number - */ -var testDataPath = reportPath + "/{dataType}"; -exports.testData = firebaseFunctions.database.ref(testDataPath) - .onWrite(function (event) { - var dataType = event.params.dataType; - if (dataTypes.includes(dataType)) { - return data_1.verifyJWTAndUpdateData(event, dataType); - } -}); -/** - * Copy valid data from /$temp/screenshot/reports/$prNumber/$secureToken/ - * to /screenshot/reports/$prNumber - * Data copied: test result for each file/test with ${filename}. The value should be true or false. - */ -var testResultsPath = reportPath + "/results/{filename}"; -exports.testResults = firebaseFunctions.database.ref(testResultsPath) - .onWrite(function (event) { - return data_1.verifyJWTAndUpdateData(event, "results/" + event.params.filename); -}); -/** - * Copy valid data from database /$temp/screenshot/images/$prNumber/$secureToken/ - * to storage /screenshots/$prNumber - * Data copied: test result images. Convert from data to image files in storage. - */ -var imageDataToFilePath = imagePath + "/{dataType}/{filename}"; -exports.imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath) - .onWrite(data_image_1.convertTestImageDataToFiles); -/** - * Copy valid goldens from storage /goldens/ to database /screenshot/goldens/ - * so we can read the goldens without credentials. - */ -exports.goldenImageToData = firebaseFunctions.storage.bucket(firebaseFunctions.config().firebase.storageBucket).object().onChange(function (event) { - return image_data_1.convertGoldenImagesToData(event.data.name, event.data.resourceState, event.data.bucket); -}); -/** - * Copy test result images for PR to Goldens. - * Copy images from /screenshot/$prNumber/test/ to /goldens/ - */ -var approveImagesPath = trustedReportPath + "/approved"; -exports.approveImages = firebaseFunctions.database.ref(approveImagesPath).onWrite(function (event) { - return test_goldens_1.copyTestImagesToGoldens(event.params.prNumber); -}); -/** - * Update github status. When the result is true, update github commit status to `success`, - * otherwise update github status to `failure`. - * The Github Status Token is set in config.secret.github - */ -var githubStatusPath = trustedReportPath + "/result"; -exports.githubStatus = firebaseFunctions.database.ref(githubStatusPath).onWrite(github_1.updateGithubStatus); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/package.json b/package.json index ad4745123364..c9d8972ec83d 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@angular/platform-browser-dynamic": "^4.0.0", "@angular/platform-server": "^4.0.0", "@angular/router": "^4.0.0", + "@google-cloud/storage": "^0.8.0", "@types/chalk": "^0.4.31", "@types/fs-extra": "0.0.37", "@types/glob": "^5.0.30", @@ -62,6 +63,7 @@ "dgeni-packages": "^0.16.5", "firebase": "^3.7.2", "firebase-admin": "^4.1.2", + "firebase-functions": "^0.5.2", "firebase-tools": "^2.2.1", "fs-extra": "^2.0.0", "glob": "^7.1.1", @@ -87,6 +89,7 @@ "http-rewrite-middleware": "^0.1.6", "image-diff": "^1.6.3", "jasmine-core": "^2.5.2", + "jsonwebtoken": "^7.3.0", "karma": "^1.5.0", "karma-browserstack-launcher": "^1.2.0", "karma-chrome-launcher": "^2.0.0", @@ -100,6 +103,7 @@ "minimist": "^1.2.0", "node-sass": "^4.5.0", "protractor": "^5.1.1", + "request": "^2.81.0", "resolve-bin": "^0.4.0", "rollup": "^0.41.6", "run-sequence": "^1.2.2", diff --git a/functions/data_image.ts b/tools/screenshot-test/functions/data_image.ts similarity index 79% rename from functions/data_image.ts rename to tools/screenshot-test/functions/data_image.ts index c9fba2dd739a..7b6b034b830a 100644 --- a/functions/data_image.ts +++ b/tools/screenshot-test/functions/data_image.ts @@ -1,6 +1,7 @@ import * as firebaseFunctions from 'firebase-functions'; import {writeFileSync} from 'fs'; -import {verifySecureTokenAndExecute} from './jwt_util'; +import {verifySecureToken} from './jwt_util'; +import {isCreateEvent} from './util/util'; const gcs = require('@google-cloud/storage')(); @@ -10,10 +11,11 @@ const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); /** * Convert data to images. Image data posted to database will be saved as png files * and uploaded to screenshot/$prNumber/dataType/$filename + * Convert BufferArray to .png image file */ export function convertTestImageDataToFiles(event: any) { // Only edit data when it is first created. Exit when the data is deleted. - if (event.data.previous.exists() || !event.data.exists()) { + if (!isCreateEvent(event)) { return; } @@ -22,11 +24,12 @@ export function convertTestImageDataToFiles(event: any) { let data = event.data.val(); let saveFilename = `${event.params.filename}.screenshot.png`; + // Check it's either diff images generated by screenshot comparison, or the test image results if (dataType !== 'diff' && dataType !== 'test') { return; } - return verifySecureTokenAndExecute(event).then(() => { + return verifySecureToken(event).then(() => { let tempPath = `/tmp/${dataType}-${saveFilename}`; let filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`; let binaryData = new Buffer(data, 'base64').toString('binary'); diff --git a/functions/github.ts b/tools/screenshot-test/functions/github.ts similarity index 95% rename from functions/github.ts rename to tools/screenshot-test/functions/github.ts index 3742bbb08c9b..ff2caf017666 100644 --- a/functions/github.ts +++ b/tools/screenshot-test/functions/github.ts @@ -1,4 +1,3 @@ -import * as firebaseAdmin from 'firebase-admin'; import * as firebaseFunctions from 'firebase-functions'; import {setGithubStatus} from './util/github'; diff --git a/functions/image_data.ts b/tools/screenshot-test/functions/image_data.ts similarity index 84% rename from functions/image_data.ts rename to tools/screenshot-test/functions/image_data.ts index c205b3a25fd7..99f5c6dce26f 100644 --- a/functions/image_data.ts +++ b/tools/screenshot-test/functions/image_data.ts @@ -4,17 +4,21 @@ import {readFileSync} from 'fs'; const gcs = require('@google-cloud/storage')(); +/** Folder on Firebase database to store golden images data */ const FIREBASE_DATA_GOLDENS = 'screenshot/goldens'; /** * Read golden files under /goldens/ and store the image data to * database /screenshot/goldens/$filename + * Convert png image files to BufferArray data */ export function convertGoldenImagesToData(name: string, resourceState: string, fileBucket: any) { // The name should always look like "goldens/xxx.png" let parsedPath = path.parse(name); // Get the file name. - if (parsedPath.root != '' || parsedPath.dir != 'goldens' || parsedPath.ext != '.png') { + if (parsedPath.root != '' || + parsedPath.dir != 'goldens' || + parsedPath.ext.toLowerCase() != '.png') { return; } diff --git a/functions/index.ts b/tools/screenshot-test/functions/index.ts similarity index 73% rename from functions/index.ts rename to tools/screenshot-test/functions/index.ts index 6944bd8d963d..8f95dcde89e6 100644 --- a/functions/index.ts +++ b/tools/screenshot-test/functions/index.ts @@ -3,7 +3,7 @@ import * as firebaseFunctions from 'firebase-functions'; import * as firebaseAdmin from 'firebase-admin'; -import {verifyJWTAndUpdateData} from './data'; +import {verifyJwtAndTransferResultToTrustedLocation} from './verify-and-copy-report'; import {convertGoldenImagesToData} from './image_data'; import {convertTestImageDataToFiles} from './data_image'; import {copyTestImagesToGoldens} from './test_goldens'; @@ -11,29 +11,37 @@ import {updateGithubStatus} from './github'; /** * Usage: Firebase functions only accept javascript file index.js - * tsc + * tsc -p tools/screenshot-test/functions/tsconfig.json + * cd functions + * npm install * firebase deploy --only functions * * * Data and images handling for Screenshot test. * - * All users can post data to temporary folder. These Functions will check the data with JsonWebToken and - * move the valid data out of temporary folder. + * All users can post data to temporary folder. These Functions will check the data with + * JsonWebToken and move the valid data out of temporary folder. * * For valid data posted to database /$temp/screenshot/reports/$prNumber/$secureToken, move it to * /screenshot/reports/$prNumber. - * These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job information + * These are data for screenshot results (success or failure), GitHub PR/commit and TravisCI job + * information. * - * For valid image results written to database /$temp/screenshot/images/$prNumber/$secureToken/, save the image - * data to image files and upload to google cloud storage under location /screenshots/$prNumber - * These are screenshot test result images, and difference images generated from screenshot comparison. + * For valid image results written to database /$temp/screenshot/images/$prNumber/$secureToken/, + * save the image data to image files and upload to google cloud storage under + * location /screenshots/$prNumber + * These are screenshot test result images, and difference images generated from screenshot + * comparison. * - * For golden images uploaded to /goldens, read the data from images files and write the data to Firebase database - * under location /screenshot/goldens - * Screenshot tests can only read restricted database data with no credentials, and they cannot access - * Google Cloud Storage. Therefore we copy the image data to database to make it available to screenshot tests. + * For golden images uploaded to /goldens, read the data from images files and write the data to + * Firebase database under location /screenshot/goldens + * Screenshot tests can only read restricted database data with no credentials, and they cannot + * access. + * Google Cloud Storage. Therefore we copy the image data to database to make it available to + * screenshot tests. * - * The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to database/storage. + * The JWT is stored in the data path, so every write to database needs a valid JWT to be copied to + * database/storage. * All invalid data will be removed. * The JWT has 3 parts: header, payload and signature. These three parts are joint by '/' in path. */ @@ -65,11 +73,11 @@ const trustedReportPath = `screenshot/reports/{prNumber}`; * sha (github PR info), result (true or false for all the tests), travis job number */ const testDataPath = `${reportPath}/{dataType}`; -exports.testData = firebaseFunctions.database.ref(testDataPath) +export let testData = firebaseFunctions.database.ref(testDataPath) .onWrite((event: any) => { const dataType = event.params.dataType; if (dataTypes.includes(dataType)) { - return verifyJWTAndUpdateData(event, dataType); + return verifyJwtAndTransferResultToTrustedLocation(event, dataType); } }); @@ -79,9 +87,9 @@ exports.testData = firebaseFunctions.database.ref(testDataPath) * Data copied: test result for each file/test with ${filename}. The value should be true or false. */ const testResultsPath = `${reportPath}/results/{filename}`; -exports.testResults = firebaseFunctions.database.ref(testResultsPath) +export let testResults = firebaseFunctions.database.ref(testResultsPath) .onWrite((event: any) => { - return verifyJWTAndUpdateData(event, `results/${event.params.filename}`); + return verifyJwtAndTransferResultToTrustedLocation(event, `results/${event.params.filename}`); }); /** @@ -90,14 +98,14 @@ exports.testResults = firebaseFunctions.database.ref(testResultsPath) * Data copied: test result images. Convert from data to image files in storage. */ const imageDataToFilePath = `${imagePath}/{dataType}/{filename}`; -exports.imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath) +export let imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath) .onWrite(convertTestImageDataToFiles); /** * Copy valid goldens from storage /goldens/ to database /screenshot/goldens/ * so we can read the goldens without credentials. */ -exports.goldenImageToData = firebaseFunctions.storage.bucket( +export let goldenImageToData = firebaseFunctions.storage.bucket( firebaseFunctions.config().firebase.storageBucket).object().onChange((event: any) => { return convertGoldenImagesToData(event.data.name, event.data.resourceState, event.data.bucket); }); @@ -107,7 +115,8 @@ exports.goldenImageToData = firebaseFunctions.storage.bucket( * Copy images from /screenshot/$prNumber/test/ to /goldens/ */ const approveImagesPath = `${trustedReportPath}/approved`; -exports.approveImages = firebaseFunctions.database.ref(approveImagesPath).onWrite((event: any) => { +export let approveImages = firebaseFunctions.database.ref(approveImagesPath) + .onWrite((event: any) => { return copyTestImagesToGoldens(event.params.prNumber); }); @@ -117,4 +126,5 @@ exports.approveImages = firebaseFunctions.database.ref(approveImagesPath).onWrit * The Github Status Token is set in config.secret.github */ const githubStatusPath = `${trustedReportPath}/result/{sha}`; -exports.githubStatus = firebaseFunctions.database.ref(githubStatusPath).onWrite(updateGithubStatus); +export let githubStatus = firebaseFunctions.database.ref(githubStatusPath) + .onWrite(updateGithubStatus); diff --git a/functions/jwt_util.ts b/tools/screenshot-test/functions/jwt_util.ts similarity index 73% rename from functions/jwt_util.ts rename to tools/screenshot-test/functions/jwt_util.ts index c51fe3e76175..45fb4df042df 100644 --- a/functions/jwt_util.ts +++ b/tools/screenshot-test/functions/jwt_util.ts @@ -1,5 +1,5 @@ import * as firebaseFunctions from 'firebase-functions'; -import {verifySecureToken} from './util/jwt'; +import {verifyJWT} from './util/jwt'; /** The repo slug. This is used to validate the JWT is sent from correct repo. */ const repoSlug = firebaseFunctions.config().repo.slug; @@ -17,15 +17,15 @@ function getSecureToken(event: firebaseFunctions.Event) { }; /** - * Verify event params have correct JsonWebToken, and execute callback when the JWT is verified. - * Delete the data if there's an error or the callback is done + * Verify that the event has a valid JsonWebToken. If the token is *not* valid, + * the data tied to the event will be deleted and the function will return a rejected promise. */ -export function verifySecureTokenAndExecute(event: firebaseFunctions.Event) { +export function verifySecureToken(event: firebaseFunctions.Event) { return new Promise((resolve, reject) => { const prNumber = event.params.prNumber; const secureToken = getSecureToken(event); - return verifySecureToken(secureToken, prNumber, secret, repoSlug).then(() => { + return verifyJWT(secureToken, prNumber, secret, repoSlug).then(() => { resolve(); event.data.ref.parent.set(null); }).catch((error: any) => { diff --git a/functions/test_goldens.ts b/tools/screenshot-test/functions/test_goldens.ts similarity index 86% rename from functions/test_goldens.ts rename to tools/screenshot-test/functions/test_goldens.ts index 337d49c35413..dee3d316ea2c 100644 --- a/functions/test_goldens.ts +++ b/tools/screenshot-test/functions/test_goldens.ts @@ -22,15 +22,17 @@ export function copyTestImagesToGoldens(prNumber: string) { failedFilenames.push(childSnapshot.key); } counter++; - if (counter == snapshot.numChildren()) return true; + if (counter == snapshot.numChildren()) { + return true; + } }); return failedFilenames; }).then((failedFilenames: string[]) => { return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then(function (data: any) { return Promise.all(data[0] - .filter((file: any) => failedFilenames.includes(path.basename(file.name, '.screenshot.png'))) + .filter((file: any) => failedFilenames.includes( + path.basename(file.name, '.screenshot.png'))) .map((file: any) => file.copy(`goldens/${path.basename(file.name)}`))); }); - }) - + }); }; diff --git a/functions/tsconfig.json b/tools/screenshot-test/functions/tsconfig.json similarity index 58% rename from functions/tsconfig.json rename to tools/screenshot-test/functions/tsconfig.json index cb6ad1adf008..cf893515ca49 100644 --- a/functions/tsconfig.json +++ b/tools/screenshot-test/functions/tsconfig.json @@ -1,23 +1,19 @@ { "compilerOptions": { - "declaration": false, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, "lib": ["es6", "es2016", "dom"], "module": "commonjs", "moduleResolution": "node", "noEmitOnError": true, "noImplicitAny": true, - "outDir": "./", "sourceMap": true, "target": "es5", - "stripInternal": false, "baseUrl": "", + "outDir": "../../../functions/", "typeRoots": [ - "../node_modules/@types/!(node)" + "../../../functions/node_modules/@types/!(node)" ] }, "files": [ - "index.ts" + "./index.ts" ] } diff --git a/functions/util/github.ts b/tools/screenshot-test/functions/util/github.ts similarity index 89% rename from functions/util/github.ts rename to tools/screenshot-test/functions/util/github.ts index 7976cac8f705..d8d816395008 100644 --- a/functions/util/github.ts +++ b/tools/screenshot-test/functions/util/github.ts @@ -23,9 +23,9 @@ export function setGithubStatus(commitSHA: string, }); let headers = { - "Authorization": `token ${token}`, - "User-Agent": `${statusData.name}/1.0`, - "Content-Type": "application/json" + 'Authorization': `token ${token}`, + 'User-Agent': `${statusData.name}/1.0`, + 'Content-Type': 'application/json' }; return new Promise((resolve) => { diff --git a/functions/util/jwt.ts b/tools/screenshot-test/functions/util/jwt.ts similarity index 60% rename from functions/util/jwt.ts rename to tools/screenshot-test/functions/util/jwt.ts index 8e903a0022eb..d73b01817308 100644 --- a/functions/util/jwt.ts +++ b/tools/screenshot-test/functions/util/jwt.ts @@ -1,11 +1,11 @@ import * as jwt from 'jsonwebtoken'; -export function verifySecureToken(token: string, - prNumber: string, - secret: string, - repoSlug: string) { +export function verifyJWT(token: string, + prNumber: string, + secret: string, + repoSlug: string) { return new Promise((resolve, reject) => { - jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => { + jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err: any, payload: any) => { if (err) { reject(err.message || err); } else if (payload.slug !== repoSlug) { diff --git a/tools/screenshot-test/functions/util/util.ts b/tools/screenshot-test/functions/util/util.ts new file mode 100644 index 000000000000..4ca369275fdd --- /dev/null +++ b/tools/screenshot-test/functions/util/util.ts @@ -0,0 +1,11 @@ +export function isEditEvent(event: any) { + return event.data.previous.exists() && event.data.exists(); +} + +export function isDeleteEvent(event: any) { + return event.data.previous.exists() && !event.data.exists(); +} + +export function isCreateEvent(event: any) { + return !event.data.previous.exists() && event.data.exists(); +} diff --git a/tools/screenshot-test/functions/verify-and-copy-report.ts b/tools/screenshot-test/functions/verify-and-copy-report.ts new file mode 100644 index 000000000000..126da12bf9a6 --- /dev/null +++ b/tools/screenshot-test/functions/verify-and-copy-report.ts @@ -0,0 +1,22 @@ +import * as firebaseAdmin from 'firebase-admin'; +import {verifySecureToken} from './jwt_util'; +import {isCreateEvent} from './util/util'; + +/** + * Verifies that a screenshot report is valid (trusted via JWT) and, if so, copies it from the + * temporary, unauthenticated location to the more permanent, trusted location. + */ +export function verifyJwtAndTransferResultToTrustedLocation(event: any, path: string) { + // Only edit data when it is first created. Exit when the data is deleted. + if (!isCreateEvent(event)) { + return; + } + + let prNumber = event.params.prNumber; + let data = event.data.val(); + + return verifySecureToken(event).then(() => { + return firebaseAdmin.database().ref().child('screenshot/reports') + .child(prNumber).child(path).set(data); + }); +}; From 5352b28cd4f9c389795c1ac3d1a127b3b75b3dab Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 10 Apr 2017 11:16:08 -0700 Subject: [PATCH 5/7] Address comments --- tools/gulp/tasks/coverage.ts | 4 ++-- tools/gulp/tasks/payload.ts | 4 ++-- tools/gulp/tasks/screenshots.ts | 6 +++--- tools/gulp/util/travis-ci.ts | 2 +- .../functions/{data_image.ts => data-image.ts} | 7 ++++--- .../functions/{image_data.ts => image-data.ts} | 4 ++-- tools/screenshot-test/functions/index.ts | 10 +++++----- .../functions/{jwt_util.ts => jwt-util.ts} | 0 .../functions/{test_goldens.ts => test-goldens.ts} | 2 +- .../functions/verify-and-copy-report.ts | 2 +- 10 files changed, 21 insertions(+), 20 deletions(-) rename tools/screenshot-test/functions/{data_image.ts => data-image.ts} (84%) rename tools/screenshot-test/functions/{image_data.ts => image-data.ts} (89%) rename tools/screenshot-test/functions/{jwt_util.ts => jwt-util.ts} (100%) rename tools/screenshot-test/functions/{test_goldens.ts => test-goldens.ts} (97%) diff --git a/tools/gulp/tasks/coverage.ts b/tools/gulp/tasks/coverage.ts index 690ae631d506..a7832db58a62 100644 --- a/tools/gulp/tasks/coverage.ts +++ b/tools/gulp/tasks/coverage.ts @@ -2,7 +2,7 @@ import {task} from 'gulp'; import {existsSync} from 'fs-extra'; import {COVERAGE_RESULT_FILE} from '../constants'; import {spawnSync} from 'child_process'; -import {isTravisPushBuild} from '../util/travis-ci'; +import {isTravisMasterBuild} from '../util/travis-ci'; import {openFirebaseDashboardDatabase} from '../util/firebase'; task('coverage:upload', () => { @@ -10,7 +10,7 @@ task('coverage:upload', () => { throw new Error('No coverage file has been found!'); } - if (!isTravisPushBuild()) { + if (!isTravisMasterBuild()) { throw new Error('Coverage results will be only uploaded inside of Travis Push builds.'); } diff --git a/tools/gulp/tasks/payload.ts b/tools/gulp/tasks/payload.ts index a7657aaa9dd3..69c4496b2931 100644 --- a/tools/gulp/tasks/payload.ts +++ b/tools/gulp/tasks/payload.ts @@ -3,7 +3,7 @@ import {join} from 'path'; import {statSync} from 'fs'; import {DIST_ROOT} from '../constants'; import {spawnSync} from 'child_process'; -import {isTravisPushBuild} from '../util/travis-ci'; +import {isTravisMasterBuild} from '../util/travis-ci'; import {openFirebaseDashboardDatabase} from '../util/firebase'; const bundlesDir = join(DIST_ROOT, 'bundles'); @@ -23,7 +23,7 @@ task('payload', ['library:clean-build'], () => { console.log('Payload Results:', JSON.stringify(results, null, 2)); // Publish the results to firebase when it runs on Travis and not as a PR. - if (isTravisPushBuild()) { + if (isTravisMasterBuild()) { return publishResults(results); } diff --git a/tools/gulp/tasks/screenshots.ts b/tools/gulp/tasks/screenshots.ts index 902d0bd95a72..9b8152525aff 100644 --- a/tools/gulp/tasks/screenshots.ts +++ b/tools/gulp/tasks/screenshots.ts @@ -6,7 +6,7 @@ import * as firebase from 'firebase'; import { openScreenshotsBucket, connectFirebaseScreenshots} from '../util/firebase'; -import {isTravisPushBuild} from '../util/travis-ci'; +import {isTravisMasterBuild} from '../util/travis-ci'; const imageDiff = require('image-diff'); @@ -27,8 +27,8 @@ const FIREBASE_STORAGE_GOLDENS = 'goldens'; task('screenshots', () => { let prNumber = process.env['TRAVIS_PULL_REQUEST']; - if (isTravisPushBuild()) { - // Only update goldens for build + if (isTravisMasterBuild()) { + // Only update goldens for master build return uploadScreenshots(); } else if (prNumber) { let firebaseApp = connectFirebaseScreenshots(); diff --git a/tools/gulp/util/travis-ci.ts b/tools/gulp/util/travis-ci.ts index 3d6524ac05a7..bd8e328b29c8 100644 --- a/tools/gulp/util/travis-ci.ts +++ b/tools/gulp/util/travis-ci.ts @@ -1,4 +1,4 @@ /** Whether gulp currently runs inside of Travis as a push. */ -export function isTravisPushBuild() { +export function isTravisMasterBuild() { return process.env['TRAVIS_PULL_REQUEST'] === 'false'; } diff --git a/tools/screenshot-test/functions/data_image.ts b/tools/screenshot-test/functions/data-image.ts similarity index 84% rename from tools/screenshot-test/functions/data_image.ts rename to tools/screenshot-test/functions/data-image.ts index 7b6b034b830a..9127fdd59084 100644 --- a/tools/screenshot-test/functions/data_image.ts +++ b/tools/screenshot-test/functions/data-image.ts @@ -1,6 +1,6 @@ import * as firebaseFunctions from 'firebase-functions'; import {writeFileSync} from 'fs'; -import {verifySecureToken} from './jwt_util'; +import {verifySecureToken} from './jwt-util'; import {isCreateEvent} from './util/util'; const gcs = require('@google-cloud/storage')(); @@ -9,11 +9,12 @@ const gcs = require('@google-cloud/storage')(); const bucket = gcs.bucket(firebaseFunctions.config().firebase.storageBucket); /** - * Convert data to images. Image data posted to database will be saved as png files + * Writes base-64 encoded test images to png files on the filesystem. + * Image data posted to database will be saved as png files * and uploaded to screenshot/$prNumber/dataType/$filename * Convert BufferArray to .png image file */ -export function convertTestImageDataToFiles(event: any) { +export function writeTestImagesToFiles(event: any) { // Only edit data when it is first created. Exit when the data is deleted. if (!isCreateEvent(event)) { return; diff --git a/tools/screenshot-test/functions/image_data.ts b/tools/screenshot-test/functions/image-data.ts similarity index 89% rename from tools/screenshot-test/functions/image_data.ts rename to tools/screenshot-test/functions/image-data.ts index 99f5c6dce26f..96eba0452faa 100644 --- a/tools/screenshot-test/functions/image_data.ts +++ b/tools/screenshot-test/functions/image-data.ts @@ -9,10 +9,10 @@ const FIREBASE_DATA_GOLDENS = 'screenshot/goldens'; /** * Read golden files under /goldens/ and store the image data to - * database /screenshot/goldens/$filename + * database /screenshot/goldens/$filename as base-64 encoded string * Convert png image files to BufferArray data */ -export function convertGoldenImagesToData(name: string, resourceState: string, fileBucket: any) { +export function copyGoldImagesToDatabase(name: string, resourceState: string, fileBucket: any) { // The name should always look like "goldens/xxx.png" let parsedPath = path.parse(name); // Get the file name. diff --git a/tools/screenshot-test/functions/index.ts b/tools/screenshot-test/functions/index.ts index 8f95dcde89e6..977412e99af6 100644 --- a/tools/screenshot-test/functions/index.ts +++ b/tools/screenshot-test/functions/index.ts @@ -4,9 +4,9 @@ import * as firebaseFunctions from 'firebase-functions'; import * as firebaseAdmin from 'firebase-admin'; import {verifyJwtAndTransferResultToTrustedLocation} from './verify-and-copy-report'; -import {convertGoldenImagesToData} from './image_data'; -import {convertTestImageDataToFiles} from './data_image'; -import {copyTestImagesToGoldens} from './test_goldens'; +import {copyGoldImagesToDatabase} from './image-data'; +import {writeTestImagesToFiles} from './data-image'; +import {copyTestImagesToGoldens} from './test-goldens'; import {updateGithubStatus} from './github'; /** @@ -99,7 +99,7 @@ export let testResults = firebaseFunctions.database.ref(testResultsPath) */ const imageDataToFilePath = `${imagePath}/{dataType}/{filename}`; export let imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath) - .onWrite(convertTestImageDataToFiles); + .onWrite(writeTestImagesToFiles); /** * Copy valid goldens from storage /goldens/ to database /screenshot/goldens/ @@ -107,7 +107,7 @@ export let imageDataToFile = firebaseFunctions.database.ref(imageDataToFilePath) */ export let goldenImageToData = firebaseFunctions.storage.bucket( firebaseFunctions.config().firebase.storageBucket).object().onChange((event: any) => { - return convertGoldenImagesToData(event.data.name, event.data.resourceState, event.data.bucket); + return copyGoldImagesToDatabase(event.data.name, event.data.resourceState, event.data.bucket); }); /** diff --git a/tools/screenshot-test/functions/jwt_util.ts b/tools/screenshot-test/functions/jwt-util.ts similarity index 100% rename from tools/screenshot-test/functions/jwt_util.ts rename to tools/screenshot-test/functions/jwt-util.ts diff --git a/tools/screenshot-test/functions/test_goldens.ts b/tools/screenshot-test/functions/test-goldens.ts similarity index 97% rename from tools/screenshot-test/functions/test_goldens.ts rename to tools/screenshot-test/functions/test-goldens.ts index dee3d316ea2c..0ed975e74d77 100644 --- a/tools/screenshot-test/functions/test_goldens.ts +++ b/tools/screenshot-test/functions/test-goldens.ts @@ -28,7 +28,7 @@ export function copyTestImagesToGoldens(prNumber: string) { }); return failedFilenames; }).then((failedFilenames: string[]) => { - return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then(function (data: any) { + return bucket.getFiles({prefix: `screenshots/${prNumber}/test`}).then((data: any) => { return Promise.all(data[0] .filter((file: any) => failedFilenames.includes( path.basename(file.name, '.screenshot.png'))) diff --git a/tools/screenshot-test/functions/verify-and-copy-report.ts b/tools/screenshot-test/functions/verify-and-copy-report.ts index 126da12bf9a6..36cccb04769f 100644 --- a/tools/screenshot-test/functions/verify-and-copy-report.ts +++ b/tools/screenshot-test/functions/verify-and-copy-report.ts @@ -1,5 +1,5 @@ import * as firebaseAdmin from 'firebase-admin'; -import {verifySecureToken} from './jwt_util'; +import {verifySecureToken} from './jwt-util'; import {isCreateEvent} from './util/util'; /** From 45dd4310d759d4777af2eb70ad3d7a98ec5fa9d2 Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 10 Apr 2017 11:29:01 -0700 Subject: [PATCH 6/7] fix tslint --- tools/screenshot-test/functions/data-image.ts | 2 +- tools/screenshot-test/functions/image-data.ts | 2 +- tools/screenshot-test/functions/jwt-util.ts | 4 ++-- tools/screenshot-test/functions/test-goldens.ts | 2 +- tools/screenshot-test/functions/util/github.ts | 2 +- tools/screenshot-test/functions/util/jwt.ts | 2 +- tools/screenshot-test/functions/verify-and-copy-report.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tools/screenshot-test/functions/data-image.ts b/tools/screenshot-test/functions/data-image.ts index 9127fdd59084..4a120615d9f7 100644 --- a/tools/screenshot-test/functions/data-image.ts +++ b/tools/screenshot-test/functions/data-image.ts @@ -37,4 +37,4 @@ export function writeTestImagesToFiles(event: any) { writeFileSync(tempPath, binaryData, 'binary'); return bucket.upload(tempPath, {destination: filePath}); }); -}; +} diff --git a/tools/screenshot-test/functions/image-data.ts b/tools/screenshot-test/functions/image-data.ts index 96eba0452faa..b0adf05d6d25 100644 --- a/tools/screenshot-test/functions/image-data.ts +++ b/tools/screenshot-test/functions/image-data.ts @@ -38,4 +38,4 @@ export function copyGoldImagesToDatabase(name: string, resourceState: string, fi let data = readFileSync(tempFilePath); return databaseRef.set(data); }).catch((error: any) => console.error(`${filenameKey} ${error}`)); -}; +} diff --git a/tools/screenshot-test/functions/jwt-util.ts b/tools/screenshot-test/functions/jwt-util.ts index 45fb4df042df..73571275089e 100644 --- a/tools/screenshot-test/functions/jwt-util.ts +++ b/tools/screenshot-test/functions/jwt-util.ts @@ -14,7 +14,7 @@ const secret = firebaseFunctions.config().secret.key; */ function getSecureToken(event: firebaseFunctions.Event) { return `${event.params.jwtHeader}.${event.params.jwtPayload}.${event.params.jwtSignature}`; -}; +} /** * Verify that the event has a valid JsonWebToken. If the token is *not* valid, @@ -34,4 +34,4 @@ export function verifySecureToken(event: firebaseFunctions.Event) { reject(); }); }); -}; +} diff --git a/tools/screenshot-test/functions/test-goldens.ts b/tools/screenshot-test/functions/test-goldens.ts index 0ed975e74d77..fdb384d8654c 100644 --- a/tools/screenshot-test/functions/test-goldens.ts +++ b/tools/screenshot-test/functions/test-goldens.ts @@ -35,4 +35,4 @@ export function copyTestImagesToGoldens(prNumber: string) { .map((file: any) => file.copy(`goldens/${path.basename(file.name)}`))); }); }); -}; +} diff --git a/tools/screenshot-test/functions/util/github.ts b/tools/screenshot-test/functions/util/github.ts index d8d816395008..405be48a45ab 100644 --- a/tools/screenshot-test/functions/util/github.ts +++ b/tools/screenshot-test/functions/util/github.ts @@ -39,4 +39,4 @@ export function setGithubStatus(commitSHA: string, resolve(response.statusCode); }); }); -}; +} diff --git a/tools/screenshot-test/functions/util/jwt.ts b/tools/screenshot-test/functions/util/jwt.ts index d73b01817308..04b43562532d 100644 --- a/tools/screenshot-test/functions/util/jwt.ts +++ b/tools/screenshot-test/functions/util/jwt.ts @@ -17,4 +17,4 @@ export function verifyJWT(token: string, } }); }); -}; +} diff --git a/tools/screenshot-test/functions/verify-and-copy-report.ts b/tools/screenshot-test/functions/verify-and-copy-report.ts index 36cccb04769f..0b1c65aa3d51 100644 --- a/tools/screenshot-test/functions/verify-and-copy-report.ts +++ b/tools/screenshot-test/functions/verify-and-copy-report.ts @@ -19,4 +19,4 @@ export function verifyJwtAndTransferResultToTrustedLocation(event: any, path: st return firebaseAdmin.database().ref().child('screenshot/reports') .child(prNumber).child(path).set(data); }); -}; +} From c2e8cbc72f6937b4815874ee68a81afb74b4cb0a Mon Sep 17 00:00:00 2001 From: Yuan Gao Date: Mon, 10 Apr 2017 16:59:22 -0700 Subject: [PATCH 7/7] try to fix closure compiler --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c9d8972ec83d..388f8c3440e0 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "ts-node": "^3.0.0", "tslint": "^5.0.0", "tslint-no-unused-var": "0.0.6", - "typescript": "~2.1.1", + "typescript": "~2.2.1", "uglify-js": "^2.8.14", "web-animations-js": "^2.2.2" }