From 807c5971c644533c2fd5a7070b490e0da8ee24bc Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Thu, 25 May 2017 22:58:06 +0200 Subject: [PATCH 1/2] build: add dashboard firebase functions * Adds a new folder for the dashboard project that for now only contains firebase functions. * The firebase functions will be used to securely make API calls to the Github API (e.g payload status) * Functions will be automatically deployed as same as screenshot functions. --- scripts/ci/publish-artifacts.sh | 9 ++-- scripts/deploy/deploy-dashboard-functions.sh | 24 +++++++++ .../deploy-screenshot-functions.sh | 0 .../publish-build-artifacts.sh | 0 .../publish-docs-content.sh | 0 tools/dashboard/firebase.json | 11 ++++ .../functions/dashboard-functions.ts | 51 +++++++++++++++++++ tools/dashboard/functions/github-status.ts | 38 ++++++++++++++ tools/dashboard/functions/index.js | 20 ++++++++ tools/dashboard/functions/package.json | 14 +++++ tools/dashboard/functions/tsconfig.json | 16 ++++++ 11 files changed, 179 insertions(+), 4 deletions(-) create mode 100755 scripts/deploy/deploy-dashboard-functions.sh rename scripts/{release => deploy}/deploy-screenshot-functions.sh (100%) rename scripts/{release => deploy}/publish-build-artifacts.sh (100%) rename scripts/{release => deploy}/publish-docs-content.sh (100%) create mode 100644 tools/dashboard/firebase.json create mode 100644 tools/dashboard/functions/dashboard-functions.ts create mode 100644 tools/dashboard/functions/github-status.ts create mode 100644 tools/dashboard/functions/index.js create mode 100644 tools/dashboard/functions/package.json create mode 100644 tools/dashboard/functions/tsconfig.json diff --git a/scripts/ci/publish-artifacts.sh b/scripts/ci/publish-artifacts.sh index a3e2be1d1fbf..56fe671737ff 100755 --- a/scripts/ci/publish-artifacts.sh +++ b/scripts/ci/publish-artifacts.sh @@ -22,10 +22,11 @@ $(npm bin)/gulp docs # Run publishing of artifacts in parallel. # This is possible because the output has been built before. -./scripts/release/publish-build-artifacts.sh --no-build & -./scripts/release/publish-docs-content.sh --no-build & +./scripts/deploy/publish-build-artifacts.sh --no-build & +./scripts/deploy/publish-docs-content.sh --no-build & -# Deploy the screenshot functions for each push build. -./scripts/release/deploy-screenshot-functions.sh & +# Deploy the screenshot and dashboard functions for each push build. +./scripts/deploy/deploy-screenshot-functions.sh & +./scripts/deploy/deploy-dashboard-functions.sh & wait diff --git a/scripts/deploy/deploy-dashboard-functions.sh b/scripts/deploy/deploy-dashboard-functions.sh new file mode 100755 index 000000000000..9272f4de729d --- /dev/null +++ b/scripts/deploy/deploy-dashboard-functions.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Go to the project root directory +cd $(dirname ${0})/../.. + +# Paths to the dashboard and functions directories. +dashboardFolder=tools/dashboard + +# Go to the dashboard folder because otherwise the Firebase CLI tries to deploy the wrong project. +cd ${dashboardFolder} + +# Install node modules for dashboard functions. Firebase CLI needs to execute the functions +# before it can collect all functions and deploy them. +(cd functions; npm install) + +if [ -z ${MATERIAL2_DASHBOARD_ACCESS_TOKEN} ]; then + echo "Error: No access token for firebase specified." \ + "Please set the environment variable 'MATERIAL2_DASHBOARD_ACCESS_TOKEN'." + exit 1 +fi + +# Deploy the dashboard functions to Firebase. For now only the functions will be deployed. +$(npm bin)/firebase deploy \ + --only functions --token ${MATERIAL2_DASHBOARD_ACCESS_TOKEN} --project material2-dashboard diff --git a/scripts/release/deploy-screenshot-functions.sh b/scripts/deploy/deploy-screenshot-functions.sh similarity index 100% rename from scripts/release/deploy-screenshot-functions.sh rename to scripts/deploy/deploy-screenshot-functions.sh diff --git a/scripts/release/publish-build-artifacts.sh b/scripts/deploy/publish-build-artifacts.sh similarity index 100% rename from scripts/release/publish-build-artifacts.sh rename to scripts/deploy/publish-build-artifacts.sh diff --git a/scripts/release/publish-docs-content.sh b/scripts/deploy/publish-docs-content.sh similarity index 100% rename from scripts/release/publish-docs-content.sh rename to scripts/deploy/publish-docs-content.sh diff --git a/tools/dashboard/firebase.json b/tools/dashboard/firebase.json new file mode 100644 index 000000000000..6a453cfa5793 --- /dev/null +++ b/tools/dashboard/firebase.json @@ -0,0 +1,11 @@ +{ + "hosting": { + "public": "dist", + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/tools/dashboard/functions/dashboard-functions.ts b/tools/dashboard/functions/dashboard-functions.ts new file mode 100644 index 000000000000..f2055f60fc5d --- /dev/null +++ b/tools/dashboard/functions/dashboard-functions.ts @@ -0,0 +1,51 @@ +import {https, config} from 'firebase-functions'; +import {verify} from 'jsonwebtoken'; +import {setGithubStatus} from './github-status'; + +/** The repo slug. This is used to validate the JWT is sent from correct repo. */ +const repoSlug = config().repoSlug; + +/** API token for the Github repository. Required to set the github status on commits and PRs. */ +const repoToken = config().repoToken; + +/** The JWT secret. This is used to validate JWT. */ +const jwtSecret = config().jwtSecret; + +export const payloadGithubStatus = https.onRequest(async (request, response) => { + const authToken = request.header('auth-token'); + const commitSha = request.header('commit-sha'); + const payloadDiff = parseInt(request.header('commit-payload-diff')); + + if (!verifyToken(authToken)) { + return response.status(403).json({message: 'Auth token is not valid'}); + } + + if (!commitSha) { + return response.status(404).json({message: 'No commit has been specified'}); + } + + if (!payloadDiff || isNaN(payloadDiff)) { + return response.status(400).json({message: 'No valid payload diff has been specified.'}); + } + + await setGithubStatus(commitSha, repoToken, { + result: true, + name: 'Library Payload', + url: `https://travis-ci.org/angular/material2/jobs/${process.env['TRAVIS_JOB_ID']}`, + description: `${payloadDiff > 0 ? `+` : ''} ${payloadDiff.toFixed(2)}KB` + }); + + response.json({message: 'Payload Github status successfully set.'}); +}); + +function verifyToken(token: string): boolean { + try { + const tokenPayload = verify(token, jwtSecret, {issuer: 'Travis CI, GmbH'}); + if (tokenPayload.slug !== repoSlug) { + console.log(`JWT slugs are not matching. Expected ${repoSlug}`); + } + return true; + } catch (e) { + return false; + } +} diff --git a/tools/dashboard/functions/github-status.ts b/tools/dashboard/functions/github-status.ts new file mode 100644 index 000000000000..cc0e6a0ed5fa --- /dev/null +++ b/tools/dashboard/functions/github-status.ts @@ -0,0 +1,38 @@ +const request = require('request'); +const {version, name} = require('./package.json'); + +/** 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, authToken: string, data: GithubStatusData) { + const state = data.result ? 'success' : 'failure'; + + const requestData = JSON.stringify({ + state: state, + target_url: data.url, + context: data.name, + description: data.description + }); + + const headers = { + 'Authorization': `token ${authToken}`, + 'User-Agent': `${name}/${version}`, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(requestData) + }; + + return new Promise((resolve) => { + request({ + url: `https://api.github.com/repos/angular/material2/statuses/${commitSHA}`, + method: 'POST', + form: requestData, + headers: headers + }, (error: any, response: any) => resolve(response.statusCode)); + }); +} diff --git a/tools/dashboard/functions/index.js b/tools/dashboard/functions/index.js new file mode 100644 index 000000000000..d089e8313d4d --- /dev/null +++ b/tools/dashboard/functions/index.js @@ -0,0 +1,20 @@ +/** + * Entry point for the Firebase functions of the dashboard app. Firebase functions only support + * JavaScript files and therefore the TypeScript files needs to be transpiled. + */ + +'use strict'; + +const path = require('path'); + +// Enable TypeScript compilation at runtime using ts-node. +require('ts-node').register({ + project: path.join(__dirname, 'tsconfig.json') +}); + +const functionExports = require('./dashboard-functions'); + +// Re-export every firebase function from TypeScript +Object.keys(functionExports).forEach(fnName => { + module.exports[fnName] = functionExports[fnName]; +}); diff --git a/tools/dashboard/functions/package.json b/tools/dashboard/functions/package.json new file mode 100644 index 000000000000..bf5530e38ebe --- /dev/null +++ b/tools/dashboard/functions/package.json @@ -0,0 +1,14 @@ +{ + "name": "material2-dashboard-functions", + "version": "0.0.1", + "main": "index.js", + "dependencies": { + "@types/jsonwebtoken": "^7.2.0", + "firebase-admin": "~4.2.1", + "firebase-functions": "^0.5.7", + "jsonwebtoken": "^7.4.1", + "request": "^2.81.0", + "ts-node": "^3.0.4", + "typescript": "^2.3.3" + } +} diff --git a/tools/dashboard/functions/tsconfig.json b/tools/dashboard/functions/tsconfig.json new file mode 100644 index 000000000000..25300d16b3de --- /dev/null +++ b/tools/dashboard/functions/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "lib": ["es2015", "es2016", "dom"], + "module": "commonjs", + "moduleResolution": "node", + "noEmitOnError": true, + "noImplicitAny": true, + "sourceMap": true, + "target": "es5", + "baseUrl": "", + "outDir": "../../../dist/dashboard-functions/" + }, + "files": [ + "./dashboard-functions.ts" + ] +} From 6a853f6fae4b469506fa90fe90041db22fabf976 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 26 May 2017 19:30:43 +0200 Subject: [PATCH 2/2] Address feedback --- tools/dashboard/functions/functions.ts | 1 + .../functions/{ => github}/github-status.ts | 11 +++++-- tools/dashboard/functions/index.js | 8 ++--- tools/dashboard/functions/jwt/verify-token.ts | 22 ++++++++++++++ ...-functions.ts => payload-github-status.ts} | 29 +++---------------- tools/dashboard/functions/tsconfig.json | 2 +- 6 files changed, 38 insertions(+), 35 deletions(-) create mode 100644 tools/dashboard/functions/functions.ts rename tools/dashboard/functions/{ => github}/github-status.ts (71%) create mode 100644 tools/dashboard/functions/jwt/verify-token.ts rename tools/dashboard/functions/{dashboard-functions.ts => payload-github-status.ts} (51%) diff --git a/tools/dashboard/functions/functions.ts b/tools/dashboard/functions/functions.ts new file mode 100644 index 000000000000..ff59155dee72 --- /dev/null +++ b/tools/dashboard/functions/functions.ts @@ -0,0 +1 @@ +export {payloadGithubStatus} from './payload-github-status'; diff --git a/tools/dashboard/functions/github-status.ts b/tools/dashboard/functions/github/github-status.ts similarity index 71% rename from tools/dashboard/functions/github-status.ts rename to tools/dashboard/functions/github/github-status.ts index cc0e6a0ed5fa..c964df091431 100644 --- a/tools/dashboard/functions/github-status.ts +++ b/tools/dashboard/functions/github/github-status.ts @@ -1,5 +1,10 @@ +import {config} from 'firebase-functions'; + const request = require('request'); -const {version, name} = require('./package.json'); +const {version, name} = require('../package.json'); + +/** API token for the Github repository. Required to set the github status on commits and PRs. */ +const repoToken = config().repoToken; /** Data that must be specified to set a Github PR status. */ export type GithubStatusData = { @@ -10,7 +15,7 @@ export type GithubStatusData = { }; /** Function that sets a Github commit status */ -export function setGithubStatus(commitSHA: string, authToken: string, data: GithubStatusData) { +export function setGithubStatus(commitSHA: string, data: GithubStatusData) { const state = data.result ? 'success' : 'failure'; const requestData = JSON.stringify({ @@ -21,7 +26,7 @@ export function setGithubStatus(commitSHA: string, authToken: string, data: Gith }); const headers = { - 'Authorization': `token ${authToken}`, + 'Authorization': `token ${repoToken}`, 'User-Agent': `${name}/${version}`, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(requestData) diff --git a/tools/dashboard/functions/index.js b/tools/dashboard/functions/index.js index d089e8313d4d..a797ac292a6c 100644 --- a/tools/dashboard/functions/index.js +++ b/tools/dashboard/functions/index.js @@ -12,9 +12,5 @@ require('ts-node').register({ project: path.join(__dirname, 'tsconfig.json') }); -const functionExports = require('./dashboard-functions'); - -// Re-export every firebase function from TypeScript -Object.keys(functionExports).forEach(fnName => { - module.exports[fnName] = functionExports[fnName]; -}); +// Export all functions from the TypeScript source. +Object.assign(exports, require('./functions')); diff --git a/tools/dashboard/functions/jwt/verify-token.ts b/tools/dashboard/functions/jwt/verify-token.ts new file mode 100644 index 000000000000..de0c477af1cc --- /dev/null +++ b/tools/dashboard/functions/jwt/verify-token.ts @@ -0,0 +1,22 @@ +import {verify} from 'jsonwebtoken'; +import {config} from 'firebase-functions'; + +/** The JWT secret. This is used to validate JWT. */ +const jwtSecret = config().jwtSecret; + +/** The repo slug. This is used to validate the JWT is sent from correct repo. */ +const repoSlug = config().repoSlug; + +export function verifyToken(token: string): boolean { + try { + const tokenPayload = verify(token, jwtSecret, {issuer: 'Travis CI, GmbH'}); + + if (tokenPayload.slug !== repoSlug) { + console.log(`JWT slugs are not matching. Expected ${repoSlug}`); + } + + return true; + } catch (e) { + return false; + } +} diff --git a/tools/dashboard/functions/dashboard-functions.ts b/tools/dashboard/functions/payload-github-status.ts similarity index 51% rename from tools/dashboard/functions/dashboard-functions.ts rename to tools/dashboard/functions/payload-github-status.ts index f2055f60fc5d..c6fdfb8b0af7 100644 --- a/tools/dashboard/functions/dashboard-functions.ts +++ b/tools/dashboard/functions/payload-github-status.ts @@ -1,15 +1,6 @@ -import {https, config} from 'firebase-functions'; -import {verify} from 'jsonwebtoken'; -import {setGithubStatus} from './github-status'; - -/** The repo slug. This is used to validate the JWT is sent from correct repo. */ -const repoSlug = config().repoSlug; - -/** API token for the Github repository. Required to set the github status on commits and PRs. */ -const repoToken = config().repoToken; - -/** The JWT secret. This is used to validate JWT. */ -const jwtSecret = config().jwtSecret; +import {https} from 'firebase-functions'; +import {verifyToken} from './jwt/verify-token'; +import {setGithubStatus} from './github/github-status'; export const payloadGithubStatus = https.onRequest(async (request, response) => { const authToken = request.header('auth-token'); @@ -28,7 +19,7 @@ export const payloadGithubStatus = https.onRequest(async (request, response) => return response.status(400).json({message: 'No valid payload diff has been specified.'}); } - await setGithubStatus(commitSha, repoToken, { + await setGithubStatus(commitSha, { result: true, name: 'Library Payload', url: `https://travis-ci.org/angular/material2/jobs/${process.env['TRAVIS_JOB_ID']}`, @@ -37,15 +28,3 @@ export const payloadGithubStatus = https.onRequest(async (request, response) => response.json({message: 'Payload Github status successfully set.'}); }); - -function verifyToken(token: string): boolean { - try { - const tokenPayload = verify(token, jwtSecret, {issuer: 'Travis CI, GmbH'}); - if (tokenPayload.slug !== repoSlug) { - console.log(`JWT slugs are not matching. Expected ${repoSlug}`); - } - return true; - } catch (e) { - return false; - } -} diff --git a/tools/dashboard/functions/tsconfig.json b/tools/dashboard/functions/tsconfig.json index 25300d16b3de..ead7ebada8a1 100644 --- a/tools/dashboard/functions/tsconfig.json +++ b/tools/dashboard/functions/tsconfig.json @@ -11,6 +11,6 @@ "outDir": "../../../dist/dashboard-functions/" }, "files": [ - "./dashboard-functions.ts" + "./functions.ts" ] }