| @@ -1,150 +1,56 @@ | ||
| Test Pilot | ||
| ========== | ||
| **NOTE:** The Test Pilot program has ended. The master branch in this repository reflects the state of the project before it ended. [The farewell message mini-site lives in the `eol` branch!](https://github.com/mozilla/testpilot/tree/eol) | ||
|
|
||
|  | ||
|
|
||
| [](https://circleci.com/gh/mozilla/testpilot/tree/master) | ||
| [](https://coveralls.io/r/mozilla/testpilot) | ||
| [](https://requires.io/github/mozilla/testpilot/requirements/?branch=master) | ||
| <img src="frontend/src/app/components/Copter/img/copter.png" alt="Test Pilot Logo" width="328" height="265"> | ||
|
|
||
| # Test Pilot | ||
|
|
||
| Test Pilot is an opt-in platform that allows us to perform controlled tests of new high-visibility product concepts in the general release channel of Firefox. | ||
|
|
||
| Test Pilot is not intended to replace trains for most features, nor is it a test bed for concepts we do not believe have a strong chance of shipping in general release. Rather, it is reserved for features that require user feedback, testing, and tuning before they ship with the browser. | ||
|
|
||
| <!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
| <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
| **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* | ||
|
|
||
| - [More Information](#more-information) | ||
| - [Development](#development) | ||
| - [Quickstart](#quickstart) | ||
| - [OS X hosts](#os-x-hosts) | ||
| - [Ubuntu Linux hosts](#ubuntu-linux-hosts) | ||
| - [Windows hosts](#windows-hosts) | ||
| - [Next Steps](#next-steps) | ||
|
|
||
| <!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
|
|
||
| ## More Information | ||
|
|
||
| - Wiki: https://wiki.mozilla.org/Test_Pilot | ||
| - IRC: #testpilot on irc.mozilla.org | ||
| - [Test Pilot Metrics](docs/README-METRICS.md) | ||
|
|
||
| ## Development | ||
|
|
||
| ### Quickstart | ||
|
|
||
| This project uses Docker in development. You'll get a lot of benefit | ||
| by acquainting yourself [with Docker and its documentation][docker-docs]. | ||
| However, you can get started on Test Pilot development with a minimum of Docker | ||
| know-how: | ||
|
|
||
| [docker-docs]: https://docs.docker.com/ | ||
|
|
||
| By default `js-lint` and `sass-lint` watch options are set to true. If you would like | ||
| to override these, you can do so in [debug-config.json](./debug-config.json). | ||
| if you'd like to run linters on `pre-commit` you can use this [pre-commit-hook](https://gist.github.com/meandavejustice/39f7edc046f3458aa076). | ||
|
|
||
| See some of our [Tips and Tricks](docs/README-DOCKER.md). | ||
|
|
||
| #### First Thing's First | ||
|
|
||
| Make sure you clone the Test Pilot repo: | ||
|
|
||
| `git clone https://github.com/mozilla/testpilot.git` | ||
|
|
||
| #### OS X hosts | ||
|
|
||
| 1. [Install Docker Toolbox](http://docs.docker.com/mac/started/) | ||
|
|
||
| 2. Make sure you have a default Docker machine: | ||
|
|
||
| `docker-machine create --driver virtualbox default` | ||
|
|
||
| 3. Make sure the default machine is running: | ||
|
|
||
| `docker-machine start default` | ||
|
|
||
| 4. Make sure your shell can see the default Docker machine: | ||
|
|
||
| `eval "$(docker-machine env default)"` | ||
| Test Pilot is not intended to replace trains for most features, nor is it a test bed for concepts we do not believe have a strong chance of shipping in general release. Rather, it is reserved for features that require user feedback, testing and tuning before they ship with the browser. | ||
|
|
||
| 5. Check the IP address of the default Docker machine: | ||
| [](https://circleci.com/gh/mozilla/testpilot/) | ||
| [](https://codecov.io/gh/mozilla/testpilot) | ||
|
|
||
| `docker-machine ip default` | ||
| --- | ||
|
|
||
| 6. Use this IP address to add an entry for `testpilot.dev` in `/etc/hosts`: | ||
| ## Table of Contents | ||
|
|
||
| `192.168.99.100 testpilot.dev` | ||
| - Developing Test Pilot | ||
| - [Quickstart](docs/development/quickstart.md) - Get your development environment working. | ||
| - [Testing](docs/development/testing.md) - Automated testing. | ||
| - [Storybook](docs/development/storybook.md) - Interactive development & testing for UI components. | ||
| - [Add-on](addon/README.md) - Working on the Test Pilot add-on. | ||
| - [Deployment](docs/development/deployment.md) - Deploying Test Pilot to staging and production | ||
| - [Dev Deployment](docs/development/dev-deployment.md) - Deploying Test Pilot to the dev environment | ||
| - [Verifying deployments](docs/development/verification.md) - Verifying Test Pilot deployments. | ||
| - Developing experiments | ||
| - [Recommended process](docs/experiments/developing_an_experiment.md) | ||
| - [Experiment metrics](docs/experiments/ga.md) - The use of Google Analytics to track experiment data. | ||
| - [Experiment Feedback Integration](docs/examples/feedback-buttons.md) | ||
| - Metrics | ||
| - [Google Analytics](docs/metrics/ga.md) - How we use Google Analytics. | ||
| - [New features](docs/metrics/new_features.md) - Everything needed to instrument something new. | ||
| - [Experiment content](docs/content/reference.md) - Management of experiment content. | ||
| - [Graduating experiments](docs/content/graduation.md) - Graduation of old experiments. | ||
| - [Process](docs/process.md) - How we create, triage, and assign work. | ||
| - [FAQ](docs/faq.md) | ||
| - [Contributing to Test Pilot](CONTRIBUTING.md) | ||
| - [Code of conduct](docs/code_of_conduct.md) | ||
| - [License](LICENSE) | ||
|
|
||
| You can do this manually, or the [bin/update-ip.sh][update-ip] script can | ||
| take care of this for you. | ||
| --- | ||
|
|
||
| [update-ip]: https://github.com/mozilla/testpilot/blob/master/bin/update-ip.sh | ||
| ## Localization | ||
|
|
||
| 7. Don't forget to cd into your Test Pilot directory: | ||
| Test Pilot localization is managed via [Pontoon](https://pontoon.mozilla.org/projects/test-pilot-website/), not direct pull requests to the repository. If you want to fix a typo, add a new language, or simply know more about localization, please get in touch with the [existing localization team](https://pontoon.mozilla.org/teams/) for your language or Mozillaās [l10n-drivers](https://wiki.mozilla.org/L10n:Mozilla_Team#Mozilla_Corporation) for guidance. | ||
|
|
||
| `cd testpilot` | ||
| --- | ||
|
|
||
| 8. Create and setup the Docker containers (this will take some time): | ||
|
|
||
| `docker-compose up` | ||
|
|
||
| #### Ubuntu Linux hosts | ||
|
|
||
| 1. [Install Docker](http://docs.docker.com/linux/started/) | ||
|
|
||
| 2. [Install Docker Compose](https://docs.docker.com/compose/install/) | ||
|
|
||
| 3. Add an entry for `testpilot.dev` in `/etc/hosts`: | ||
|
|
||
| `127.0.0.1 testpilot.dev` | ||
|
|
||
| You can do this manually, or the [bin/update-ip.sh][update-ip] script can | ||
| take care of this for you. | ||
|
|
||
| 4. Don't forget to cd into your Test Pilot directory: | ||
|
|
||
| `cd testpilot` | ||
|
|
||
| 5. Create and setup the Docker containers (this will take some time): | ||
|
|
||
| `sudo docker-compose up` | ||
|
|
||
| #### Windows hosts | ||
|
|
||
| * **Help wanted**: Getting things working on Windows may be similar to OS X, | ||
| but the team has little experience with that environment. | ||
|
|
||
| ### Next Steps | ||
|
|
||
| * Start editing files - changes should be picked up automatically. | ||
|
|
||
| * Visit the Django server, using the hostname you added to `/etc/hosts`: | ||
|
|
||
| `http://testpilot.dev:8000/` | ||
|
|
||
| * Visit Django admin, login with username `admin` and password `admin`: | ||
|
|
||
| `http://testpilot.dev:8000/admin/` | ||
|
|
||
| * For further reading: | ||
|
|
||
| * [`README-DOCKER.md`](./docs/README-DOCKER.md) - for more hints & tips on Docker in | ||
| development, including how to set up custom configurations and run common | ||
| tests & checks. | ||
|
|
||
| * [`circle.yml`](./circle.yml) - to see what checks are run automatically in Circle | ||
| CI, which you should ensure pass locally before submitting a Pull Request on | ||
| GitHub | ||
|
|
||
| * [`addon/README.md`](./addon/README.md) - for more details on the addon this | ||
| site uses to enable advanced features. | ||
| ## More Information | ||
|
|
||
| * [`docs/WORK.md`](./docs/WORK.md) - information on how we create, triage and assign work. | ||
| - Wiki: https://wiki.mozilla.org/Test_Pilot | ||
| - IRC: #testpilot on irc.mozilla.org | ||
|
|
||
| * [`docs/DEPLOYMENT.md`](./docs/DEPLOYMENT.md) - process for deploying Test Pilot to stage and production. | ||
| --- | ||
|
|
||
| * [`docs/FAQs.md`](./docs/FAQs.md) - frequently asked questions. | ||
| <img src="https://avatars2.githubusercontent.com/u/131524?s=200&v=4" width="50"></img> |
| @@ -1,2 +1,8 @@ | ||
| rules: | ||
| func-names: 0 | ||
| extends: | ||
| - ../.eslintrc | ||
|
|
||
| overrides: | ||
| - | ||
| files: ['background.js', 'options/options.js'] | ||
| rules: | ||
| import/unambiguous: off |
| @@ -1,189 +1,68 @@ | ||
| # Test Pilot | ||
| The add-on where ideas come to idea | ||
| Test Pilot main add-on | ||
| ---------------------- | ||
|
|
||
| <!-- START doctoc generated TOC please keep comment here to allow auto update --> | ||
| <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> | ||
| **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* | ||
| This add-on serves a few basic purposes common to participation in any Test Pilot experiment: | ||
|
|
||
| - [installation](#installation) | ||
| - [configuration](#configuration) | ||
| - [development](#development) | ||
| - [running once for testing](#running-once-for-testing) | ||
| - [packaging](#packaging) | ||
| - [distributing](#distributing) | ||
| - [Events](#events) | ||
| - [Talking to the addon](#talking-to-the-addon) | ||
| - [Maintainers](#maintainers) | ||
| - [Attribution](#attribution) | ||
| * Toolbar button as a quick shortcut to Test Pilot. | ||
| * Notifications when experiments are updated and new ones arrive. | ||
| * Daily metric reporting about participation in experiments. | ||
|
|
||
| <!-- END doctoc generated TOC please keep comment here to allow auto update --> | ||
| ## Building and Development | ||
|
|
||
| ## installation | ||
| First, ensure that you've followed [the quickstart guide](../docs/development/quickstart.md) for working on the Test Pilot site itself. This includes ensuring you've got the right version of Node.js (currently, [v6.x LTS](https://nodejs.org/dist/latest-v6.x/)). | ||
|
|
||
| `npm install` | ||
|
|
||
| ## configuration | ||
|
|
||
| prod: check the `CONFIG` property in `package.json` | ||
| dev: [dev-prefs.json](dev-prefs.json) | ||
|
|
||
| ## development | ||
| ``` | ||
| cd addon | ||
| npm start | ||
| ``` | ||
|
|
||
| A relatively easy path for working on this addon involves the following steps: | ||
| If you just want an XPI package of the add-on, use one of these commands: | ||
| ``` | ||
| npm run package # builds for local dev by default | ||
| ``` | ||
|
|
||
| 1. Install [Firefox Developer Edition][devedition]. | ||
| If your packaging for another environment then you must include some environment variables: | ||
| ``` | ||
| ENVIRONMENT_TITLE=production ENVIRONMENT_URL=https://testpilot.firefox.com/ npm run package | ||
| ``` | ||
|
|
||
| 2. [Disable add-on signature checks.][sigchecks] TL;DR: Enter `about:config` in | ||
| the URL bar. Set `xpinstall.signatures.required` and | ||
| `xpinstall.whitelist.required` flags to false. | ||
| Here is the full list of urls for each environment: | ||
|
|
||
| 3. Install the [Extension Auto-Installer][autoinstaller] Add-on in Firefox | ||
| Developer Edition. | ||
| ``` | ||
| ENVIRONMENT_TITLE=local ENVIRONMENT_URL=https://example.com/ | ||
| ENVIRONMENT_TITLE=dev ENVIRONMENT_URL=https://testpilot.dev.mozaws.net/ | ||
| ENVIRONMENT_TITLE=l10n ENVIRONMENT_URL=https://testpilot-l10n.dev.mozaws.net/ | ||
| ENVIRONMENT_TITLE=stage ENVIRONMENT_URL=https://testpilot.stage.mozaws.net/ | ||
| ENVIRONMENT_TITLE=production ENVIRONMENT_URL=https://testpilot.firefox.com/ | ||
| ``` | ||
|
|
||
| 4. Run `npm start` to fire up a watcher that will build the Test Pilot add-on | ||
| whenever files change and auto-update the installed version in Firefox. | ||
| If you'd like to actively work on the add-on, here are some additional steps to set up a more convenient workflow: | ||
|
|
||
| 5. Read all about [setting up an extension development | ||
| environment][extensiondev] on MDN. | ||
| 1. Install [Firefox Developer Edition][devedition]. (Nightly should work, too, but Dev Edition is preferred.) | ||
|
|
||
| [devedition]: https://www.mozilla.org/en-US/firefox/developer/ | ||
| [sigchecks]: https://wiki.mozilla.org/Add-ons/Extension_Signing#FAQ | ||
| [autoinstaller]: https://addons.mozilla.org/en-US/firefox/addon/autoinstaller/ | ||
| [extensiondev]: https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment | ||
|
|
||
| For UI hacking you can run `npm run watch-ui` to easily debug `lib/templates.js` and `data/panel.css` | ||
|
|
||
| ## running once for testing | ||
|
|
||
| * Install [Firefox Beta][fxbeta] | ||
|
|
||
| * `npm run once` | ||
|
|
||
| This should package the add-on and fire up Firefox Beta using a fresh profile | ||
| with the add-on installed. | ||
|
|
||
| [fxbeta]: https://www.mozilla.org/en-US/firefox/channel/#beta | ||
|
|
||
| ## packaging | ||
|
|
||
| `npm run sign` | ||
|
|
||
| ## distributing | ||
|
|
||
| We serve the add-on from the `/static/addon/` directory. We will need | ||
| to get the add-on signed via [AMO](http://addons.mozilla.org/) and move | ||
| it into the correct directory. This is all packaged into a script, | ||
| `npm run sign` in the [package.json](./package.json). | ||
|
|
||
| ## Events | ||
|
|
||
| Accepted: | ||
| * `install-experiment` | ||
| * `uninstall-experiment` | ||
| * `uninstall-self` | ||
| * `sync-installed` | ||
|
|
||
| Emitted: | ||
| * `sync-installed-result` | ||
| * `addon-install:install-started` | ||
| * `addon-install:install-new` | ||
| * `addon-install:install-cancelled` | ||
| * `addon-install:install-ended` | ||
| * `addon-install:install-failed` | ||
| * `addon-install:download-started` | ||
| * `addon-install:download-progress` | ||
| * `addon-install:download-ended` | ||
| * `addon-install:download-cancelled` | ||
| * `addon-install:download-failed` | ||
| * `addon-uninstall:uninstall-started` | ||
| * `addon-uninstall:uninstall-ended` | ||
| * `addon-manage:enabled` | ||
| * `addon-manage:disabled` | ||
| * `addon-self:installed` | ||
| * `addon-self:enabled` | ||
| * `addon-self:upgraded` | ||
| * `addon-self:uninstalled` | ||
|
|
||
| Any emitted events prefixed with `addon-install:` will have an associated object | ||
| which will be structured as such: | ||
|
|
||
| ``` json | ||
| { | ||
| "name": "Fox Splitter", | ||
| "error": 0, | ||
| "state": 6, | ||
| "version": "2.1.2012122901.1-signed", | ||
| "progress": 517308, | ||
| "maxProgress": 517308 | ||
| } | ||
|
|
||
| Finally, to start up an add-on build with file watching: | ||
| ``` | ||
| The event `addon-install:install-ended` will include some extra properties: | ||
|
|
||
| ``` json | ||
| { | ||
| "id": "foxsplitter@sakura.ne.jp", | ||
| "name": "Fox Splitter", | ||
| "error": 0, | ||
| "state": 6, | ||
| "version": "2.1.2012122901.1-signed", | ||
| "progress": 517308, | ||
| "maxProgress": 517308, | ||
| "description": "Splits browser window as you like.", | ||
| "homepageURL": "http://piro.sakura.ne.jp/xul/foxsplitter/index.html.en", | ||
| "iconURL": "file:///tmp/074a4b62-239e-49bb-b75a-4935c349855c/extensions/foxsplitter@piro.sakura.ne.jp/icon.png", | ||
| "size": 511221, | ||
| "signedState": 2, | ||
| "permissions": 13 | ||
| } | ||
| npm start | ||
| ``` | ||
|
|
||
| #### Talking to the add-on | ||
| This command will watch as you edit files, rebuilding the add-on with every change and re-installing it in your browser by way of the web-ext tool. | ||
|
|
||
| You will need to setup the following function (or an equivalent) to send messages to the add-on. | ||
| 1. make sure you select the correct environment in the test pilot options menu from `about:addons`. Defaults to 'production'. | ||
|
|
||
| ``` javascript | ||
| function sendToAddon (data) { | ||
| document.documentElement.dispatchEvent(new CustomEvent( | ||
| 'from-web-to-addon', { bubbles: true, detail: data } | ||
| )); | ||
| } | ||
| ``` | ||
| Then you can use the `sendToAddon` method to send messages. | ||
| ## General architecture | ||
|
|
||
| ``` javascript | ||
| sendToAddon({type: 'loaded'}); | ||
| ``` | ||
| and to setup listeners. | ||
|
|
||
| ``` javascript | ||
| window.addEventListener("from-addon-to-web", function (event) { | ||
| if (!event.detail || !event.detail.type) { return; } | ||
| statusUpdate(event.detail.type, event.detail); | ||
| switch (event.detail.type) { | ||
| case 'sync-installed': | ||
| syncInstalledAddons(event.detail); | ||
| break; | ||
| default: | ||
| console.log('WEB RECEIVED FROM ADDON', JSON.stringify(event.detail, null, ' ')); | ||
| break; | ||
| } | ||
| }, false); | ||
| ``` | ||
| This add-on is a [WebExtension][]. | ||
|
|
||
| Submit updates: | ||
| ``` javascript | ||
| sendToAddon({type: 'install-experiment', detail: {xpi_url: 'https://people.mozilla.com/~jhirsch/universal-search-addon/addon.xpi'}}); | ||
| ``` | ||
| [WebExtension]: https://developer.mozilla.org/en-US/Add-ons/WebExtensions | ||
|
|
||
| The entry point for the webextension lives in [`./background.js`](./background.js). | ||
|
|
||
| ## Maintainers | ||
| * A daily report of how many experiments are currently enabled | ||
|
|
||
| * Dave Justice <djustice@mozilla.com> | ||
| * Les Orchard <lorchard@mozilla.com> | ||
|
|
||
| ## Attribution | ||
| The details of what measurements are implemented for any particular experiment can be found with the respective source code repositories for each experiment, generally in a document at `docs/METRICS.md`. | ||
|
|
||
| Arrow Icon made by | ||
| [Appzgear](http://www.flaticon.com/authors/appzgear) from | ||
| [www.flaticon.com](http://www.flaticon.com) is licensed by | ||
| [CC BY 3.0](http://creativecommons.org/licenses/by/3.0/) | ||
| For more general details on where these metric events are sent for measurement & | ||
| analysis, read [`../docs/metrics/ga.md`](../docs/metrics/ga.md). |
| @@ -0,0 +1,186 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| /* global browser, currentEnvironment */ | ||
|
|
||
| function log(...args) { | ||
| // console.log(...["[TESTPILOT v2] (background)"].concat(args)); // eslint-disable-lint no-console | ||
| } | ||
|
|
||
| const storage = browser.storage.local; | ||
| const RESOURCE_UPDATE_INTERVAL = 4 * 60 * 60 * 1000; // 4 hours | ||
| const ONE_DAY = 60 * 60 * 1000 * 24; | ||
| const TWO_WEEKS = 2 * 7 * ONE_DAY; | ||
|
|
||
| /* browser action constants */ | ||
| const BROWSER_ACTION_LINK_BASE = [ | ||
| "experiments", | ||
| "?utm_source=testpilot-addon", | ||
| "&utm_medium=firefox-browser", | ||
| "&utm_campaign=testpilot-doorhanger" | ||
| ].join(""); | ||
| const BROWSER_ACTION_LINK_NOT_BADGED = BROWSER_ACTION_LINK_BASE + | ||
| "&utm_content=not+badged"; | ||
| const BROWSER_ACTION_LINK_BADGED = BROWSER_ACTION_LINK_BASE + | ||
| "&utm_content=badged"; | ||
| let BROWSER_ACTION_LINK = BROWSER_ACTION_LINK_NOT_BADGED; | ||
|
|
||
| const resources = { | ||
| experiments: [], | ||
| news_updates: [] | ||
| }; | ||
|
|
||
| function uuidv4() { | ||
| return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => | ||
| ( | ||
| c ^ | ||
| (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) | ||
| ).toString(16) | ||
| ); | ||
| } | ||
|
|
||
| function getInstalledTxpAddons() { | ||
| return browser.management.getAll().then((infoArray) => { | ||
| const installed = infoArray.map((exp) => exp.id); | ||
| const txpAddons = resources.experiments.map((exp) => exp.id); | ||
|
|
||
| return installed.filter(function(n) { | ||
| return txpAddons.includes(n); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| async function setup() { | ||
| setInterval(fetchResources, RESOURCE_UPDATE_INTERVAL); | ||
| setupBrowserAction(); | ||
| await fetchResources(); | ||
| setDailyPing(); | ||
| } | ||
|
|
||
| async function sendPingOnAlarm(alarmInfo) { | ||
| if (alarmInfo.name === "daily-ping") { | ||
| const { lastPing } = await storage.get({"lastPing": Date.now() - ONE_DAY}); | ||
| if (((new Date) - lastPing) > ONE_DAY) { | ||
| const installedTxpAddons = await getInstalledTxpAddons(); | ||
| let { clientUUID } = await storage.get("clientUUID"); | ||
| if (!clientUUID) { | ||
| await storage.set({ clientUUID: clientUUID = uuidv4() }); | ||
| } | ||
| submitPing(alarmInfo.name, "installed-addons", installedTxpAddons, clientUUID); | ||
| await storage.set({lastPing: Date.now()}); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function setDailyPing() { | ||
| browser.alarms.create("daily-ping", { | ||
| delayInMinutes: 1, | ||
| periodInMinutes: 60 // check hourly | ||
| }); | ||
|
|
||
| browser.alarms.onAlarm.addListener(sendPingOnAlarm); | ||
| } | ||
|
|
||
| function setupBrowserAction() { | ||
| log("setupBrowserAction"); | ||
| browser.browserAction.setBadgeBackgroundColor({ color: "#0996f8" }); | ||
| browser.browserAction.onClicked.addListener(() => { | ||
| // reset badge immediately | ||
| browser.browserAction.setBadgeText({ text: "" }); | ||
| storage.set({ clicked: Date.now() }); | ||
| browser.tabs.create({ | ||
| url: `${currentEnvironment.baseUrl}${BROWSER_ACTION_LINK}` | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| function fetchResources() { | ||
| log("fetchResources"); | ||
| return Promise.all( | ||
| Object.keys(resources).map(path => | ||
| fetch(`${currentEnvironment.baseUrl}api/${path}.json`) | ||
| .then(res => res.json()) | ||
| .then((data) => data.results ? data.results : data) | ||
| .then((data) => [path, data]) | ||
| .catch(err => { | ||
| log("fetchResources error", path, err); | ||
| return [path, null]; | ||
| })) | ||
| ).then(results => { | ||
| log("fetchResources results", results); | ||
| results.forEach(([path, data]) => (resources[path] = data)); | ||
| updateBadgeTextOnNew(resources.experiments, resources.news_updates); | ||
| log("fetchResources results parsed", resources); | ||
| }); | ||
| } | ||
|
|
||
| async function updateBadgeTextOnNew(experiments, news_updates) { | ||
| let { clicked } = await storage.get("clicked"); | ||
| if (!clicked) { | ||
| // Set initial button click timestamp if not found | ||
| clicked = Date.now(); | ||
| await storage.set({ clicked }); | ||
| } | ||
|
|
||
| const newExperiments = (experiments || []).filter(experiment => { | ||
| // don't include experiments which haven't launched yet | ||
| if (new Date() < new Date(experiment.launch_date)) return false; | ||
| const dt = new Date(experiment.modified || experiment.created).getTime(); | ||
| return dt >= clicked; | ||
| }); | ||
|
|
||
| // check for port number on local, we need to strip it off | ||
| // to properly fetch cookies. | ||
| const baseUrl = currentEnvironment.baseUrl; | ||
| const portIndex = baseUrl.indexOf(":8000"); | ||
| const cookieUrl = (portIndex > -1) ? baseUrl.substring(0, portIndex) : baseUrl; | ||
|
|
||
| let lastViewed = 0; | ||
| const cookie = await browser.cookies.get({ | ||
| url: cookieUrl, | ||
| name: "updates-last-viewed-date" | ||
| }); | ||
|
|
||
| if (cookie) lastViewed = cookie.value; | ||
|
|
||
| /* only show badge for news update if: | ||
| * - has the "major" key | ||
| * - update has been published in the past two weeks | ||
| * - update has not been "seen" by the frontend (lastViewed) | ||
| * - update has not been "seen" by the addon (clicked) | ||
| */ | ||
| const twoWeeksAgo = Date.now() - TWO_WEEKS; | ||
| const newsUpdates = (news_updates || []) | ||
| .filter(u => u.major) | ||
| .filter(u => new Date(u.published).getTime() >= twoWeeksAgo) | ||
| .filter(u => new Date(u.published).getTime() >= lastViewed) | ||
| .filter(u => new Date(u.published).getTime() >= clicked); | ||
|
|
||
| BROWSER_ACTION_LINK = (newExperiments.length || newsUpdates.length) > 0 | ||
| ? BROWSER_ACTION_LINK_BADGED | ||
| : BROWSER_ACTION_LINK_NOT_BADGED; | ||
|
|
||
| browser.browserAction.setBadgeText({ | ||
| text: (newExperiments.length || newsUpdates.length) > 0 ? "!" : "" | ||
| }); | ||
| } | ||
|
|
||
| function submitPing(object, event, addons, clientUUID) { | ||
| return fetch("https://ssl.google-analytics.com/collect", { | ||
| method: "POST", | ||
| body: new URLSearchParams({ | ||
| v: 1, | ||
| t: "event", | ||
| ec: "add-on Interactions", | ||
| ea: object, | ||
| el: event, | ||
| tid: "UA-49796218-47", | ||
| cid: clientUUID, | ||
| dimension14: addons | ||
| }) | ||
| }); | ||
| } | ||
|
|
||
| // Let's Go! | ||
| setup(); |
| @@ -0,0 +1,41 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| const fs = require("fs-extra"); | ||
| const packageInfo = require("../package.json"); | ||
| const manifestBase = require("../manifest-base.json"); | ||
| const manifestFilename = __dirname + "/../manifest.json"; | ||
|
|
||
| if (typeof process.env["ENVIRONMENT_TITLE"] === "undefined") { | ||
| process.env["ENVIRONMENT_TITLE"] = "local"; | ||
| process.env["ENVIRONMENT_URL"] = "https://example.com:8000/"; | ||
| } | ||
|
|
||
| const newName = (process.env["ENVIRONMENT_TITLE"] === "production") ? | ||
| "Test Pilot" : `Test Pilot ${process.env["ENVIRONMENT_TITLE"]} build`; | ||
| const newId = (process.env["ENVIRONMENT_TITLE"] === "production") ? | ||
| "@testpilot-addon" : `@testpilot-addon-${process.env["ENVIRONMENT_TITLE"]}`; | ||
|
|
||
| const currentEnvContent = ` | ||
| /* This file is generated by "bin/process-with-env" */ | ||
| // eslint-disable-next-line no-unused-vars | ||
| const currentEnvironment = { | ||
| name: "${process.env["ENVIRONMENT_TITLE"]}", | ||
| baseUrl: "${process.env["ENVIRONMENT_URL"]}" | ||
| }; | ||
| `; | ||
|
|
||
| fs.writeJson(manifestFilename, Object.assign(manifestBase, { | ||
| name: newName, | ||
| version: packageInfo.version, | ||
| applications: { | ||
| gecko: Object.assign(manifestBase.applications.gecko, {id: newId}) | ||
| }, | ||
| permissions: manifestBase.permissions.concat([process.env["ENVIRONMENT_URL"]]) | ||
| })).then(() => { | ||
| fs.writeFile("current-environment.js", currentEnvContent).then(() => { | ||
| console.log("it is complete"); | ||
| process.exit(); | ||
| }); | ||
| }).catch(err => { | ||
| console.error(err) | ||
| }); |
| @@ -1,29 +1,31 @@ | ||
| #!/usr/bin/env node | ||
|
|
||
| var fs = require('fs'); | ||
| var util = require('util'); | ||
| var childProcess = require('child_process'); | ||
|
|
||
| // Load the current manifest.json | ||
| var manifestFilename = __dirname + '/../package.json'; | ||
| var manifest = require(manifestFilename); | ||
|
|
||
| // Come up with a version tag suffix based on CIRCLE_TAG, or fall back to | ||
| // looking up the current git commit hash. | ||
| var tag; | ||
| if (process.env['CIRCLE_TAG']) { | ||
| tag = 'tag-' + process.env['CIRCLE_TAG']; | ||
| } else { | ||
| // Note: Must use short git hash, because AMO rejects versions > 32 chars | ||
| tag = 'dev-' + childProcess.execSync('git rev-parse --short HEAD') | ||
| .toString('utf-8') | ||
| .trim(); | ||
| } | ||
|
|
||
| // Update the version tag suffix. | ||
| var versionParts = manifest.version.split('-'); | ||
| manifest.version = versionParts[0] + '-' + tag; | ||
|
|
||
| // Write the modified manifest.json | ||
| var manifestJSON = JSON.stringify(manifest, null, ' '); | ||
| fs.writeFileSync(manifestFilename, manifestJSON); | ||
| #!/usr/bin/env node | ||
|
|
||
| const fs = require('fs'); | ||
| const util = require('util'); | ||
| const childProcess = require('child_process'); | ||
|
|
||
| // Load the current package.json | ||
| const packageFilename = __dirname + '/../package.json'; | ||
| const package = require(packageFilename); | ||
|
|
||
|
|
||
| // Come up with a version tag suffix based on CIRCLE_TAG, or fall back to | ||
| // looking up the current git commit hash. | ||
| const tag = 'v' + childProcess | ||
| .execSync('git rev-parse --short HEAD').toString('utf-8').trim() | ||
| .split('').sort().reverse().join(''); | ||
|
|
||
| const semver = /^(\d+\.?){1,3}/.exec(package.version)[0]; | ||
|
|
||
| // Update the version tag suffix. | ||
| package.version = semver + tag; | ||
|
|
||
| // Write the modified package.json | ||
| var packageJSON = JSON.stringify(package, null, ' '); | ||
| fs.writeFileSync(packageFilename, packageJSON); | ||
|
|
||
| var manifestFilename = __dirname + '/../manifest.json'; | ||
| var manifest = require(manifestFilename); | ||
| manifest.version = package.version; | ||
| var manifestJSON = JSON.stringify(manifest, null, ' '); | ||
| fs.writeFileSync(manifestFilename, manifestJSON); |
| @@ -0,0 +1,7 @@ | ||
|
|
||
| /* This file is generated by "bin/process-with-env" */ | ||
| // eslint-disable-next-line no-unused-vars | ||
| const currentEnvironment = { | ||
| name: "local", | ||
| baseUrl: "https://example.com:8000/" | ||
| }; |
| @@ -0,0 +1 @@ | ||
| <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M29.924 14.338a3.52 3.52 0 0 1 1.909 2.6c.183 1.66-.954 3.768-3.02 5.7.96 2.962 2.054 7.362.062 7.362-1.449 0-3.36-2.1-4.841-4.158a21.843 21.843 0 0 1-7.069 2.037A21.694 21.694 0 0 1 7.194 26.7C5.832 28.446 4.245 30 3 30c-1.664 0-1.173-3.076-.407-5.816a6.147 6.147 0 0 1-2.52-3.748c-.249-2.264 2.1-4.138 5.878-5.493C6.143 9.9 8.743 2 16 2a8.73 8.73 0 0 1 7.733 4.7A49.053 49.053 0 0 1 31 6c1 0 1 0 1 1a24.16 24.16 0 0 1-2.076 7.338zM16 4.9c-4.423 0-6 4.1-6.865 9.3C8.838 15.981 8 18 14 18c7 0 8.974-3.218 8.859-3.856C22 9.392 20.4 4.9 16 4.9z"/></svg> |
| @@ -0,0 +1,38 @@ | ||
| { | ||
| "manifest_version": 2, | ||
| "name": "Test Pilot", | ||
| "author": "Mozilla (https://mozilla.org/)", | ||
| "applications": { | ||
| "gecko": { | ||
| "id": "@testpilot-addon", | ||
| "update_url": "https://testpilot.firefox.com/files/@testpilot-addon/updates.json" | ||
| } | ||
| }, | ||
| "icons": { | ||
| "16": "icons/icon-16.png", | ||
| "32": "icons/icon-32.png", | ||
| "64": "icons/icon-64.png", | ||
| "96": "icons/icon-96.png" | ||
| }, | ||
| "description": "Test Pilot is a privacy-sensitive user research program focused on getting new features into Firefox faster.", | ||
| "homepage_url": "https://testpilot.firefox.com", | ||
| "permissions": [ | ||
| "alarms", | ||
| "management", | ||
| "notifications", | ||
| "storage", | ||
| "cookies", | ||
| "tabs" | ||
| ], | ||
| "background": { | ||
| "scripts": [ | ||
| "current-environment.js", | ||
| "background.js" | ||
| ] | ||
| }, | ||
| "browser_action": { | ||
| "default_title": "Firefox Test Pilot", | ||
| "browser_style": true, | ||
| "default_icon": "icons/icon.svg" | ||
| } | ||
| } |
| @@ -1,70 +1,34 @@ | ||
| { | ||
| "title": "Test Pilot", | ||
| "name": "testpilot-addon", | ||
| "version": "0.6.1", | ||
| "id": "@testpilot-addon", | ||
| "version": "3.0.6", | ||
| "private": true, | ||
| "description": "Test Pilot is a privacy-sensitive user research program focused on getting new features into Firefox faster.", | ||
| "repository": "mozilla/testpilot", | ||
| "homepage": "https://testpilot.firefox.com/", | ||
| "icon": "resource://@testpilot-addon/data/icon-96.png", | ||
| "bugs": { | ||
| "url": "https://github.com/mozilla/testpilot/issues" | ||
| }, | ||
| "main": "index.js", | ||
| "author": "Mozilla (https://mozilla.org/)", | ||
| "engines": { | ||
| "firefox": ">=38.0a1", | ||
| "fennec": ">=38.0a1" | ||
| }, | ||
| "updateURL": "https://testpilot.firefox.com/static/addon/update.rdf", | ||
| "updateLink": "https://testpilot.firefox.com/static/addon/addon.xpi", | ||
| "scripts": { | ||
| "start": "npm run watch", | ||
| "once": "jpm run -b beta --prefs dev-prefs.json", | ||
| "watch": "jpm watchpost --post-url http://localhost:8888", | ||
| "watch-ui": "watchify-server ui-test.js --index ui-test.html", | ||
| "lint": "eslint .", | ||
| "sign": "./bin/update-version && ./bin/sign", | ||
| "package": "jpm xpi && mv @testpilot-addon-$npm_package_version.xpi ../testpilot/frontend/static-src/addon/addon.xpi && mv @testpilot-addon-$npm_package_version.update.rdf ../testpilot/frontend/static-src/addon/update.rdf" | ||
| }, | ||
| "license": "MPL-2.0", | ||
| "devDependencies": { | ||
| "babel-eslint": "4.1.8", | ||
| "eslint": "^1.10.3", | ||
| "eslint-config-airbnb": "^0.0.8", | ||
| "jpm": "1.0.6", | ||
| "jsonwebtoken": "5.7.0", | ||
| "request": "2.69.0", | ||
| "watchify-server": "1.0.2" | ||
| "babel-eslint": "9.0.0", | ||
| "cross-env": "5.2.0", | ||
| "eslint": "5.7.0", | ||
| "eslint-plugin-import": "2.14.0", | ||
| "fs-extra": "7.0.0", | ||
| "onchange": "4.1.0", | ||
| "shx": "0.3.2", | ||
| "web-ext": "2.9.1" | ||
| }, | ||
| "preferences": [ | ||
| { | ||
| "name": "SERVER_ENVIRONMENT", | ||
| "title": "Environment", | ||
| "description": "Which Test Pilot server environment to use?", | ||
| "type": "menulist", | ||
| "value": "production", | ||
| "options": [ | ||
| { | ||
| "value": "local", | ||
| "label": "Local (testpilot.dev)" | ||
| }, | ||
| { | ||
| "value": "dev", | ||
| "label": "Dev (testpilot.dev.mozaws.net)" | ||
| }, | ||
| { | ||
| "value": "stage", | ||
| "label": "Staging (testpilot.stage.mozaws.net)" | ||
| }, | ||
| { | ||
| "value": "production", | ||
| "label": "Production (testpilot.firefox.com)" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "dependencies": { | ||
| "mustache": "2.2.1" | ||
| "scripts": { | ||
| "start": "onchange -p -v \"*.js\" -- npm run package", | ||
| "clean": "shx rm -rf web-ext-artifacts && rm -f current-environment.js && rm -f manifest.json", | ||
| "prepackage": "npm run clean && node bin/process-with-env && npm run lint && npm run lint:extension && npm run update-version", | ||
| "package": "cross-env web-ext build --ignore-files manifest-base.json package.json package-lock.json bin/update-version bin/process-with-env README.md LICENSE --overwrite-dest && shx mv web-ext-artifacts/test_pilot*.zip ./addon.xpi", | ||
| "lint": "eslint .", | ||
| "lint:extension": "web-ext lint --self-hosted", | ||
| "update-version": "node bin/update-version" | ||
| } | ||
| } |
| @@ -0,0 +1,19 @@ | ||
| #!/usr/bin/env node | ||
| const YAML = require("yamljs"); | ||
| const ContentTransformerPlugin = require(__dirname + "/../frontend/lib/content-transformer-plugin"); | ||
| const buildJSON = require(__dirname + "/../frontend/lib/content/json"); | ||
|
|
||
| const contentTransformer = new ContentTransformerPlugin({ | ||
| inputs: { | ||
| experiments: "content-src/experiments/**/*.yaml", | ||
| news_updates: "content-src/news_updates.yaml" | ||
| }, | ||
| parser: ({ file }) => YAML.parse(file.content.toString("utf8")), | ||
| transforms: [ buildJSON ] | ||
| }); | ||
|
|
||
| contentTransformer.applyOnce('frontend/build').then(result => { | ||
| console.log("Assets output:"); | ||
| Object.keys(result).forEach(key => console.log("\t", key)); | ||
| }); | ||
|
|
| @@ -0,0 +1,14 @@ | ||
| #!/bin/bash | ||
|
|
||
| echo "$(dirname $0)" | ||
| cd $(dirname $0)/.. | ||
|
|
||
| HASH=$(git --no-pager log --format=format:"%H" -1) | ||
| TAG=$(git show-ref --tags | awk "/$HASH/ {print \$NF}" | sed 's/refs.tags.//') | ||
|
|
||
| printf '{"commit":"%s","version":"%s","source":"https://github.com/mozilla/testpilot"}\n' \ | ||
| "$HASH" \ | ||
| "$TAG" \ | ||
| > version.json | ||
|
|
||
| cat version.json |
| @@ -0,0 +1,4 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
| cd addon/ | ||
| npm run package |
| @@ -0,0 +1,4 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
| NODE_ENV=production ENABLE_PONTOON=1 ENABLE_DEV_CONTENT=1 ENABLE_DEV_LOCALES=1 npm run static | ||
| zip -r frontend.zip frontend/build dist |
| @@ -0,0 +1,10 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
| printf '{"commit":"%s","version":"%s","source":"https://github.com/%s/%s","build":"%s"}\n' \ | ||
| "$CIRCLE_SHA1" \ | ||
| "$CIRCLE_TAG" \ | ||
| "$CIRCLE_PROJECT_USERNAME" \ | ||
| "$CIRCLE_PROJECT_REPONAME" \ | ||
| "$CIRCLE_BUILD_URL" \ | ||
| > version.json | ||
| cat version.json |
| @@ -0,0 +1,83 @@ | ||
| #!/usr/bin/env bash | ||
| # https://github.com/bellkev/circle-lock-test/blob/master/do-exclusively | ||
|
|
||
| # sets $branch, $tag, $rest | ||
| parse_args() { | ||
| while [[ $# -gt 0 ]]; do | ||
| case $1 in | ||
| -b|--branch) branch="$2" ;; | ||
| -t|--tag) tag="$2" ;; | ||
| *) break ;; | ||
| esac | ||
| shift 2 | ||
| done | ||
| rest=("$@") | ||
| } | ||
|
|
||
| # reads $branch, $tag, $commit_message | ||
| should_skip() { | ||
| if [[ "$branch" && "$CIRCLE_BRANCH" != "$branch" ]]; then | ||
| echo "Not on branch $branch. Skipping..." | ||
| return 0 | ||
| fi | ||
|
|
||
| if [[ "$tag" && "$commit_message" != *\[$tag\]* ]]; then | ||
| echo "No [$tag] commit tag found. Skipping..." | ||
| return 0 | ||
| fi | ||
|
|
||
| return 1 | ||
| } | ||
|
|
||
| # reads $branch, $tag | ||
| # sets $jq_prog | ||
| make_jq_prog() { | ||
| local jq_filters="" | ||
|
|
||
| if [[ $branch ]]; then | ||
| jq_filters+=" and .branch == \"$branch\"" | ||
| fi | ||
|
|
||
| if [[ $tag ]]; then | ||
| jq_filters+=" and (.subject | contains(\"[$tag]\"))" | ||
| fi | ||
|
|
||
| jq_prog=".[] | select(.build_num < $CIRCLE_BUILD_NUM and (.status | test(\"running|pending|queued\")) $jq_filters) | .build_num" | ||
| } | ||
|
|
||
|
|
||
| if [[ "$0" != *bats* ]]; then | ||
| set -e | ||
| set -u | ||
| set -o pipefail | ||
|
|
||
| branch="" | ||
| tag="" | ||
| rest=() | ||
| api_url="https://circleci.com/api/v1/project/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME?circle-token=$CIRCLE_TOKEN&limit=100" | ||
|
|
||
| parse_args "$@" | ||
| commit_message=$(git log -1 --pretty=%B) | ||
| if should_skip; then exit 0; fi | ||
| make_jq_prog | ||
|
|
||
| echo "Checking for running builds..." | ||
|
|
||
| while true; do | ||
| builds=$(curl -s -H "Accept: application/json" "$api_url" | jq "$jq_prog") | ||
| if [[ $builds ]]; then | ||
| echo "Waiting on builds:" | ||
| echo "$builds" | ||
| else | ||
| break | ||
| fi | ||
| echo "Retrying in 5 seconds..." | ||
| sleep 5 | ||
| done | ||
|
|
||
| echo "Acquired lock" | ||
|
|
||
| if [[ "${#rest[@]}" -ne 0 ]]; then | ||
| "${rest[@]}" | ||
| fi | ||
| fi |
| @@ -0,0 +1,11 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
|
|
||
| # Skip builds for Pontoon commits | ||
| IS_PONTOON=$(git show -s --format=%s | grep -q 'Pontoon:' && echo 'true' || echo '') | ||
| if [[ $IS_PONTOON ]]; then | ||
| echo "Skipping Integration Tests on Pontoon commit."; | ||
| exit 0; | ||
| fi | ||
|
|
||
| tox -e flake8 |
| @@ -0,0 +1,5 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
|
|
||
| npm install | ||
| cd addon && npm install |
| @@ -0,0 +1,34 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
|
|
||
| IS_PONTOON=$(git show -s --format=%s | grep -q 'Pontoon:' && echo 'true' || echo '') | ||
| if [[ $IS_PONTOON ]]; then | ||
| echo "Skipping Integration Tests on Pontoon commit."; | ||
| exit 0; | ||
| fi | ||
|
|
||
| GECKODRIVER_URL=$( | ||
| curl -s 'https://api.github.com/repos/mozilla/geckodriver/releases/latest' | | ||
| python -c "import sys, json; r = json.load(sys.stdin); print([a for a in r['assets'] if 'linux64' in a['name']][0]['browser_download_url']);" | ||
| ); | ||
|
|
||
|
|
||
| curl -L -o geckodriver.tar.gz $GECKODRIVER_URL | ||
| gunzip -c geckodriver.tar.gz | tar xopf - | ||
| chmod +x geckodriver | ||
| sudo mv geckodriver /bin | ||
| geckodriver --version | ||
| # Install pip | ||
| sudo apt-get install python-pip python-dev build-essential | ||
| sudo pip install --upgrade pip | ||
|
|
||
| sudo pip install tox mozdownload mozinstall==1.15 | ||
|
|
||
| mkdir -p ~/project/firefox-downloads/ | ||
| find ~/project/firefox-downloads/ -type f -mtime +90 -delete | ||
| mozdownload --version latest --destination ~/project/firefox-downloads/firefox/ | ||
| mozdownload --version latest-beta --destination ~/project/firefox-downloads/firefox_dev/ | ||
| mozdownload --version latest --type daily --destination ~/project/firefox-downloads/firefox_nightly/ | ||
|
|
||
| # Dependencies for firefox | ||
| sudo apt-get update && sudo apt-get install -y libgtk3.0-cil-dev libasound2 libasound2 libdbus-glib-1-2 libdbus-1-3 |
| @@ -0,0 +1,14 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
|
|
||
| DISTRIBUTION_ID=$1 | ||
|
|
||
| aws configure set preview.cloudfront true | ||
|
|
||
| INVALIDATION_ID=$(aws cloudfront create-invalidation \ | ||
| --distribution-id $DISTRIBUTION_ID \ | ||
| --paths '/*' | jq -r '.Invalidation.Id'); | ||
|
|
||
| aws cloudfront wait invalidation-completed \ | ||
| --distribution-id $DISTRIBUTION_ID \ | ||
| --id $INVALIDATION_ID |
| @@ -0,0 +1,5 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
| cd addon/ | ||
| npm run lint | ||
| npm run test |
| @@ -0,0 +1,16 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
|
|
||
| # Skip builds for Pontoon commits | ||
| IS_PONTOON=$(git show -s --format=%s | grep -q 'Pontoon:' && echo 'true' || echo '') | ||
| if [[ $IS_PONTOON ]]; then | ||
| echo "Skipping Integration Tests on Pontoon commit."; | ||
| exit 0; | ||
| fi | ||
|
|
||
| mozinstall $(ls -t /home/ubuntu/firefox-downloads/firefox_dev/*.tar.bz2 | head -1) | ||
| firefox --version | ||
| export PYTEST_ADDOPTS=--html=integration-test-results/ui-test-dev.html | ||
| export SKIP_INSTALL_TEST=True | ||
| npm start & | ||
| tox -e ui-tests |
| @@ -0,0 +1,16 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
| export PATH=~/project/firefox:$PATH | ||
|
|
||
| # Skip builds for Pontoon commits | ||
| IS_PONTOON=$(git show -s --format=%s | grep -q 'Pontoon:' && echo 'true' || echo '') | ||
| if [[ $IS_PONTOON ]]; then | ||
| echo "Skipping Integration Tests on Pontoon commit."; | ||
| exit 0; | ||
| fi | ||
|
|
||
| mozinstall $(ls -t firefox-downloads/firefox_nightly/*.tar.bz2 | head -1) | ||
| firefox --version | ||
| export PYTEST_ADDOPTS=--html=integration-test-results/ui-test-nightly.html | ||
| npm start & | ||
| tox -e ui-tests |
| @@ -0,0 +1,17 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
| export PATH=~/project/firefox:$PATH | ||
|
|
||
| # Skip builds for Pontoon commits | ||
| IS_PONTOON=$(git show -s --format=%s | grep -q 'Pontoon:' && echo 'true' || echo '') | ||
| if [[ $IS_PONTOON ]]; then | ||
| echo "Skipping Integration Tests on Pontoon commit."; | ||
| exit 0; | ||
| fi | ||
|
|
||
| mozinstall $(ls -t firefox-downloads/firefox/*.tar.bz2 | head -1) | ||
| firefox --version | ||
| export PYTEST_ADDOPTS=--html=integration-test-results/ui-test-release.html | ||
| export SKIP_INSTALL_TEST=True | ||
| npm start & | ||
| tox -e ui-tests |
| @@ -0,0 +1,7 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
|
|
||
| npm run lint | ||
| npm run l10n:check | ||
| npm run flow | ||
| npm run test:ci |
| @@ -0,0 +1,76 @@ | ||
| #!/bin/bash | ||
| set -ex | ||
|
|
||
| STORYBOOK_BUCKET=${STORYBOOK_BUCKET:-testpilot-storybook.dev.mozaws.net} | ||
| if [ -z "$STORYBOOK_BUCKET" ]; then | ||
| echo "The S3 bucket is not set. Failing." | ||
| exit 1; | ||
| fi | ||
|
|
||
| # Skip builds for Pontoon commits | ||
| IS_PONTOON=$(git show -s --format=%s | grep -q 'Pontoon:' && echo 'true' || echo '') | ||
| if [[ $IS_PONTOON ]]; then | ||
| echo "Skipping Storybook deploy on Pontoon commit."; | ||
| exit 0; | ||
| fi | ||
|
|
||
| # Skip builds for non-Mozilla users | ||
| if [ "$CIRCLE_PROJECT_USERNAME" != "mozilla" ]; then | ||
| echo "Skipping Storybook deploy for non-Mozilla CircleCI"; | ||
| exit 0; | ||
| fi | ||
|
|
||
| # Skip builds without credentials | ||
| if [[ -z "$AWS_ACCESS_KEY_ID" || -z "$AWS_SECRET_ACCESS_KEY" ]]; then | ||
| echo "Skipping Storybook deploy for missing credentials"; | ||
| exit 0; | ||
| fi | ||
|
|
||
| # HACK: Build static storybook with URL paths edited to include git hash | ||
| HASH=$(git --no-pager log --format=format:"%H" -1) | ||
| cp -r .storybook .storybook-$HASH | ||
| sed -i "s#href=\"/static#href=\"/$HASH/static#g" .storybook-$HASH/preview-head.html | ||
| # HACK: correct paths to experiment images in existing build | ||
| sed -i "s#url(/static/#url(../#g" ./frontend/build/static/styles/experiments.css | ||
| ./node_modules/.bin/build-storybook -c .storybook-$HASH -o ./frontend/storybook/$HASH | ||
| cp -r ./frontend/build/static ./frontend/storybook/$HASH | ||
|
|
||
| # Deploy the files to bucket with git hash subdirectory | ||
| aws s3 cp \ | ||
| --recursive \ | ||
| --acl "public-read" \ | ||
| ./frontend/storybook/${HASH} s3://${STORYBOOK_BUCKET}/${HASH}/ | ||
|
|
||
| STORYBOOK_URL="http://$STORYBOOK_BUCKET/$HASH/" | ||
| echo "Deployed Storybook to $STORYBOOK_URL" | ||
|
|
||
| # Deploy a client-side redirect page for a per-PR URL | ||
| if [[ ! -z $CI_PULL_REQUEST ]]; then | ||
| STORYBOOK_PR_PATH=$(echo $CI_PULL_REQUEST | cut -d/ -f6-7); | ||
| STORYBOOK_PR_NUMBER=$(echo $CI_PULL_REQUEST | cut -d/ -f7); | ||
| STORYBOOK_PR_URL=http://${STORYBOOK_BUCKET}/${STORYBOOK_PR_PATH}/index.html | ||
| REDIRECT_HTML=$(cat <<EOF | ||
| <!DOCTYPE html> | ||
| <html> | ||
| <head> | ||
| <title>Storybook for ${STORYBOOK_PR_PATH}</title> | ||
| <meta http-equiv="refresh" content="0;URL='${STORYBOOK_URL}'" /> | ||
| </head> | ||
| <body> | ||
| <p><a href="${STORYBOOK_PR_URL}">${STORYBOOK_URL}</a></p> | ||
| </body> | ||
| </html> | ||
| EOF | ||
| ) | ||
| echo $REDIRECT_HTML | \ | ||
| aws s3 cp --content-type "text/html" --acl "public-read" - \ | ||
| s3://${STORYBOOK_BUCKET}/${STORYBOOK_PR_PATH}/index.html | ||
|
|
||
| echo "Deployed Storybook PR redirect to ${STORYBOOK_PR_URL}" | ||
|
|
||
| curl \ | ||
| -H'Content-Type: application/json' \ | ||
| -H"Authorization: token $STORYBOOK_GITHUB_ACCESS_TOKEN" \ | ||
| --data "{\"body\":\"# Storybook Deployment\\nPull Request: $STORYBOOK_PR_URL\\nCommit: $STORYBOOK_URL\"}" \ | ||
| https://api.github.com/repos/mozilla/testpilot/issues/${STORYBOOK_PR_NUMBER}/comments | ||
| fi |
| @@ -0,0 +1,164 @@ | ||
| #!/bin/bash | ||
|
|
||
| # This file is used to deploy Test Pilot to an S3 bucket. It expects to be run | ||
| # from the root of the Test Pilot directory and you'll need your S3 bucket name | ||
| # in an environment variable $TESTPILOT_BUCKET | ||
| # | ||
| # It takes a single argument: If you're deploying to a dev instance, pass in | ||
| # "dev" and it will tweak the rules slightly: | ||
| # | ||
| # ./deploy.sh dev | ||
| # | ||
| # Questions? Hit up #testpilot on IRC | ||
|
|
||
| if [ ! -d "dist" ]; then | ||
| echo "Can't find /dist/ directory. Are you running from the Test Pilot root?" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ -z "$TESTPILOT_BUCKET" ]; then | ||
| echo "The S3 bucket is not set. Failing." | ||
| exit 1 | ||
| fi | ||
|
|
||
| if [ "$1" = "dev" ]; then | ||
| DEST="dev" | ||
| elif [ "$1" = "stage" ]; then | ||
| DEST="stage" | ||
| fi | ||
|
|
||
|
|
||
| # The basic strategy is to sync all the files that need special attention | ||
| # first, and then sync everything else which will get defaults | ||
|
|
||
|
|
||
| # For short-lived assets; in seconds | ||
| TEN_MINUTES="600" | ||
|
|
||
| # For long-lived assets; in seconds | ||
| ONE_YEAR="31536000" | ||
|
|
||
| HPKP="\"Public-Key-Pins\": \"max-age=300;pin-sha256=\\\"WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=\\\";pin-sha256=\\\"r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=\\\";pin-sha256=\\\"YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=\\\";pin-sha256=\\\"sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis=\\\";\"" | ||
|
|
||
| # HACK: If this is changed, be sure to update the CSP constant in frontend/lib/dev-server.js | ||
| CSP="\"content-security-policy\": \"default-src 'self'; connect-src 'self' https://sentry.prod.mozaws.net https://www.google-analytics.com https://ssl.google-analytics.com https://location.services.mozilla.com https://basket.mozilla.org; font-src 'self' https://code.cdn.mozilla.net; form-action 'none'; frame-ancestors 'self'; img-src 'self' https://ssl.google-analytics.com https://www.google-analytics.com; object-src 'none'; script-src 'self' https://ssl.google-analytics.com; style-src 'self' https://code.cdn.mozilla.net; report-uri /__cspreport__; frame-src https://www.youtube.com;\"" | ||
| HSTS="\"strict-transport-security\": \"max-age=${ONE_YEAR}; includeSubDomains; preload\"" | ||
| TYPE="\"x-content-type-options\": \"nosniff\"" | ||
| XSS="\"x-xss-protection\": \"1; mode=block\"" | ||
| ACAO="\"Access-Control-Allow-Origin\": \"*\"" | ||
|
|
||
| # Our dev server has a couple different rules to allow easier debugging and | ||
| # enable localization. Also expires more often. | ||
| if [ "$DEST" = "dev" ]; then | ||
| TEN_MINUTES="15" | ||
| ONE_YEAR="15" | ||
|
|
||
| # HACK: If this is changed, be sure to update the CSP constant in frontend/lib/dev-server.js | ||
| CSP="\"content-security-policy\": \"default-src 'self'; connect-src 'self' https://sentry.prod.mozaws.net https://www.google-analytics.com https://ssl.google-analytics.com https://location.services.mozilla.com https://basket.mozilla.org; font-src 'self' https://code.cdn.mozilla.net; form-action 'none'; frame-ancestors 'self' https://pontoon.mozilla.org; img-src 'self' https://pontoon.mozilla.org https://ssl.google-analytics.com https://www.google-analytics.com; object-src 'none'; script-src 'self' https://pontoon.mozilla.org https://ssl.google-analytics.com; style-src 'self' https://pontoon.mozilla.org https://code.cdn.mozilla.net; report-uri /__cspreport__; frame-src https://www.youtube.com;\"" | ||
| fi | ||
|
|
||
| # build version.json if it isn't provided | ||
| [ -e version.json ] || $(dirname $0)/build-version-json.sh | ||
|
|
||
| if [ -e version.json ]; then | ||
| mv version.json dist/__version__ | ||
| # __version__ JSON; short cache | ||
| aws s3 cp \ | ||
| --cache-control "max-age=${TEN_MINUTES}" \ | ||
| --content-type "application/json" \ | ||
| --metadata "{${ACAO}, ${HPKP}, ${HSTS}, ${TYPE}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/__version__ s3://${TESTPILOT_BUCKET}/__version__ | ||
| fi | ||
|
|
||
| # HTML; short cache | ||
| aws s3 sync \ | ||
| --cache-control "max-age=${TEN_MINUTES}" \ | ||
| --content-type "text/html" \ | ||
| --exclude "*" \ | ||
| --include "*.html" \ | ||
| --metadata "{${HPKP}, ${CSP}, ${HSTS}, ${TYPE}, ${XSS}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/ s3://${TESTPILOT_BUCKET}/ | ||
|
|
||
| # JSON; short cache | ||
| aws s3 sync \ | ||
| --cache-control "max-age=${TEN_MINUTES}" \ | ||
| --content-type "application/json" \ | ||
| --exclude "*" \ | ||
| --include "*.json" \ | ||
| --metadata "{${ACAO}, ${HPKP}, ${HSTS}, ${TYPE}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/ s3://${TESTPILOT_BUCKET}/ | ||
|
|
||
| # XPI; short cache; amazon won't detect the content-type correctly | ||
| aws s3 sync \ | ||
| --cache-control "max-age=${TEN_MINUTES}" \ | ||
| --content-type "application/x-xpinstall" \ | ||
| --exclude "*" \ | ||
| --include "*.xpi" \ | ||
| --metadata "{${HPKP}, ${HSTS}, ${TYPE}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/ s3://${TESTPILOT_BUCKET}/ | ||
|
|
||
| # RDF; short cache; amazon won't detect the content-type correctly | ||
| aws s3 sync \ | ||
| --cache-control "max-age=${TEN_MINUTES}" \ | ||
| --content-type "text/rdf" \ | ||
| --exclude "*" \ | ||
| --include "*.rdf" \ | ||
| --metadata "{${HPKP}, ${HSTS}, ${TYPE}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/ s3://${TESTPILOT_BUCKET}/ | ||
|
|
||
| # l10n files; short cache; | ||
| aws s3 sync \ | ||
| --cache-control "max-age=${TEN_MINUTES}" \ | ||
| --exclude "*" \ | ||
| --include "*.ftl" \ | ||
| --metadata "{${HPKP}, ${HSTS}, ${TYPE}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/ s3://${TESTPILOT_BUCKET}/ | ||
|
|
||
| # SVG; cache forever, assign correct content-type | ||
| aws s3 sync \ | ||
| --cache-control "max-age=${ONE_YEAR}, immutable" \ | ||
| --content-type "image/svg+xml" \ | ||
| --exclude "*" \ | ||
| --include "*.svg" \ | ||
| --metadata "{${HPKP}, ${HSTS}, ${TYPE}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/ s3://${TESTPILOT_BUCKET}/ | ||
|
|
||
| # Everything else; cache forever, because it has hashes in the filenames | ||
| aws s3 sync \ | ||
| --delete \ | ||
| --exclude "*.rdf" \ | ||
| --exclude "*.xpi" \ | ||
| --cache-control "max-age=${ONE_YEAR}, immutable" \ | ||
| --metadata "{${HPKP}, ${HSTS}, ${TYPE}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| dist/ s3://${TESTPILOT_BUCKET}/ | ||
|
|
||
| # HTML - `path/index.html` to `path` resources; short cache | ||
| for fn in $(find dist -name 'index.html' -not -path 'dist/index.html'); do | ||
| s3path=${fn#dist/} | ||
| s3path=${s3path%/index.html} | ||
| aws s3 cp \ | ||
| --cache-control "max-age=${TEN_MINUTES}" \ | ||
| --content-type "text/html" \ | ||
| --exclude "*" \ | ||
| --include "*.html" \ | ||
| --metadata "{${HPKP}, ${CSP}, ${HSTS}, ${TYPE}, ${XSS}}" \ | ||
| --metadata-directive "REPLACE" \ | ||
| --acl "public-read" \ | ||
| $fn s3://${TESTPILOT_BUCKET}/${s3path} | ||
| done |
| @@ -0,0 +1,4 @@ | ||
| #!/usr/bin/env node | ||
| const { DevServer } = require(__dirname + '/../frontend/lib/dev-server'); | ||
| const { PORT = 8000, STATIC_ROOT = 'frontend/build' } = process.env; | ||
| DevServer({ PORT, STATIC_ROOT }); |
| @@ -0,0 +1,18 @@ | ||
| #!/usr/bin/env node | ||
| const YAML = require("yamljs"); | ||
| const ContentTransformerPlugin = require(__dirname + "/../frontend/lib/content-transformer-plugin"); | ||
| const buildL10N = require(__dirname + "/../frontend/lib/content/l10n"); | ||
|
|
||
| const contentTransformer = new ContentTransformerPlugin({ | ||
| inputs: { | ||
| experiments: "content-src/experiments/**/*.yaml", | ||
| news_updates: "content-src/news_updates.yaml" | ||
| }, | ||
| parser: ({ file }) => YAML.parse(file.content.toString("utf8")), | ||
| transforms: [ buildL10N(["."]) ] | ||
| }); | ||
|
|
||
| contentTransformer.applyOnce('.').then(result => { | ||
| console.log("Assets output:"); | ||
| Object.keys(result).forEach(key => console.log("\t", key)); | ||
| }); |