├── .eslintrc.js
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── builder
├── .dockerignore
├── Dockerfile
├── Dockerfile.nonheadless
├── README.md
├── app.yaml
├── chromeuser-script_nonheadless.sh
├── docker_build.sh
├── docker_run.sh
├── entrypoint.sh
├── entrypoint_nonheadless.sh
├── package.json
├── server.js
└── yarn.lock
├── deploy.sh
├── frontend
├── .gcloudignore
├── README.md
├── app.yaml
├── lighthouse-ci.js
├── package.json
├── public
│ ├── app.js
│ ├── logo-nolight.png
│ ├── logo.svg
│ ├── styles.css
│ └── try.html
├── server.js
└── yarn.lock
├── package.json
├── runlighthouse.js
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | // start with google standard style
3 | // https://github.com/google/eslint-config-google/blob/master/index.js
4 | "extends": ["eslint:recommended", "google"],
5 | "env": {
6 | "node": true,
7 | "es6": true,
8 | "browser": true,
9 | },
10 | "parserOptions": {
11 | "ecmaVersion": 8,
12 | "ecmaFeatures": {
13 | "jsx": false,
14 | "experimentalObjectRestSpread": false
15 | },
16 | "sourceType": "script"
17 | },
18 | "rules": {
19 | // 2 == error, 1 == warning, 0 == off
20 | "indent": [2, 2, {
21 | "SwitchCase": 1,
22 | "VariableDeclarator": 2
23 | }],
24 | "max-len": [2, 100, {
25 | "ignoreComments": true,
26 | "ignoreUrls": true,
27 | "tabWidth": 2
28 | }],
29 | "no-empty": [2, {
30 | "allowEmptyCatch": true
31 | }],
32 | "no-implicit-coercion": [2, {
33 | "boolean": false,
34 | "number": true,
35 | "string": true
36 | }],
37 | "no-unused-expressions": [2, {
38 | "allowShortCircuit": true,
39 | "allowTernary": false
40 | }],
41 | "no-unused-vars": [2, {
42 | "vars": "all",
43 | "args": "after-used",
44 | "argsIgnorePattern": "(^reject$|^_$)",
45 | "varsIgnorePattern": "(^_$)"
46 | }],
47 | "quotes": [2, "single"],
48 | "strict": [2, "global"],
49 | "prefer-const": 2,
50 |
51 | // Disabled rules
52 | "require-jsdoc": 0,
53 | "valid-jsdoc": 0,
54 | "comma-dangle": 0,
55 | "arrow-parens": 0,
56 | "no-console": 0
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | npm-debug.log
4 | .vscode
5 |
6 | frontend/.oauth_token
7 | frontend/app.yaml
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Folders
2 | .vscode/
3 | node_modules/
4 | builder/
5 | frontend/
6 |
7 | # Dev files
8 | deploy.sh
9 | .editorconfig
10 | .eslintignore
11 | .eslintrc.js
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2014 Google Inc.
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lighthouse Bot (deprecated)
2 |
3 | **Update:** LighthouseBot has been deprecated and we now recommend using the official [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci) project to automate running Lighthouse for every commit, view the changes, and prevent regressions
4 |
5 | ## Historical README below
6 |
7 | This repo contained the frontend and backend for running Lighthouse in CI and integration with Github Pull Requests. An example web service is hosted for demo purposes.
8 |
9 | ## Auditing GitHub Pull Requests
10 |
11 | > Please note: This drop in service is considered **Beta**. There are no SLAs or uptime guarantees. If you're interested in running your own CI server in a Docker container, check out [Running your own CI server](#running-your-own-ci-server).
12 |
13 | Lighthouse can be setup as part of your CI on **Travis only**. As new pull requests come in, the **Lighthouse Bot tests the changes and reports back the new score**.
14 |
15 |
16 |
17 | To audit pull requests, do the following:
18 |
19 | ### 1. Initial setup
20 |
21 | #### Add the lighthousebot to your repo
22 |
23 | First, add [lighthousebot](https://github.com/lighthousebot) as a collaborator on your repo. Lighthouse CI uses an OAuth token scoped to the `repo` permission in order to update the status of your PRs and post comments on the issue as the little Lighthouse icon.
24 |
25 | _* Until Lighthousebot accepts your invitation to collaborate, which is currently a lengthy manual process, it does not have permission to update the status of your PRs. However, it will post a comment on your PR._
26 |
27 | #### Get an API Key
28 |
29 | [Request an API Key](https://goo.gl/forms/9BzzhHd1sKzsvyC52). API keys will eventually be
30 | enforced and are necessary so we can contact you when there are changes to the CI system.
31 |
32 | Once you have a key, update Travis settings by adding an `LIGHTHOUSE_API_KEY` environment variables with your key:
33 |
34 |
35 |
36 | The `lighthousebot` script will include your key in requests made to the CI server.
37 |
38 | ### 2. Deploy the PR
39 |
40 | We recommend deploying your PR to a real staging server instead of running a local server on Travis.
41 | A staging environment will produce realistic performance numbers that are
42 | more representative of your production setup. The Lighthouse report will be more accurate.
43 |
44 | In `.travis.yml`, add an `after_success` that **deploys the PR's changes to a staging server**.
45 |
46 | ```bash
47 | after_success:
48 | - ./deploy.sh # TODO(you): deploy the PR changes to your staging server.
49 | ```
50 |
51 | Since every hosting environment has different deployment setups, the implementation of `deploy.sh` is left to the reader.
52 |
53 | > **Tip:** Using Google App Engine? Check out [`deploy_pr_gae.sh`](https://github.com/GoogleChrome/chromium-dashboard/blob/master/travis/deploy_pr_gae.sh) which shows how to install the GAE SDK and deploy PR changes programmatically.
54 |
55 | ### 3. Call lighthousebot
56 |
57 | Install the script:
58 |
59 | npm i --save-dev https://github.com/GoogleChromeLabs/lighthousebot
60 |
61 | Add an NPM script to your `package.json`:
62 |
63 | ```js
64 | "scripts": {
65 | "lh": "lighthousebot"
66 | }
67 | ```
68 |
69 | Next, in `.travis.yml` call [`npm run lh`][runlighthouse-link] as the last step in `after_success`:
70 |
71 | ```yml
72 | install:
73 | - npm install # make sure to install the deps when Travis runs.
74 | after_success:
75 | - ./deploy.sh # TODO(you): deploy the PR changes to your staging server.
76 | - npm run lh -- https://staging.example.com
77 | ```
78 |
79 | When Lighthouse is done auditing the URL, the bot will post a comment to the pull
80 | request containing the updated scores:
81 |
82 |
83 |
84 | You can also opt-out of the comment by using the `--no-comment` flag.
85 |
86 | #### Failing a PR when it drops your Lighthouse score
87 |
88 | Lighthouse CI can prevent PRs from being merged when one of the scores falls
89 | below a specified value. Just include one or more of `--pwa`, `--perf`, `--seo`,
90 | `--a11y`, or `--bp`:
91 |
92 | ```yml
93 | after_success:
94 | - ./deploy.sh # TODO(you): deploy the PR changes to your staging server.
95 | - npm run lh -- --perf=96 --pwa=100 https://staging.example.com
96 | ```
97 |
98 |
99 |
100 | #### Options
101 |
102 | ```bash
103 | $ lighthouse-ci -h
104 |
105 | Usage:
106 | runlighthouse.js [--perf,pwa,seo,a11y,bp=] [--no-comment] [--runner=chrome,wpt]
107 |
108 | Options:
109 | Minimum score values can be passed per category as a way to fail the PR if
110 | the thresholds are not met. If you don't provide thresholds, the PR will
111 | be mergeable no matter what the scores.
112 |
113 | --pwa Minimum PWA score for the PR to be considered "passing". [Number]
114 | --perf Minimum performance score for the PR to be considered "passing". [Number]
115 | --seo Minimum seo score for the PR to be considered "passing". [Number]
116 | --a11y Minimum accessibility score for the PR to be considered "passing". [Number]
117 | --bp Minimum best practices score for the PR to be considered "passing". [Number]
118 |
119 | --no-comment Doesn't post a comment to the PR issue summarizing the Lighthouse results. [Boolean]
120 |
121 | --runner Selects Lighthouse running on Chrome or WebPageTest. [--runner=chrome,wpt]
122 |
123 | --help Prints help.
124 |
125 | Examples:
126 |
127 | Runs Lighthouse and posts a summary of the results.
128 | runlighthouse.js https://example.com
129 |
130 | Fails the PR if the performance score drops below 93. Posts the summary comment.
131 | runlighthouse.js --perf=93 https://example.com
132 |
133 | Fails the PR if perf score drops below 93 or the PWA score drops below 100. Posts the summary comment.
134 | runlighthouse.js --perf=93 --pwa=100 https://example.com
135 |
136 | Runs Lighthouse on WebPageTest. Fails the PR if the perf score drops below 93.
137 | runlighthouse.js --perf=93 --runner=wpt --no-comment https://example.com
138 | ```
139 |
140 | ## Running on WebPageTest instead of Chrome
141 |
142 | By default, `lighthousebot` runs your PRs through Lighthouse hosted in the cloud. As an alternative, you can test on real devices using the WebPageTest integration:
143 |
144 | ```bash
145 | lighthousebot --perf=96 --runner=wpt https://staging.example.com
146 | ```
147 |
148 | At the end of testing, your PR will be updated with a link to the WebPageTest results containing the Lighthouse report!
149 |
150 | ## Running your own CI server
151 |
152 | Want to setup your own Lighthouse instance in a Docker container?
153 |
154 | The good news is Docker does most of the work for us! The bulk of getting started is in [Development](#development). That will take you through initial setup and show how to run the CI frontend.
155 |
156 | For the backend, see [builder/README.md](https://github.com/GoogleChromeLabs/lighthousebot/blob/master/builder/README.md) for building and running the Docker container.
157 |
158 | Other changes, to the "Development" section:
159 |
160 | - Create a personal OAuth token in https://github.com/settings/tokens. Drop it in `frontend/.oauth_token`.
161 | - Add a `LIGHTHOUSE_CI_HOST` env variable to Travis settings that points to your own URL. The one where you deploy the Docker container.
162 |
163 | ## Development
164 |
165 | Initial setup:
166 |
167 | 1. Ask an existing dev for the oauth2 token. If you need to regenerate one, see below.
168 | - Create `frontend/.oauth_token` and copy in the token value.
169 |
170 | Run the dev server:
171 |
172 | cd frontend
173 | npm run start
174 |
175 | This will start a web server and use the token in `.oauth_token`. The token is used to update PR status in Github.
176 |
177 | In your test repo:
178 |
179 | - Run `npm i --save-dev https://github.com/GoogleChromeLabs/lighthousebot`
180 | - Follow the steps in [Auditing Github Pull Requests](#auditing-github-pull-requests) for setting up
181 | your repo.
182 |
183 | Notes:
184 |
185 | - If you want to make changes to the builder, you'll need [Docker](https://www.docker.com/) and the [GAE Node SDK](https://cloud.google.com/appengine/docs/flexible/nodejs/download).
186 | - To make changes to the CI server, you'll probably want to run [ngrok](https://ngrok.com/) so you can test against a local server instead of deploying for each change. In Travis settings,
187 | add a `LIGHTHOUSE_CI_HOST` env variable that points to your ngrok instance.
188 |
189 | ##### Generating a new OAuth2 token
190 |
191 | If you need to generate a new OAuth token:
192 |
193 | 1. Sign in to the [lighthousebot](https://github.com/lighthousebot) Github account. (Admins: the credentials are in the usual password tool).
194 | 2. Visit personal access tokens: https://github.com/settings/tokens.
195 | 3. Regenerate the token. **Important**: this invalidates the existing token so other developers will need to be informed.
196 | 4. Update token in `frontend/.oauth_token`.
197 |
198 | #### Deploy
199 |
200 | By default, these scripts deploy to [Google App Engine Flexible containers](https://cloud.google.com/appengine/docs/flexible/nodejs/) (Node). If you're running your own CI server, use your own setup :)
201 |
202 | Deploy the frontend:
203 |
204 | npm run deploy YYYY-MM-DD frontend
205 |
206 | Deploy the CI builder backend:
207 |
208 | npm run deploy YYYY-MM-DD builder
209 |
210 | ## Source & Components
211 |
212 | This repo contains several different pieces for the Lighthouse Bot: a backend, frontend, and frontend UI.
213 |
214 | ### UI Frontend
215 | > Quick way to try Lighthouse: https://lighthouse-ci.appspot.com/try
216 |
217 | Relevant source:
218 |
219 | - `frontend/public/` - UI for https://lighthouse-ci.appspot.com/try.
220 |
221 | ### Bot CI server (frontend)
222 | > Server that responds to requests from Travis.
223 |
224 | REST endpoints:
225 | - `https://lighthouse-ci.appspot.com/run_on_chrome`
226 | - `https://lighthouse-ci.appspot.com/run_on_wpt`
227 |
228 | #### Example
229 |
230 | **Note:** `lighthousebot` does this for you.
231 |
232 | ```
233 | POST https://lighthouse-ci.appspot.com/run_on_chrome
234 | Content-Type: application/json
235 | X-API-KEY:
236 |
237 | {
238 | testUrl: "https://staging.example.com",
239 | thresholds: {
240 | pwa: 100,
241 | perf: 96,
242 | },
243 | addComment: true,
244 | repo: {
245 | owner: "",
246 | name: ""
247 | },
248 | pr: {
249 | number: ,
250 | sha: ""
251 | }
252 | }
253 | ```
254 |
255 | Relevant source:
256 |
257 | - [`frontend/server.js`](https://github.com/GoogleChromeLabs/lighthousebot/blob/master/frontend/server.js) - server which accepts Github pull requests and updates the status of your PR.
258 |
259 | ### CI backend (builder)
260 | > Server that runs Lighthouse against a URL, using Chrome.
261 |
262 | REST endpoints:
263 | - `https://lighthouse-ci.appspot.com/ci`
264 |
265 | #### Example
266 |
267 | **Note:** `lighthousebot` does this for you.
268 |
269 | ```bash
270 | curl -X POST \
271 | -H "Content-Type: application/json" \
272 | -H "X-API-KEY: " \
273 | --data '{"output": "json", "url": "https://staging.example.com"}' \
274 | https://builder-dot-lighthouse-ci.appspot.com/ci
275 | ```
276 |
277 | ## FAQ
278 |
279 | ##### Why not deployment events?
280 |
281 | Github's [Deployment API](https://developer.github.com/v3/repos/deployments/) would
282 | be ideal, but it has some downsides:
283 |
284 | - Github Deployments happen __after__ a pull is merged. We want to support blocking PR
285 | merges based on a LH score.
286 | - We want to be able to audit changes as they're add to the PR. `pull_request`/`push` events are more appropriate for that.
287 |
288 | ##### Why not a Github Webhook?
289 |
290 | The main downside of a Github webhook is that there's no way to include custom
291 | data in the payload Github sends to the webhook handler. For example, how would
292 | Lighthouse know what url to test? With a webhook, the user also has to setup it
293 | up and configure it properly.
294 |
295 | Future work: Lighthouse Bot could define a file that developer includes in their
296 | repo. The bot's endpoint could pull a `.lighthouse_ci` file that includes meta
297 | data `{minLighthouseScore: 96, testUrl: 'https://staging.example.com'}`. However,
298 | this requires work from the developer.
299 |
300 | [runlighthouse-link]: https://github.com/GoogleChromeLabs/lighthousebot/blob/master/runlighthouse.js
301 |
--------------------------------------------------------------------------------
/builder/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .gitignore
3 | .dockerignore
4 | Dockerfile*
5 | *-debug.log
6 | *-error.log
7 | .git
8 | .hg
9 | .svn
10 | README.md
11 |
--------------------------------------------------------------------------------
/builder/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:10-slim
2 |
3 | LABEL maintainer="Eric Bidelman "
4 |
5 | # Install utilities
6 | RUN apt-get update --fix-missing && apt-get -y upgrade
7 |
8 | # Install latest chrome dev package.
9 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
10 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
11 | && apt-get update \
12 | && apt-get install -y google-chrome-unstable --no-install-recommends \
13 | && rm -rf /var/lib/apt/lists/* \
14 | && rm -rf /src/*.deb
15 |
16 | ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
17 | RUN chmod +x /usr/local/bin/dumb-init
18 |
19 | # Download latest Lighthouse from npm.
20 | # cache bust so we always get the latest version of LH when building the image.
21 | ARG CACHEBUST=1
22 | RUN npm i lighthouse -g
23 |
24 | # Install express.
25 | COPY package.json .
26 | RUN npm i --production
27 |
28 | # Add the simple server.
29 | COPY server.js /
30 | RUN chmod +x /server.js
31 |
32 | COPY entrypoint.sh /
33 | RUN chmod +x /entrypoint.sh
34 |
35 | # Add a chrome user and setup home dir.
36 | RUN groupadd --system chrome && \
37 | useradd --system --create-home --gid chrome --groups audio,video chrome && \
38 | mkdir --parents /home/chrome/reports && \
39 | chown --recursive chrome:chrome /home/chrome
40 |
41 | USER chrome
42 |
43 | #VOLUME /home/chrome/reports
44 | #WORKDIR /home/chrome/reports
45 |
46 | # Disable Lighthouse error reporting to prevent prompt.
47 | ENV CI=true
48 |
49 | EXPOSE 8080
50 |
51 | ENTRYPOINT ["dumb-init", "--", "/entrypoint.sh"]
52 | #CMD ["lighthouse", "--help"]
53 |
--------------------------------------------------------------------------------
/builder/Dockerfile.nonheadless:
--------------------------------------------------------------------------------
1 | FROM node:8-slim
2 |
3 | LABEL maintainer="Eric Bidelman "
4 |
5 | # Install utilities, Xvfb and dbus for X11
6 | RUN apt-get update --fix-missing && apt-get -y upgrade
7 | RUN apt-get install -y sudo xvfb dbus-x11 --no-install-recommends
8 |
9 | # Install latest chrome dev package.
10 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
11 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
12 | && apt-get update \
13 | && apt-get install -y google-chrome-unstable --no-install-recommends \
14 | && rm -rf /var/lib/apt/lists/* \
15 | && rm -rf /src/*.deb
16 |
17 | ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init
18 | RUN chmod +x /usr/local/bin/dumb-init
19 |
20 | # Download latest Lighthouse from npm.
21 | # cache bust so we always get the latest version of LH when building the image.
22 | ARG CACHEBUST=1
23 | RUN npm i lighthouse -g
24 |
25 | # Install express.
26 | COPY package.json .
27 | RUN npm i --production
28 |
29 | # Add the simple server.
30 | COPY server.js /
31 | RUN chmod +x /server.js
32 |
33 | # Copy the chrome-user script used to start Chrome as non-root
34 | COPY chromeuser-script_nonheadless.sh /
35 | RUN chmod +x /chromeuser-script_nonheadless.sh
36 |
37 | # Set the entrypoint
38 | COPY entrypoint_nonheadless.sh /
39 | RUN chmod +x /entrypoint_nonheadless.sh
40 |
41 | # Add a user and make it a sudo user.
42 | RUN groupadd -r chrome && useradd -r -m -g chrome -G audio,video chrome && \
43 | mkdir -p /home/chrome/reports && \
44 | chown -R chrome:chrome /home/chrome && \
45 | sudo adduser chrome sudo
46 |
47 | # Disable Lighthouse error reporting to prevent prompt.
48 | ENV CI=true
49 |
50 | EXPOSE 8080
51 |
52 | ENTRYPOINT ["dumb-init", "--", "/entrypoint_nonheadless.sh"]
53 | #ENTRYPOINT ["dumb-init", "--"]
54 |
55 | #CMD ["/entrypoint_nonheadless.sh"]
56 | #CMD ["/bin/bash"]
57 | #CMD ["node", "server.js"]
58 |
--------------------------------------------------------------------------------
/builder/README.md:
--------------------------------------------------------------------------------
1 | # Lighthouse in Docker
2 |
3 | > Run Lighthouse in a Docker container (as a CLI or a web service)
4 |
5 | This folder repo example Dockerfiles for running Lighthouse using [Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome) and full Chrome and can
6 | be used in cloud environments like [Google App Engine Flex](https://cloud.google.com/appengine/docs/flexible/nodejs/) (Node).
7 |
8 | Main source files:
9 |
10 | - [`Dockerfile`](https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile) - Dockerfile for running Lighthouse using headless Chrome.
11 | - [`Dockerfile.nonheadless`](https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile.nonheadless) - Dockerfile for running Lighthouse using full Chrome.
12 | - `server.js` - The server implementation for the `/ci` endpoint. See [Using the container as a web service](#using-the-container-as-a-cli).
13 |
14 | ## Build it
15 |
16 | Fire up Docker, then run:
17 |
18 | ```bash
19 | yarn build
20 | ```
21 |
22 | **Image size: ~690MB.**
23 |
24 | ## Running the container
25 |
26 | There are two ways to run the container. One is directly from the command line.
27 | The other option starts a server and allows you to run Lighthouse as a web service (LaaS).
28 |
29 | ### Using the container as a CLI
30 |
31 | The container can be from the the CLI just like using the Lighthouse npm module. See
32 | Lighthouse docs for [CLI options](https://github.com/GoogleChrome/lighthouse#cli-options).
33 |
34 | ```bash
35 | # Audit example.com. Lighthouse results are printed to stdout.
36 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com
37 |
38 |
39 | # Audits example.com and saves HTML report to a file.
40 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com --quiet > report.html
41 |
42 | # Audits example.com and saves JSON results to a file.
43 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com --quiet --output=json > report.json
44 |
45 | # Print Lighthouse version used in the container.
46 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci --version
47 | ```
48 |
49 | ### Using the container as a web service (LaaS)
50 |
51 | The container also ships with a web service that supports a REST API. You can
52 | use it to run Lighthouse in the cloud and return scores.
53 |
54 | To run the web server, invoke `docker run` without any arguments:
55 |
56 | ```bash
57 | docker run -dit -p 8080:8080 --rm --name lighthouse_ci --cap-add=SYS_ADMIN lighthouse_ci
58 |
59 | # or
60 | yarn serve
61 |
62 | # or
63 | # handy for building + restarting the container
64 | yarn restart
65 | ```
66 |
67 | This starts a server on `8080` and exposes a REST endpoint at `http://localhost:8080/ci`.
68 | By default, requests ask for an API key to help prevent abuse and associate
69 | requests with users. However, uou don't have to use one in your own server.
70 | If you don't to require keys from users, simply include the parameter but use a
71 | fake value (e.g. "abc123").
72 |
73 | **Examples**
74 |
75 | `GET` requests will stream logs from Lighthouse until the report is ready. Once
76 | ready, the page redirects to the final output:
77 |
78 |
79 | ```bash
80 | curl http://localhost:8080/ci?key=&url=https://example.com&output=html
81 | ```
82 |
83 | The endpoint also supports `POST` requests. Instead of query params, send JSON
84 | with the same parameter names:
85 |
86 | ```bash
87 | curl -X POST \
88 | -H "Content-Type: application/json" \
89 | -H "X-API-KEY: " \
90 | --data '{"output": "json", "url": "https://example.com"}' \
91 | http://localhost:8080/ci
92 | ```
93 |
94 | where `output` is `json` or `html`.
95 |
96 | ## Using full Chrome instead of headless Chrome
97 |
98 | By default, the Dockerfile launches headless Chrome to run Lighthouse. If you
99 | want to to use "headlful" Chrome, build the image using `Dockerfile.nonheadless`:
100 |
101 | ```bash
102 | docker build -f Dockerfile.nonheadless -t lighthouse_ci . --build-arg CACHEBUST=$(date +%d)
103 | ```
104 |
105 | Everything else should remain the same.
106 |
107 | ## Deploy to Google App Engine Flex (Node)
108 |
109 | First, get yourself the [Google Cloud SDK](https://cloud.google.com/sdk/).
110 |
111 | When you're ready to deploy the app, run `gcloud deploy` with your app id and version:
112 |
113 | ```
114 | gcloud app deploy app.yaml --project YOUR_PROJECT_ID --version 2017-10-16
115 | ```
116 |
--------------------------------------------------------------------------------
/builder/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: custom
2 | env: flex
3 | service: builder
4 |
5 | health_check:
6 | enable_health_check: False
7 |
8 | automatic_scaling:
9 | min_num_instances: 1
10 | max_num_instances: 5
11 |
12 | resources:
13 | cpu: 4
14 | memory_gb: 16
15 | disk_size_gb: 10
16 |
--------------------------------------------------------------------------------
/builder/chromeuser-script_nonheadless.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Run full chrome with a bunch of stuff turned off.
4 | nohup google-chrome \
5 | --no-first-run \
6 | --disable-gpu \
7 | --disable-translate \
8 | --disable-default-apps \
9 | --disable-extensions \
10 | --disable-background-networking \
11 | --disable-sync \
12 | --metrics-recording-only \
13 | --safebrowsing-disable-auto-update \
14 | --disable-setuid-sandbox \
15 | --user-data-dir=${TMP_PROFILE_DIR} \
16 | --remote-debugging-port=9222 'about:blank' &
17 |
--------------------------------------------------------------------------------
/builder/docker_build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Switch if you want to Lighthouse with full Chrome instead of headless.
4 | # docker build -f Dockerfile.nonheadless -t lighthouse_ci . --build-arg CACHEBUST=$(date +%d)
5 |
6 | docker build -t lighthouse_ci . --build-arg CACHEBUST=$(date +%d)
7 |
--------------------------------------------------------------------------------
/builder/docker_run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | docker kill lighthouse_ci
4 | docker run -dit -p 8080:8080 --rm --name lighthouse_ci --cap-add=SYS_ADMIN lighthouse_ci
5 |
--------------------------------------------------------------------------------
/builder/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | if [ -z "$1" ]; then
4 | npm run start
5 | else
6 | lighthouse --port=9222 --chrome-flags="--headless" --output-path=stdout $@
7 | fi
8 |
--------------------------------------------------------------------------------
/builder/entrypoint_nonheadless.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Using full Chrome in Docker requires us to start xvfb and launch our own instance.
4 |
5 | /etc/init.d/dbus start
6 |
7 | Xvfb :99 -ac -screen 0 1280x1024x24 -nolisten tcp &
8 | xvfb=$!
9 | export DISPLAY=:99
10 |
11 | TMP_PROFILE_DIR=$(mktemp -d -t lighthouse.XXXXXXXXXX)
12 |
13 | su chrome /chromeuser-script_nonheadless.sh
14 |
15 | if [ -z "$1" ]; then
16 | npm run start
17 | else
18 | lighthouse --port=9222 --output-path=stdout $@
19 | fi
20 |
--------------------------------------------------------------------------------
/builder/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lighthouse_ci",
3 | "description": "Lighthouse in Docker",
4 | "author": "Eric Bidelman , Cedric Bellet",
5 | "license": "Apache-2.0",
6 | "main": "server.js",
7 | "scripts": {
8 | "start": "node server.js",
9 | "build": "./docker_build.sh",
10 | "restart": "yarn build && yarn serve",
11 | "serve": "./docker_run.sh",
12 | "chrome": "docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com --fast --quiet --output=json | node -e \"let f = ''; process.stdin.on('data', d => f += d); process.stdin.on('close', () => console.log(JSON.parse(f).userAgent));\""
13 | },
14 | "dependencies": {
15 | "body-parser": "^1.18.3",
16 | "express": "^4.16.3"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/builder/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs');
4 | const express = require('express');
5 | const spawn = require('child_process').spawn;
6 | const bodyParser = require('body-parser');
7 |
8 | const API_KEY_HEADER = 'X-API-KEY';
9 | const PORT = 8080;
10 | const REPORTS_DIR = './home/chrome/reports';
11 |
12 | function validURL(url, res) {
13 | if (!url) {
14 | res.status(400).send('Please provide a URL.');
15 | return false;
16 | }
17 |
18 | if (!url.startsWith('http')) {
19 | res.status(400).send('URL must start with http.');
20 | return false;
21 | }
22 |
23 | return true;
24 | }
25 |
26 | function getDefaultArgs(outputPath, format) {
27 | return [
28 | `--output-path=${outputPath}`,
29 | `--output=${format}`,
30 | // Dicey to use port=0 to launch a new instance of Chrome per invocation
31 | // of LH. On Linux, eventually Chrome Launcher begins to fail.
32 | // Root is https://github.com/GoogleChrome/chrome-launcher/issues/6.
33 | // '--port=9222',
34 | '--port=0', // choose random port every time so we launch a new instance of Chrome.
35 | // Note: this is a noop when using Dockerfile.nonheadless b/c Chrome is already launched.
36 | '--chrome-flags="--headless"',
37 | ];
38 | }
39 |
40 | // Handler for CI.
41 | function runLH(params, req, res, next) {
42 | const url = params.url;
43 | const format = params.output || params.format || 'html';
44 | const log = params.log || req.method === 'GET';
45 |
46 | if (!validURL(url, res)) {
47 | return;
48 | }
49 |
50 | const fileName = `report.${Date.now()}.${format}`;
51 | const outputPath = `${REPORTS_DIR}/${fileName}`;
52 | const args = getDefaultArgs(outputPath, format);
53 |
54 | const child = spawn('lighthouse', [...args, url]);
55 | child.stderr.pipe(process.stderr);
56 | child.stdout.pipe(process.stdout);
57 |
58 | if (log) {
59 | res.writeHead(200, {
60 | 'Content-Type': 'text/html',
61 | 'Cache-Control': 'no-cache',
62 | 'Connection': 'keep-alive',
63 | 'X-Accel-Buffering': 'no' // Forces Flex App Engine to keep connection open for streaming.
64 | });
65 |
66 | res.write(`
67 |
76 | ');
90 | res.write(``);
91 | res.end();
92 | } else {
93 | res.sendFile(`/${outputPath}`, {}, err => {
94 | if (err) {
95 | next(err);
96 | }
97 | // delete report
98 | fs.unlink(outputPath, err => {
99 | if (err) {
100 | next(err);
101 | }
102 | });
103 | });
104 | }
105 | });
106 | }
107 |
108 | // Serve sent event handler for https://lighthouse-ci.appspot.com/try.
109 | function runLighthouseAsEventStream(req, res, next) {
110 | const url = req.query.url;
111 | const format = req.query.output || req.query.format || 'html';
112 |
113 | if (!validURL(url, res)) {
114 | return;
115 | }
116 |
117 | // Send headers for event-stream connection.
118 | res.writeHead(200, {
119 | 'Content-Type': 'text/event-stream',
120 | 'Cache-Control': 'no-cache',
121 | 'Connection': 'keep-alive',
122 | 'Access-Control-Allow-Origin': '*',
123 | 'X-Accel-Buffering': 'no' // Forces Flex App Engine to keep connection open for SSE.
124 | });
125 |
126 | const fileName = `report.${Date.now()}.${format}`;
127 | const outputPath = `./${REPORTS_DIR}/${fileName}`;
128 | const args = getDefaultArgs(outputPath, format);
129 |
130 | const child = spawn('lighthouse', [...args, url]);
131 | // console.log('pid', child.pid);
132 |
133 | child.stderr.pipe(process.stderr);
134 | child.stdout.pipe(process.stdout);
135 |
136 | let log = '';
137 |
138 | // child.on('exit', (statusCode, signal) => {
139 | // console.log(statusCode, signal);
140 | // });
141 |
142 | child.stderr.on('data', data => {
143 | const str = data.toString();
144 | res.write(`data: ${str}\n\n`);
145 | log += str;
146 | });
147 |
148 | child.on('close', statusCode => {
149 | if (log.match(/Error: /gm)) {
150 | res.write(`data: ERROR\n\n`);
151 | } else {
152 | const serverOrigin = `https://${req.hostname}/`;
153 | res.write(`data: done ${serverOrigin + fileName}\n\n`);
154 | }
155 |
156 | res.status(410).end();
157 | log = '';
158 | });
159 | }
160 |
161 | const app = express();
162 | app.use(bodyParser.json());
163 |
164 | app.use(function enableCors(req, res, next) {
165 | res.set('Access-Control-Allow-Origin', '*');
166 |
167 | // Record GA hit.
168 | // const visitor = ua(GA_ACCOUNT, {https: true});
169 | // visitor.pageview(req.originalUrl).send();
170 |
171 | next();
172 | });
173 |
174 | app.use(express.static(REPORTS_DIR));
175 |
176 | app.get('/ci', (req, res, next) => {
177 | const apiKey = req.query.key;
178 | // Require API for get requests.
179 | if (!apiKey) {
180 | res.status(403).send('Missing API key. Please include the key parameter');
181 | return;
182 | }
183 | console.log(`${API_KEY_HEADER}: ${apiKey}`);
184 | runLH(req.query, req, res, next);
185 | });
186 |
187 | app.post('/ci', (req, res, next) => {
188 | // // Require an API key from users.
189 | // if (!req.get(API_KEY_HEADER)) {
190 | // const msg = `${API_KEY_HEADER} is missing`;
191 | // const err = new Error(msg);
192 | // res.status(403).json(err.message);
193 | // return;
194 | // }
195 |
196 | console.log(`${API_KEY_HEADER}: ${req.get(API_KEY_HEADER)}`);
197 |
198 | runLH(req.body, req, res, next);
199 | });
200 |
201 | app.get('/stream', (req, res, next) => {
202 | runLighthouseAsEventStream(req, res, next);
203 | });
204 |
205 | app.listen(PORT);
206 | console.log(`Running on http://localhost:${PORT}`);
207 |
--------------------------------------------------------------------------------
/builder/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | accepts@~1.3.5:
6 | version "1.3.5"
7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
8 | dependencies:
9 | mime-types "~2.1.18"
10 | negotiator "0.6.1"
11 |
12 | array-flatten@1.1.1:
13 | version "1.1.1"
14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
15 |
16 | body-parser@1.18.2:
17 | version "1.18.2"
18 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454"
19 | dependencies:
20 | bytes "3.0.0"
21 | content-type "~1.0.4"
22 | debug "2.6.9"
23 | depd "~1.1.1"
24 | http-errors "~1.6.2"
25 | iconv-lite "0.4.19"
26 | on-finished "~2.3.0"
27 | qs "6.5.1"
28 | raw-body "2.3.2"
29 | type-is "~1.6.15"
30 |
31 | body-parser@^1.18.3:
32 | version "1.18.3"
33 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
34 | dependencies:
35 | bytes "3.0.0"
36 | content-type "~1.0.4"
37 | debug "2.6.9"
38 | depd "~1.1.2"
39 | http-errors "~1.6.3"
40 | iconv-lite "0.4.23"
41 | on-finished "~2.3.0"
42 | qs "6.5.2"
43 | raw-body "2.3.3"
44 | type-is "~1.6.16"
45 |
46 | bytes@3.0.0:
47 | version "3.0.0"
48 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
49 |
50 | content-disposition@0.5.2:
51 | version "0.5.2"
52 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
53 |
54 | content-type@~1.0.4:
55 | version "1.0.4"
56 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
57 |
58 | cookie-signature@1.0.6:
59 | version "1.0.6"
60 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
61 |
62 | cookie@0.3.1:
63 | version "0.3.1"
64 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
65 |
66 | debug@2.6.9:
67 | version "2.6.9"
68 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
69 | dependencies:
70 | ms "2.0.0"
71 |
72 | depd@1.1.1, depd@~1.1.1:
73 | version "1.1.1"
74 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359"
75 |
76 | depd@~1.1.2:
77 | version "1.1.2"
78 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
79 |
80 | destroy@~1.0.4:
81 | version "1.0.4"
82 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
83 |
84 | ee-first@1.1.1:
85 | version "1.1.1"
86 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
87 |
88 | encodeurl@~1.0.2:
89 | version "1.0.2"
90 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
91 |
92 | escape-html@~1.0.3:
93 | version "1.0.3"
94 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
95 |
96 | etag@~1.8.1:
97 | version "1.8.1"
98 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
99 |
100 | express@^4.16.3:
101 | version "4.16.3"
102 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53"
103 | dependencies:
104 | accepts "~1.3.5"
105 | array-flatten "1.1.1"
106 | body-parser "1.18.2"
107 | content-disposition "0.5.2"
108 | content-type "~1.0.4"
109 | cookie "0.3.1"
110 | cookie-signature "1.0.6"
111 | debug "2.6.9"
112 | depd "~1.1.2"
113 | encodeurl "~1.0.2"
114 | escape-html "~1.0.3"
115 | etag "~1.8.1"
116 | finalhandler "1.1.1"
117 | fresh "0.5.2"
118 | merge-descriptors "1.0.1"
119 | methods "~1.1.2"
120 | on-finished "~2.3.0"
121 | parseurl "~1.3.2"
122 | path-to-regexp "0.1.7"
123 | proxy-addr "~2.0.3"
124 | qs "6.5.1"
125 | range-parser "~1.2.0"
126 | safe-buffer "5.1.1"
127 | send "0.16.2"
128 | serve-static "1.13.2"
129 | setprototypeof "1.1.0"
130 | statuses "~1.4.0"
131 | type-is "~1.6.16"
132 | utils-merge "1.0.1"
133 | vary "~1.1.2"
134 |
135 | finalhandler@1.1.1:
136 | version "1.1.1"
137 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
138 | dependencies:
139 | debug "2.6.9"
140 | encodeurl "~1.0.2"
141 | escape-html "~1.0.3"
142 | on-finished "~2.3.0"
143 | parseurl "~1.3.2"
144 | statuses "~1.4.0"
145 | unpipe "~1.0.0"
146 |
147 | forwarded@~0.1.2:
148 | version "0.1.2"
149 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
150 |
151 | fresh@0.5.2:
152 | version "0.5.2"
153 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
154 |
155 | http-errors@1.6.2, http-errors@~1.6.2:
156 | version "1.6.2"
157 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
158 | dependencies:
159 | depd "1.1.1"
160 | inherits "2.0.3"
161 | setprototypeof "1.0.3"
162 | statuses ">= 1.3.1 < 2"
163 |
164 | http-errors@1.6.3, http-errors@~1.6.3:
165 | version "1.6.3"
166 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
167 | dependencies:
168 | depd "~1.1.2"
169 | inherits "2.0.3"
170 | setprototypeof "1.1.0"
171 | statuses ">= 1.4.0 < 2"
172 |
173 | iconv-lite@0.4.19:
174 | version "0.4.19"
175 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
176 |
177 | iconv-lite@0.4.23:
178 | version "0.4.23"
179 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
180 | dependencies:
181 | safer-buffer ">= 2.1.2 < 3"
182 |
183 | inherits@2.0.3:
184 | version "2.0.3"
185 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
186 |
187 | ipaddr.js@1.8.0:
188 | version "1.8.0"
189 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e"
190 |
191 | media-typer@0.3.0:
192 | version "0.3.0"
193 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
194 |
195 | merge-descriptors@1.0.1:
196 | version "1.0.1"
197 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
198 |
199 | methods@~1.1.2:
200 | version "1.1.2"
201 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
202 |
203 | mime-db@~1.30.0:
204 | version "1.30.0"
205 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
206 |
207 | mime-db@~1.36.0:
208 | version "1.36.0"
209 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397"
210 |
211 | mime-types@~2.1.15:
212 | version "2.1.17"
213 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
214 | dependencies:
215 | mime-db "~1.30.0"
216 |
217 | mime-types@~2.1.18:
218 | version "2.1.20"
219 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19"
220 | dependencies:
221 | mime-db "~1.36.0"
222 |
223 | mime@1.4.1:
224 | version "1.4.1"
225 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
226 |
227 | ms@2.0.0:
228 | version "2.0.0"
229 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
230 |
231 | negotiator@0.6.1:
232 | version "0.6.1"
233 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
234 |
235 | on-finished@~2.3.0:
236 | version "2.3.0"
237 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
238 | dependencies:
239 | ee-first "1.1.1"
240 |
241 | parseurl@~1.3.2:
242 | version "1.3.2"
243 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
244 |
245 | path-to-regexp@0.1.7:
246 | version "0.1.7"
247 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
248 |
249 | proxy-addr@~2.0.3:
250 | version "2.0.4"
251 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
252 | dependencies:
253 | forwarded "~0.1.2"
254 | ipaddr.js "1.8.0"
255 |
256 | qs@6.5.1:
257 | version "6.5.1"
258 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
259 |
260 | qs@6.5.2:
261 | version "6.5.2"
262 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
263 |
264 | range-parser@~1.2.0:
265 | version "1.2.0"
266 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
267 |
268 | raw-body@2.3.2:
269 | version "2.3.2"
270 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89"
271 | dependencies:
272 | bytes "3.0.0"
273 | http-errors "1.6.2"
274 | iconv-lite "0.4.19"
275 | unpipe "1.0.0"
276 |
277 | raw-body@2.3.3:
278 | version "2.3.3"
279 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
280 | dependencies:
281 | bytes "3.0.0"
282 | http-errors "1.6.3"
283 | iconv-lite "0.4.23"
284 | unpipe "1.0.0"
285 |
286 | safe-buffer@5.1.1:
287 | version "5.1.1"
288 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
289 |
290 | "safer-buffer@>= 2.1.2 < 3":
291 | version "2.1.2"
292 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
293 |
294 | send@0.16.2:
295 | version "0.16.2"
296 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
297 | dependencies:
298 | debug "2.6.9"
299 | depd "~1.1.2"
300 | destroy "~1.0.4"
301 | encodeurl "~1.0.2"
302 | escape-html "~1.0.3"
303 | etag "~1.8.1"
304 | fresh "0.5.2"
305 | http-errors "~1.6.2"
306 | mime "1.4.1"
307 | ms "2.0.0"
308 | on-finished "~2.3.0"
309 | range-parser "~1.2.0"
310 | statuses "~1.4.0"
311 |
312 | serve-static@1.13.2:
313 | version "1.13.2"
314 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
315 | dependencies:
316 | encodeurl "~1.0.2"
317 | escape-html "~1.0.3"
318 | parseurl "~1.3.2"
319 | send "0.16.2"
320 |
321 | setprototypeof@1.0.3:
322 | version "1.0.3"
323 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
324 |
325 | setprototypeof@1.1.0:
326 | version "1.1.0"
327 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
328 |
329 | "statuses@>= 1.3.1 < 2":
330 | version "1.3.1"
331 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
332 |
333 | "statuses@>= 1.4.0 < 2":
334 | version "1.5.0"
335 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
336 |
337 | statuses@~1.4.0:
338 | version "1.4.0"
339 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
340 |
341 | type-is@~1.6.15:
342 | version "1.6.15"
343 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
344 | dependencies:
345 | media-typer "0.3.0"
346 | mime-types "~2.1.15"
347 |
348 | type-is@~1.6.16:
349 | version "1.6.16"
350 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
351 | dependencies:
352 | media-typer "0.3.0"
353 | mime-types "~2.1.18"
354 |
355 | unpipe@1.0.0, unpipe@~1.0.0:
356 | version "1.0.0"
357 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
358 |
359 | utils-merge@1.0.1:
360 | version "1.0.1"
361 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
362 |
363 | vary@~1.1.2:
364 | version "1.1.2"
365 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
366 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | deployVersion=$1
4 | app=$2
5 | usage="Usage: deploy.sh `date +%Y-%m-%d` builder|frontend "
6 | #readonly APPDIR=$(dirname $BASH_SOURCE)
7 |
8 | if [ -z "$deployVersion" ]
9 | then
10 | echo "App version not specified."
11 | echo $usage
12 | exit 0
13 | fi
14 |
15 | if [ -z "$app" ]
16 | then
17 | echo 'Please specify "builder" or "frontend" target to deploy.'
18 | echo $usage
19 | exit 0
20 | fi
21 |
22 | echo "Deploying $app version: $deployVersion"
23 |
24 | if [ $app == "builder" ]
25 | then
26 | gcloud app deploy builder/app.yaml \
27 | --project lighthouse-ci --version $deployVersion
28 | elif [ $app == "frontend" ]
29 | then
30 | gcloud app deploy frontend/app.yaml \
31 | --project lighthouse-ci --version $deployVersion
32 | else
33 | echo $usage
34 | exit 0
35 | fi
36 |
--------------------------------------------------------------------------------
/frontend/.gcloudignore:
--------------------------------------------------------------------------------
1 | # This file specifies files that are *not* uploaded to Google Cloud Platform
2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of
3 | # "#!include" directives (which insert the entries of the given .gitignore-style
4 | # file at that point).
5 | #
6 | # For more information, run:
7 | # $ gcloud topic gcloudignore
8 | #
9 | .gcloudignore
10 | # If you would like to upload your .git directory, .gitignore file or files
11 | # from your .gitignore file, remove the corresponding line
12 | # below:
13 | .git
14 | .gitignore
15 |
16 | # Node.js dependencies:
17 | node_modules/
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | Frontend that shows how to communicate with the Lighthhouse CI builder backend.
2 |
3 | Try it: https://lighthouse-ci.appspot.com/try
4 |
--------------------------------------------------------------------------------
/frontend/app.yaml:
--------------------------------------------------------------------------------
1 | runtime: nodejs8
2 | #env: flex
3 |
4 | #automatic_scaling:
5 | # min_num_instances: 1
6 | # max_num_instances: 1
7 |
--------------------------------------------------------------------------------
/frontend/lighthouse-ci.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2017 Google Inc. All rights reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | 'use strict';
17 |
18 | const fetch = require('node-fetch'); // polyfill
19 | const Github = require('@octokit/rest');
20 | const URL = require('url').URL;
21 | const URLSearchParams = require('url').URLSearchParams;
22 |
23 | class LighthouseCI {
24 | /**
25 | * @param {!string} token Github OAuth token that has repo:status access.
26 | */
27 | constructor(token) {
28 | this.github = new Github({debug: false});
29 | this.github.authenticate({type: 'oauth', token});
30 | }
31 |
32 | handleError(err, prInfo) {
33 | console.error(err);
34 | this.updateGithubStatus(Object.assign({
35 | state: 'error',
36 | description: `Error. ${err.message}`
37 | }, prInfo));
38 | }
39 |
40 | testOnHeadlessChrome(body, headers) {
41 | headers = Object.assign(headers, {
42 | 'Content-Type': 'application/json'
43 | });
44 |
45 | // POST https://builder-dot-lighthouse-ci.appspot.com/ci
46 | // '{"output": "json", "url": }"'
47 | return fetch('https://builder-dot-lighthouse-ci.appspot.com/ci', {
48 | method: 'POST',
49 | body: JSON.stringify(body),
50 | headers
51 | }).then(resp => resp.json())
52 | .catch(err => {
53 | throw err;
54 | });
55 | }
56 |
57 | /**
58 | * Uses WebPageTest's Rest API to run Lighthouse and score a URL.
59 | * See https://sites.google.com/a/webpagetest.org/docs/advanced-features/webpagetest-restful-apis
60 | * @param {!string} apiKey
61 | * @param {!string} testUrl URL to audit.
62 | * @param {!string} pingback URL for WPT to ping when result is ready.
63 | * @return {!Promise} json response from starting a WPT run.
64 | */
65 | startOnWebpageTest(apiKey, testUrl, pingback) {
66 | const params = new URLSearchParams();
67 | params.set('k', apiKey);
68 | params.set('f', 'json');
69 | params.set('pingback', pingback); // The pingback is passed an "id" parameter of the test.
70 | // TODO: match emulation to LH settings.
71 | params.set('location', 'Dulles_Nexus5:Nexus 5 - Chrome Beta.3G_EM');
72 | // For native: Dulles_Linux:Chrome.Native
73 | // params.set('location', 'Dulles_MotoG4:Moto G4 - Chrome Beta.3GFast');
74 | params.set('mobile', 1); // Emulate mobile (for desktop cases).
75 | params.set('type', 'lighthouse'); // LH-only run.
76 | params.set('lighthouse', 1);
77 | params.set('url', testUrl);
78 |
79 | const wptUrl = new URL('https://www.webpagetest.org/runtest.php');
80 | wptUrl.search = params;
81 |
82 | return fetch(wptUrl.toString())
83 | .then(resp => resp.json())
84 | .catch(err => {
85 | throw err;
86 | });
87 | }
88 |
89 | /**
90 | * Returns the scores for each category.
91 | * @param {!Object} lhr Lighthouse results object.
92 | * @return {!Object>}
93 | */
94 | static getOverallScores(lhr) {
95 | const cats = Object.keys(lhr.categories);
96 | const obj = {};
97 | for (const cat of cats) {
98 | obj[cat] = lhr.categories[cat].score * 100;
99 | }
100 | return obj;
101 | }
102 |
103 | /**
104 | * Updates associated PR status.
105 | * @param {!Object=} opts Options to set the status with.
106 | * @return {!Promise