redbadger/website-honestly

View on GitHub
lighthouse/runner.js

Summary

Maintainability
A
0 mins
Test Coverage
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
const fs = require('fs');
const fetch = require('node-fetch');

const repoFullName = process.env.GITHUB_REPOSITORY;
const githubEventPath = process.env.GITHUB_EVENT_PATH;
const githubAuthToken = process.env.GITHUB_TOKEN;

/* eslint-disable no-use-before-define */

function runLighthouse({ targetUrl }) {
  const chromeFlags = [
    '--headless',
    '--no-sandbox', // chrome sandboxing requires docker container to have the
    // `SYS_ADMIN` capability added which is not supported by GitHub actions
  ];

  return chromeLauncher.launch({ chromeFlags }).then(chrome => {
    const opts = {
      port: chrome.port,
      output: 'html',
    };

    const config = null;

    return lighthouse(targetUrl, opts, config).then(results => {
      // use results.lhr for the JS-consumable output
      // https://github.com/GoogleChrome/lighthouse/blob/master/types/lhr.d.ts
      // use results.report for the HTML/JSON/CSV output as a string
      // use results.artifacts for the trace/screenshots/other specific case you need (rarer)
      return chrome.kill().then(() => results);
    });
  });
}

function readArgumentsFromEnvironment() {
  if (!githubEventPath) {
    // eslint-disable-next-line no-console
    console.warn(`GITHUB_EVENT_PATH environment variable is not set, running locally.`);
    return {
      targetUrl: 'https://www-staging.red-badger.com/658e7ac/',
      ref: '7865779',
      repo: {
        owner: 'redbadger',
        name: 'website-honestly',
      },
    };
  }

  const { deployment } = JSON.parse(fs.readFileSync(githubEventPath));

  const [owner, name] = repoFullName.split('/');

  return {
    targetUrl: deployment.url,
    ref: deployment.ref,
    repo: {
      owner,
      name,
    },
  };
}

function run() {
  const { targetUrl, repo, ref } = readArgumentsFromEnvironment();

  runLighthouse({ targetUrl }).then(results => reportResults({ results, repo, ref }));
}

async function reportResults({ results, repo, ref }) {
  const scores = Object.entries(results.lhr.categories)
    .map(([categoryName, { score }]) => `| ${categoryName} | ${score * 100} |`)
    .join('\n');

  const body = `
⚡️ Lighthouse scores for latest changes:


| Category | Score |
| -------- | ----- |
${scores}
  `;

  const pullRequestIds = await getPullRequestIdsForRef({ repo, ref });

  await Promise.all(pullRequestIds.map(issueId => createComment({ body, issueId })));
}

async function createComment({ body, issueId }) {
  const query = `
  mutation AddLighthouseComment($IssueId: ID!, $Body: String!) {
    addComment(input: {subjectId: $IssueId, body: $Body}) {
      clientMutationId
    }
  }
  `;

  const variables = {
    IssueId: issueId,
    Body: body,
  };

  await gitHubGraphqlRequest({ query, variables });
}

async function getPullRequestIdsForRef({ repo: { owner, name }, ref }) {
  const query = `
  query PullRequestIdsForRef($RepoOwner: String!, $RepoName: String!, $Ref: String!) {
    repository(owner: $RepoOwner, name: $RepoName) {
      object(expression: $Ref) {
        ...on Commit {
          associatedPullRequests(first: 10) {
            nodes {
              id
            }
          }
        }
      }
    }
  }
  `;

  const variables = {
    RepoOwner: owner,
    RepoName: name,
    Ref: ref,
  };

  const {
    repository: {
      object: { associatedPullRequests },
    },
  } = await gitHubGraphqlRequest({ query, variables });

  if (!associatedPullRequests) return [];

  return associatedPullRequests.nodes.map(({ id }) => id);
}

async function gitHubGraphqlRequest({ query, variables }) {
  const response = await fetch(`https://api.github.com/graphql`, {
    method: 'POST',
    headers: {
      Authorization: `bearer ${githubAuthToken}`,
    },
    body: JSON.stringify({ query, variables }),
  });

  if (!response.ok) {
    throw new Error(`Bad response from GitHub api: ${response.statusText}`);
  }

  const { data } = await response.json();

  return data;
}

run();

process.on('unhandledRejection', error => {
  // eslint-disable-next-line no-console
  console.error(error);
  process.exit(1);
});