Skip to content
This repository has been archived by the owner on Nov 26, 2024. It is now read-only.

Commit

Permalink
Adding circle ci and storybook builds to each PR
Browse files Browse the repository at this point in the history
  • Loading branch information
georgewrmarshall committed Apr 27, 2022
1 parent 0246771 commit a0ce79e
Show file tree
Hide file tree
Showing 8 changed files with 638 additions and 4 deletions.
89 changes: 89 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
version: 2.1

executors:
node-browsers:
docker:
- image: circleci/node:14-browsers

workflows:
storybook:
jobs:
- prep-deps
- prep-build-storybook:
requires:
- prep-deps
- job-announce-storybook:
requires:
- prep-build-storybook
- job-publish-storybook:
filters:
branches:
only: main
requires:
- prep-build-storybook

jobs:
prep-deps:
executor: node-browsers
steps:
- checkout
- restore_cache:
key: dependency-cache-v1-{{ checksum "yarn.lock" }}
- run:
name: Install deps
command: |
yarn install
- save_cache:
key: dependency-cache-v1-{{ checksum "yarn.lock" }}
paths:
- node_modules/
- run:
name: Postinstall
command: |
yarn setup
- persist_to_workspace:
root: .
paths:
- node_modules

prep-build-storybook:
executor: node-browsers
steps:
- checkout
- attach_workspace:
at: .
- run:
name: build:storybook
command: yarn build-storybook
- persist_to_workspace:
root: .
paths:
- storybook-static

job-announce-storybook:
executor: node-browsers
steps:
- checkout
- attach_workspace:
at: .
- store_artifacts:
path: storybook-static
destination: storybook
- run:
name: build:announce
command: ./.circleci/scripts/metamaskbot-build-announce.js

job-publish-storybook:
executor: node-browsers
steps:
- add_ssh_keys:
fingerprints:
- '3d:49:29:f4:b2:e8:ea:af:d1:32:eb:2a:fc:15:85:d8'
- checkout
- attach_workspace:
at: .
- run:
name: storybook:deploy
command: |
git remote add storybook [email protected]:MetaMask/metamask-storybook.git
yarn storybook:deploy
3 changes: 3 additions & 0 deletions .circleci/scripts/highlights/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### highlights

the purpose of this directory is to house utilities for generating "highlight" messages for the metamaskbot comment based on changes included in the PR
31 changes: 31 additions & 0 deletions .circleci/scripts/highlights/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const storybook = require('./storybook.js');

module.exports = { getHighlights };

async function getHighlights({ artifactBase }) {
let highlights = '';
// here we assume the PR base branch ("target") is `develop` in lieu of doing
// a query against the github api which requires an access token
// see https://discuss.circleci.com/t/how-to-retrieve-a-pull-requests-base-branch-name-github/36911
const changedFiles = await getChangedFiles({ target: 'develop' });
console.log(`detected changed files vs develop:`);
for (const filename of changedFiles) {
console.log(` ${filename}`);
}
const announcement = await storybook.getHighlightAnnouncement({
changedFiles,
artifactBase,
});
if (announcement) {
highlights += announcement;
}
return highlights;
}

async function getChangedFiles({ target }) {
const { stdout } = await exec(`git diff --name-only ${target}...HEAD`);
const changedFiles = stdout.split('\n').slice(0, -1);
return changedFiles;
}
91 changes: 91 additions & 0 deletions .circleci/scripts/highlights/storybook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const path = require('path');
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);
const dependencyTree = require('dependency-tree');

const cwd = process.cwd();
const resolutionCache = {};

// 1. load stories
// 2. load list per story
// 3. filter against files
module.exports = {
getHighlights,
getHighlightAnnouncement,
};

async function getHighlightAnnouncement({ changedFiles, artifactBase }) {
const highlights = await getHighlights({ changedFiles });
if (!highlights.length) {
return null;
}
const highlightsBody = highlights
.map((entry) => `\n- [${entry}](${urlForStoryFile(entry, artifactBase)})`)
.join('');
const announcement = `<details>
<summary>storybook</summary>
${highlightsBody}
</details>\n\n`;
return announcement;
}

async function getHighlights({ changedFiles }) {
const highlights = [];
const storyFiles = await getAllStories();
// check each story file for dep graph overlap with changed files
for (const storyFile of storyFiles) {
const list = await getLocalDependencyList(storyFile);
if (list.some((entry) => changedFiles.includes(entry))) {
highlights.push(storyFile);
}
}
return highlights;
}

async function getAllStories() {
const { stdout } = await exec('find ui -name "*.stories.js"');
const matches = stdout.split('\n').slice(0, -1);
return matches;
}

async function getLocalDependencyList(filename) {
const list = dependencyTree
.toList({
filename,
// not sure what this does but its mandatory
directory: cwd,
webpackConfig: `.storybook/main.js`,
// skip all dependencies
filter: (entry) => !entry.includes('node_modules'),
// for memoization across trees: 30s -> 5s
visited: resolutionCache,
})
.map((entry) => path.relative(cwd, entry));
return list;
}

function urlForStoryFile(filename, artifactBase) {
const storyId = sanitize(filename);
return `${artifactBase}/storybook/index.html?path=/story/${storyId}`;
}

/**
* Remove punctuation and illegal characters from a story ID.
* See:
* https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
* https://github.com/ComponentDriven/csf/blame/7ac941eee85816a4c567ca85460731acb5360f50/src/index.ts
*
* @param {string} string - The string to sanitize.
* @returns The sanitized string.
*/
function sanitize(string) {
return (
string
.toLowerCase()
// eslint-disable-next-line no-useless-escape
.replace(/[ ’–—―′¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/giu, '-')
.replace(/-+/gu, '-')
.replace(/^-+/u, '')
.replace(/-+$/u, '')
);
}
70 changes: 70 additions & 0 deletions .circleci/scripts/metamaskbot-build-announce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env node
const { promises: fs } = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const { getHighlights } = require('./highlights');

start().catch(console.error);

async function start() {
const { GITHUB_COMMENT_TOKEN, CIRCLE_PULL_REQUEST } = process.env;
console.log('CIRCLE_PULL_REQUEST', CIRCLE_PULL_REQUEST);
const { CIRCLE_SHA1 } = process.env;
console.log('CIRCLE_SHA1', CIRCLE_SHA1);
const { CIRCLE_BUILD_NUM } = process.env;
console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM);
const { CIRCLE_WORKFLOW_JOB_ID } = process.env;
console.log('CIRCLE_WORKFLOW_JOB_ID', CIRCLE_WORKFLOW_JOB_ID);

if (!CIRCLE_PULL_REQUEST) {
console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`);
return;
}

const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop();
const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7);
const BUILD_LINK_BASE = `https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0`;

const storybookUrl = `${BUILD_LINK_BASE}/storybook/index.html`;
const storybookLink = `<a href="${storybookUrl}">Storybook</a>`;

// link to artifacts
const allArtifactsUrl = `https://circleci.com/gh/MetaMask/metamask-extension/${CIRCLE_BUILD_NUM}#artifacts/containers/0`;

const contentRows = [
`storybook: ${storybookLink}`,
`<a href="${allArtifactsUrl}">all artifacts</a>`,
];
const hiddenContent = `<ul>${contentRows
.map((row) => `<li>${row}</li>`)
.join('\n')}</ul>`;
const exposedContent = `Builds ready [${SHORT_SHA1}]`;
const artifactsBody = `<details><summary>${exposedContent}</summary>${hiddenContent}</details>\n\n`;

try {
const highlights = await getHighlights({ artifactBase: BUILD_LINK_BASE });
if (highlights) {
const highlightsBody = `### highlights:\n${highlights}\n`;
commentBody += highlightsBody;
}
} catch (error) {
console.error(`Error constructing highlight results: '${error}'`);
}

const JSON_PAYLOAD = JSON.stringify({ body: commentBody });
const POST_COMMENT_URI = `https://api.github.com/repos/metamask/design-tokens/issues/${CIRCLE_PR_NUMBER}/comments`;
console.log(`Announcement:\n${commentBody}`);
console.log(`Posting to: ${POST_COMMENT_URI}`);

const response = await fetch(POST_COMMENT_URI, {
method: 'POST',
body: JSON_PAYLOAD,
headers: {
'User-Agent': 'metamaskbot',
Authorization: `token ${GITHUB_COMMENT_TOKEN}`,
},
});
if (!response.ok) {
throw new Error(`Post comment failed with status '${response.statusText}'`);
}
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,6 @@ node_modules/
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# Storybook build folder
storybook-static
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"babel-loader": "^8.2.3",
"dependency-tree": "^8.1.2",
"eslint": "^7.23.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.22.1",
Expand All @@ -61,6 +62,7 @@
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-storybook": "^0.5.6",
"jest": "^26.4.2",
"node-fetch": "^2.6.0",
"prettier": "^2.2.1",
"prettier-plugin-packagejson": "^2.2.11",
"react": "^17.0.2",
Expand Down
Loading

0 comments on commit a0ce79e

Please sign in to comment.