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/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/github-status.ts b/tools/dashboard/functions/github/github-status.ts new file mode 100644 index 000000000000..c964df091431 --- /dev/null +++ b/tools/dashboard/functions/github/github-status.ts @@ -0,0 +1,43 @@ +import {config} from 'firebase-functions'; + +const request = require('request'); +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 = { + result: boolean; + name: string; + description: string; + url: string; +}; + +/** Function that sets a Github commit status */ +export function setGithubStatus(commitSHA: 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 ${repoToken}`, + '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..a797ac292a6c --- /dev/null +++ b/tools/dashboard/functions/index.js @@ -0,0 +1,16 @@ +/** + * 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') +}); + +// 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/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/payload-github-status.ts b/tools/dashboard/functions/payload-github-status.ts new file mode 100644 index 000000000000..c6fdfb8b0af7 --- /dev/null +++ b/tools/dashboard/functions/payload-github-status.ts @@ -0,0 +1,30 @@ +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'); + 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, { + 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.'}); +}); diff --git a/tools/dashboard/functions/tsconfig.json b/tools/dashboard/functions/tsconfig.json new file mode 100644 index 000000000000..ead7ebada8a1 --- /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": [ + "./functions.ts" + ] +}