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

Adding Circle CI and storybook builds for PRs #105

Merged
merged 6 commits into from
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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

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
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, '')
);
}
74 changes: 74 additions & 0 deletions .circleci/scripts/metamaskbot-build-announce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#!/usr/bin/env node
const { promises: fs } = require('fs');
const path = require('path');
const fetch = require('node-fetch');
const { getHighlights } = require('./highlights');

start().catch((error) => {
console.error(error);
process.exit(1);
});

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/design-tokens/${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`;

let commentBody = artifactsBody;
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