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

Commit bf0f3ce

Browse files
Adding Circle CI and storybook builds for PRs (#105)
1 parent 339197c commit bf0f3ce

File tree

8 files changed

+621
-4
lines changed

8 files changed

+621
-4
lines changed

.circleci/config.yml

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
version: 2.1
2+
3+
executors:
4+
node-browsers:
5+
docker:
6+
- image: circleci/node:14-browsers
7+
8+
workflows:
9+
storybook:
10+
jobs:
11+
- prep-deps
12+
- prep-build-storybook:
13+
requires:
14+
- prep-deps
15+
- job-announce-storybook:
16+
requires:
17+
- prep-build-storybook
18+
19+
jobs:
20+
prep-deps:
21+
executor: node-browsers
22+
steps:
23+
- checkout
24+
- restore_cache:
25+
key: dependency-cache-v1-{{ checksum "yarn.lock" }}
26+
- run:
27+
name: Install deps
28+
command: |
29+
yarn install
30+
- save_cache:
31+
key: dependency-cache-v1-{{ checksum "yarn.lock" }}
32+
paths:
33+
- node_modules/
34+
- run:
35+
name: Postinstall
36+
command: |
37+
yarn setup
38+
- persist_to_workspace:
39+
root: .
40+
paths:
41+
- node_modules
42+
43+
prep-build-storybook:
44+
executor: node-browsers
45+
steps:
46+
- checkout
47+
- attach_workspace:
48+
at: .
49+
- run:
50+
name: build:storybook
51+
command: yarn build-storybook
52+
- persist_to_workspace:
53+
root: .
54+
paths:
55+
- storybook-static
56+
57+
job-announce-storybook:
58+
executor: node-browsers
59+
steps:
60+
- checkout
61+
- attach_workspace:
62+
at: .
63+
- store_artifacts:
64+
path: storybook-static
65+
destination: storybook
66+
- run:
67+
name: build:announce
68+
command: ./.circleci/scripts/metamaskbot-build-announce.js
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### highlights
2+
3+
the purpose of this directory is to house utilities for generating "highlight" messages for the metamaskbot comment based on changes included in the PR

.circleci/scripts/highlights/index.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { promisify } = require('util');
2+
const exec = promisify(require('child_process').exec);
3+
const storybook = require('./storybook.js');
4+
5+
module.exports = { getHighlights };
6+
7+
async function getHighlights({ artifactBase }) {
8+
let highlights = '';
9+
// here we assume the PR base branch ("target") is `develop` in lieu of doing
10+
// a query against the github api which requires an access token
11+
// see https://discuss.circleci.com/t/how-to-retrieve-a-pull-requests-base-branch-name-github/36911
12+
const changedFiles = await getChangedFiles({ target: 'develop' });
13+
console.log(`detected changed files vs develop:`);
14+
for (const filename of changedFiles) {
15+
console.log(` ${filename}`);
16+
}
17+
const announcement = await storybook.getHighlightAnnouncement({
18+
changedFiles,
19+
artifactBase,
20+
});
21+
if (announcement) {
22+
highlights += announcement;
23+
}
24+
return highlights;
25+
}
26+
27+
async function getChangedFiles({ target }) {
28+
const { stdout } = await exec(`git diff --name-only ${target}...HEAD`);
29+
const changedFiles = stdout.split('\n').slice(0, -1);
30+
return changedFiles;
31+
}
+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const path = require('path');
2+
const { promisify } = require('util');
3+
const exec = promisify(require('child_process').exec);
4+
const dependencyTree = require('dependency-tree');
5+
6+
const cwd = process.cwd();
7+
const resolutionCache = {};
8+
9+
// 1. load stories
10+
// 2. load list per story
11+
// 3. filter against files
12+
module.exports = {
13+
getHighlights,
14+
getHighlightAnnouncement,
15+
};
16+
17+
async function getHighlightAnnouncement({ changedFiles, artifactBase }) {
18+
const highlights = await getHighlights({ changedFiles });
19+
if (!highlights.length) {
20+
return null;
21+
}
22+
const highlightsBody = highlights
23+
.map((entry) => `\n- [${entry}](${urlForStoryFile(entry, artifactBase)})`)
24+
.join('');
25+
const announcement = `<details>
26+
<summary>storybook</summary>
27+
${highlightsBody}
28+
</details>\n\n`;
29+
return announcement;
30+
}
31+
32+
async function getHighlights({ changedFiles }) {
33+
const highlights = [];
34+
const storyFiles = await getAllStories();
35+
// check each story file for dep graph overlap with changed files
36+
for (const storyFile of storyFiles) {
37+
const list = await getLocalDependencyList(storyFile);
38+
if (list.some((entry) => changedFiles.includes(entry))) {
39+
highlights.push(storyFile);
40+
}
41+
}
42+
return highlights;
43+
}
44+
45+
async function getAllStories() {
46+
const { stdout } = await exec('find ui -name "*.stories.js"');
47+
const matches = stdout.split('\n').slice(0, -1);
48+
return matches;
49+
}
50+
51+
async function getLocalDependencyList(filename) {
52+
const list = dependencyTree
53+
.toList({
54+
filename,
55+
// not sure what this does but its mandatory
56+
directory: cwd,
57+
webpackConfig: `.storybook/main.js`,
58+
// skip all dependencies
59+
filter: (entry) => !entry.includes('node_modules'),
60+
// for memoization across trees: 30s -> 5s
61+
visited: resolutionCache,
62+
})
63+
.map((entry) => path.relative(cwd, entry));
64+
return list;
65+
}
66+
67+
function urlForStoryFile(filename, artifactBase) {
68+
const storyId = sanitize(filename);
69+
return `${artifactBase}/storybook/index.html?path=/story/${storyId}`;
70+
}
71+
72+
/**
73+
* Remove punctuation and illegal characters from a story ID.
74+
* See:
75+
* https://gist.github.com/davidjrice/9d2af51100e41c6c4b4a
76+
* https://github.com/ComponentDriven/csf/blame/7ac941eee85816a4c567ca85460731acb5360f50/src/index.ts
77+
*
78+
* @param {string} string - The string to sanitize.
79+
* @returns The sanitized string.
80+
*/
81+
function sanitize(string) {
82+
return (
83+
string
84+
.toLowerCase()
85+
// eslint-disable-next-line no-useless-escape
86+
.replace(/[ ¿'`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/giu, '-')
87+
.replace(/-+/gu, '-')
88+
.replace(/^-+/u, '')
89+
.replace(/-+$/u, '')
90+
);
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env node
2+
const { promises: fs } = require('fs');
3+
const path = require('path');
4+
const fetch = require('node-fetch');
5+
const { getHighlights } = require('./highlights');
6+
7+
start().catch((error) => {
8+
console.error(error);
9+
process.exit(1);
10+
});
11+
12+
async function start() {
13+
const { GITHUB_COMMENT_TOKEN, CIRCLE_PULL_REQUEST } = process.env;
14+
console.log('CIRCLE_PULL_REQUEST', CIRCLE_PULL_REQUEST);
15+
const { CIRCLE_SHA1 } = process.env;
16+
console.log('CIRCLE_SHA1', CIRCLE_SHA1);
17+
const { CIRCLE_BUILD_NUM } = process.env;
18+
console.log('CIRCLE_BUILD_NUM', CIRCLE_BUILD_NUM);
19+
const { CIRCLE_WORKFLOW_JOB_ID } = process.env;
20+
console.log('CIRCLE_WORKFLOW_JOB_ID', CIRCLE_WORKFLOW_JOB_ID);
21+
22+
if (!CIRCLE_PULL_REQUEST) {
23+
console.warn(`No pull request detected for commit "${CIRCLE_SHA1}"`);
24+
return;
25+
}
26+
27+
const CIRCLE_PR_NUMBER = CIRCLE_PULL_REQUEST.split('/').pop();
28+
const SHORT_SHA1 = CIRCLE_SHA1.slice(0, 7);
29+
const BUILD_LINK_BASE = `https://output.circle-artifacts.com/output/job/${CIRCLE_WORKFLOW_JOB_ID}/artifacts/0`;
30+
31+
const storybookUrl = `${BUILD_LINK_BASE}/storybook/index.html`;
32+
const storybookLink = `<a href="${storybookUrl}">Storybook</a>`;
33+
34+
// link to artifacts
35+
const allArtifactsUrl = `https://circleci.com/gh/MetaMask/design-tokens/${CIRCLE_BUILD_NUM}#artifacts/containers/0`;
36+
37+
const contentRows = [
38+
`storybook: ${storybookLink}`,
39+
`<a href="${allArtifactsUrl}">all artifacts</a>`,
40+
];
41+
const hiddenContent = `<ul>${contentRows
42+
.map((row) => `<li>${row}</li>`)
43+
.join('\n')}</ul>`;
44+
const exposedContent = `Builds ready [${SHORT_SHA1}]`;
45+
const artifactsBody = `<details><summary>${exposedContent}</summary>${hiddenContent}</details>\n\n`;
46+
47+
let commentBody = artifactsBody;
48+
try {
49+
const highlights = await getHighlights({ artifactBase: BUILD_LINK_BASE });
50+
if (highlights) {
51+
const highlightsBody = `### highlights:\n${highlights}\n`;
52+
commentBody += highlightsBody;
53+
}
54+
} catch (error) {
55+
console.error(`Error constructing highlight results: '${error}'`);
56+
}
57+
58+
const JSON_PAYLOAD = JSON.stringify({ body: commentBody });
59+
const POST_COMMENT_URI = `https://api.github.com/repos/metamask/design-tokens/issues/${CIRCLE_PR_NUMBER}/comments`;
60+
console.log(`Announcement:\n${commentBody}`);
61+
console.log(`Posting to: ${POST_COMMENT_URI}`);
62+
63+
const response = await fetch(POST_COMMENT_URI, {
64+
method: 'POST',
65+
body: JSON_PAYLOAD,
66+
headers: {
67+
'User-Agent': 'metamaskbot',
68+
Authorization: `token ${GITHUB_COMMENT_TOKEN}`,
69+
},
70+
});
71+
if (!response.ok) {
72+
throw new Error(`Post comment failed with status '${response.statusText}'`);
73+
}
74+
}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,6 @@ node_modules/
7272
.yarn/build-state.yml
7373
.yarn/install-state.gz
7474
.pnp.*
75+
76+
# Storybook build folder
77+
storybook-static

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@typescript-eslint/eslint-plugin": "^4.21.0",
5353
"@typescript-eslint/parser": "^4.21.0",
5454
"babel-loader": "^8.2.3",
55+
"dependency-tree": "^8.1.2",
5556
"eslint": "^7.23.0",
5657
"eslint-config-prettier": "^8.1.0",
5758
"eslint-plugin-import": "^2.22.1",
@@ -61,6 +62,7 @@
6162
"eslint-plugin-prettier": "^3.3.1",
6263
"eslint-plugin-storybook": "^0.5.6",
6364
"jest": "^26.4.2",
65+
"node-fetch": "^2.6.0",
6466
"prettier": "^2.2.1",
6567
"prettier-plugin-packagejson": "^2.2.11",
6668
"react": "^17.0.2",

0 commit comments

Comments
 (0)