Skip to content

Commit 5085489

Browse files
committed
Add firebase functions. Change screenshot to write to temp files.
1 parent 05c865d commit 5085489

File tree

9 files changed

+263
-49
lines changed

9 files changed

+263
-49
lines changed

.travis.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ sudo: false
44
node_js:
55
- '6.9.4'
66

7+
addons:
8+
jwt:
9+
# SAUCE_ACCESS_KEY<=secret for FIREBASE_ACCESS_TOKEN to work around travis-ci/travis-ci#7223, unencrypted value in valentine as FIREBASE_ACCESS_TOKEN>
10+
# we alias FIREBASE_ACCESS_TOKEN to $SAUCE_ACCESS_KEY in env.sh and set the SAUCE_ACCESS_KEY there
11+
- secure: "PKts/IbxuJRWWOEeiGbl8Z9zds0M+hIdCH/g/E4WbQ9yzSvSbdwzfmRfFccQFjxjsrY7+SJMVjsURZy+xUyBpzqgWYHUItnSVqjZb8DlyAU2IXyg8TM9BVLkGGe6k5k4PIFVmfMMMzQwWMM0X0W9w3oYmfHL5egxwSHvf9HIqLolLNXg8sqamIdS5d5KoCXf1c+oRjN/IMBktzNBR6N4OFOZQXVoepXNiIvTWAcTtOPBvFWdKP2n7RVioHKdm4a85aCUpDJp+LYGaLqiQZoRzmzfVTnAhTAPdd4ao5w/+jojrfZIHV55bqYF9rLnQMTneKsiyVNVYJzOLuxmARa/EEKfZld+J3rX4/o4cogrU38YSZF+T7J9g/7CTsnIZ3F6W6m+8iJbIBh55nGOQi5PVe458Q/nGb3fgQd2Z4+6lK9k479H4Ssh/Y7hbVQbepqEVIXzZKqWX6/ZE4iWoR/Q2dm0hySFmmB/R2etixX5JxhnHvgobTYIQ+1liJVp/3YFW1ru64Yg6yz/V291Bhh9g31znmTROCJ/usAmZZaLUqW1TDKnLIMP+M74MF9XERqcWKywXRFwxP4E5uDnx/vAyN49gL+SDfrBUxUtXrTkKZAlglwo9SgA7cOYEPWrionvKcGm87gCBYHFUmXZNQVzh212fpuJYXb/vy0sPDj8La4="
12+
713
branches:
814
only:
915
- master
@@ -12,11 +18,11 @@ env:
1218
global:
1319
- LOGS_DIR=/tmp/angular-material2-build/logs
1420
- SAUCE_USERNAME=angular-ci
15-
- SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
1621
- BROWSER_STACK_USERNAME=angularteam1
1722
- BROWSER_STACK_ACCESS_KEY=BWCd4SynLzdDcv8xtzsB
1823
- BROWSER_PROVIDER_READY_FILE=/tmp/angular-material2-build/readyfile
1924
- BROWSER_PROVIDER_ERROR_FILE=/tmp/angular-material2-build/errorfile
25+
2026
matrix:
2127
# Order: a slower build first, so that we don't occupy an idle travis worker waiting for others to complete.
2228
- MODE=lint
@@ -32,6 +38,9 @@ matrix:
3238
- env: "MODE=saucelabs_optional"
3339
- env: "MODE=browserstack_optional"
3440

41+
before_install:
42+
- source ./scripts/ci/env.sh
43+
3544
install:
3645
- npm install
3746

functions/config.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"firebase": {
3+
"apiKey": "AIzaSyBekh5ZSi1vEhaE2qetH4RU91gHmUmpqgg",
4+
"authDomain": "material2-screenshots.firebaseapp.com",
5+
"databaseURL": "https://material2-screenshots.firebaseio.com",
6+
"storageBucket": "material2-screenshots.appspot.com",
7+
"messagingSenderId": "975527407245"
8+
}
9+
}

functions/index.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use strict';
2+
3+
const functions = require('firebase-functions');
4+
const gcs = require('@google-cloud/storage')();
5+
const admin = require('firebase-admin');
6+
const jwt = require('jsonwebtoken');
7+
const fs = require('fs');
8+
9+
admin.initializeApp(functions.config().firebase);
10+
11+
const dataTypes = ['filenames', 'commit', 'result', 'sha', 'travis'];
12+
const repoSlug = functions.config().repo.slug;
13+
const secret = functions.config().secret.key;
14+
const bucket = gcs.bucket(functions.config().firebase.storageBucket);
15+
16+
/** Copy valid data from /temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber */
17+
exports.copyData = functions.database.ref('/temp/screenshot/reports/{prNumber}/{token1}/{token2}/{token3}/{dataType}')
18+
.onWrite(event => {
19+
const dataType = event.params.dataType;
20+
if (dataTypes.indexOf(dataType) == -1) {
21+
return;
22+
}
23+
return handleDataChange(event, dataType);
24+
});
25+
26+
/** Copy valid data from /temp/screenshot/reports/$prNumber/$secureToken/ to /screenshot/reports/$prNumber */
27+
exports.copyDataResult = functions.database.ref('/temp/screenshot/reports/{prNumber}/{token1}/{token2}/{token3}/results/{filename}')
28+
.onWrite(event => {
29+
return handleDataChange(event, `results/${event.params.filename}`);
30+
});
31+
32+
/** Copy valid data from database /temp/screenshot/images/$prNumber/$secureToken/ to storage /screenshots/$prNumber */
33+
exports.copyImage = functions.database.ref('/temp/screenshot/images/{prNumber}/{token1}/{token2}/{token3}/{dataType}/{filename}')
34+
.onWrite(event => {
35+
// Only edit data when it is first created. Exit when the data is deleted.
36+
if (event.data.previous.exists() || !event.data.exists()) {
37+
return;
38+
}
39+
40+
const dataType = event.params.dataType;
41+
const prNumber = event.params.prNumber;
42+
const secureToken = `${event.params.token1}.${event.params.token2}.${event.params.token3}`;
43+
const saveFilename = `${event.params.filename}.screenshot.png`;
44+
45+
if (dataType != 'diff' && dataType != 'test') {
46+
return;
47+
}
48+
49+
return validateSecureToken(secureToken, prNumber).then((payload) => {
50+
const tempPath = `/tmp/${dataType}-${saveFilename}`
51+
const filePath = `screenshots/${prNumber}/${dataType}/${saveFilename}`;
52+
const binaryData = new Buffer(event.data.val(), 'base64').toString('binary');
53+
fs.writeFile(tempPath, binaryData, 'binary');
54+
return bucket.upload(tempPath, {
55+
destination: filePath
56+
}).then(() => {
57+
return event.data.ref.parent.set(null);
58+
});
59+
}).catch((error) => {
60+
console.error(`Invalid secure token ${secureToken} ${error}`);
61+
return event.data.ref.parent.set(null);
62+
});
63+
});
64+
65+
/**
66+
* Copy valid goldens from storage /goldens/ to database /screenshot/goldens/
67+
* so we can read the goldens without credentials
68+
*/
69+
exports.copyGoldens = functions.storage.bucket(functions.config().firebase.storageBucket).object().onChange(event => {
70+
const filePath = event.data.name;
71+
72+
// Get the file name.
73+
const fileNames = filePath.split('/');
74+
if (fileNames.length != 2 && fileNames[0] != 'goldens') {
75+
return;
76+
}
77+
const filenameKey = fileNames[1].replace('.screenshot.png', '');
78+
79+
if (event.data.resourceState === 'not_exists') {
80+
return admin.database().ref(`screenshot/goldens/${filenameKey}`).set(null);
81+
}
82+
83+
// Download file from bucket.
84+
const bucket = gcs.bucket(event.data.bucket);
85+
const tempFilePath = `/tmp/${fileNames[1]}`;
86+
return bucket.file(filePath).download({
87+
destination: tempFilePath
88+
}).then(() => {
89+
const data = fs.readFileSync(tempFilePath);
90+
return admin.database().ref(`screenshot/goldens/${filenameKey}`).set(data);
91+
});
92+
});
93+
94+
function handleDataChange(event, path) {
95+
// Only edit data when it is first created. Exit when the data is deleted.
96+
if (event.data.previous.exists() || !event.data.exists()) {
97+
return;
98+
}
99+
100+
const prNumber = event.params.prNumber;
101+
const secureToken = `${event.params.token1}.${event.params.token2}.${event.params.token3}`;
102+
const original = event.data.val();
103+
104+
return validateSecureToken(secureToken, prNumber).then((payload) => {
105+
return admin.database().ref().child('screenshot/reports').child(prNumber).child(path).set(original).then(() => {
106+
return event.data.ref.parent.set(null);
107+
});
108+
}).catch((error) => {
109+
console.error(`Invalid secure token ${secureToken} ${error}`);
110+
return event.data.ref.parent.set(null);
111+
});
112+
}
113+
114+
function validateSecureToken(token, prNumber) {
115+
return new Promise((resolve, reject) => {
116+
jwt.verify(token, secret, {issuer: 'Travis CI, GmbH'}, (err, payload) => {
117+
if (err) {
118+
reject(err.message || err);
119+
} else if (payload.slug !== repoSlug) {
120+
reject(`jwt slug invalid. expected: ${repoSlug}`);
121+
} else if (payload['pull-request'].toString() !== prNumber) {
122+
reject(`jwt pull-request invalid. expected: ${prNumber} actual: ${payload['pull-request']}`);
123+
} else {
124+
resolve(payload);
125+
}
126+
});
127+
});
128+
}

functions/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "angular-material2-functions",
3+
"description": "Angular Material2 screenshot test functions",
4+
"dependencies": {
5+
"@google-cloud/storage": "^0.8.0",
6+
"firebase-admin": "^4.1.3",
7+
"firebase-functions": "^0.5.2",
8+
"jsonwebtoken": "^7.3.0"
9+
}
10+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"dgeni-packages": "^0.16.5",
5959
"firebase-admin": "^4.1.2",
6060
"firebase-tools": "^2.2.1",
61+
"firebase": "^3.7.2",
6162
"fs-extra": "^2.0.0",
6263
"glob": "^7.1.1",
6364
"google-cloud": "^0.48.0",

scripts/ci/env.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env bash
2+
3+
4+
if [[ ${TRAVIS:-} ]]; then
5+
# If FIREBASE_ACCESS_TOKEN not set yet, export the FIREBASE_ACCESS_TOKEN using the JWT token that Travis generated and exported for SAUCE_ACCESS_KEY.
6+
# This is a workaround for travis-ci/travis-ci#7223
7+
# WARNING: FIREBASE_ACCESS_TOKEN should NOT be printed
8+
export FIREBASE_ACCESS_TOKEN=${FIREBASE_ACCESS_TOKEN:-$SAUCE_ACCESS_KEY}
9+
10+
# - we overwrite the value set by Travis JWT addon here to work around travis-ci/travis-ci#7223 for FIREBASE_ACCESS_TOKEN
11+
export SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
12+
fi

tools/gulp/tasks/e2e.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ task('e2e', (done: (err?: string) => void) => {
6161
'serve:e2eapp',
6262
':test:protractor',
6363
':serve:e2eapp:stop',
64+
'screenshots',
6465
(err: any) => done(err)
6566
);
6667
});

0 commit comments

Comments
 (0)