This repository has been archived by the owner on Mar 12, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 78
/
dangerfile.ts
197 lines (165 loc) · 7.67 KB
/
dangerfile.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
import { danger, fail, markdown, warn } from "danger"
import { compact, includes, uniq } from "lodash"
// TypeScript thinks we're in React Native,
// so the node API gives us errors:
import * as fs from "fs"
import * as path from "path"
import recurseSync from "recursive-readdir-sync"
const allFiles = recurseSync("./src")
// Setup
const pr = danger.github.pr
const modified = danger.git.modified_files
const bodyAndTitle = (pr.body + pr.title).toLowerCase()
// Custom modifiers for people submitting PRs to be able to say "skip this"
const acceptedNoTests = bodyAndTitle.includes("#skip_new_tests")
const typescriptOnly = (file: string) => includes(file, ".ts")
const filesOnly = (file: string) => fs.existsSync(file) && fs.lstatSync(file).isFile()
// Custom subsets of known files
const modifiedAppFiles = modified.filter(p => includes(p, "lib/")).filter(p => filesOnly(p) && typescriptOnly(p))
// Modified or Created can be treated the same a lot of the time
const touchedFiles = modified.concat(danger.git.created_files).filter(filesOnly)
const createdFiles = danger.git.created_files.filter(filesOnly)
const appOnlyFilter = (filename: string) =>
includes(filename, "src/lib/") &&
!includes(filename, "__tests__") &&
!includes(filename, "__mocks__") &&
typescriptOnly(filename)
const touchedAppOnlyFiles = touchedFiles.filter(appOnlyFilter)
const createdAppOnlyFiles = createdFiles.filter(appOnlyFilter)
const touchedComponents = touchedFiles.filter(p => includes(p, "src/lib/components") && !includes(p, "__tests__"))
// Rules
// If it's not a branch PR
if (pr.base.repo.full_name !== pr.head.repo.full_name) {
warn("This PR comes from a fork, and won't get JS generated for QA testing this PR inside the Emission Example app.")
}
// Check that every file created has a corresponding test file
const correspondingTestsForAppFiles = createdAppOnlyFiles.map(f => {
const newPath = path.dirname(f)
const name = path.basename(f).replace(".ts", "-tests.ts")
return `${newPath}/__tests__/${name}`
})
// New app files should get new test files
// Allow warning instead of failing if you say "Skip New Tests" inside the body, make it explicit.
const testFilesThatDontExist = correspondingTestsForAppFiles
.filter(f => !f.includes("Index-tests.tsx")) // skip indexes
.filter(f => !f.includes("types-tests.ts")) // skip type definitions
.filter(f => !f.includes("__stories__")) // skip stories
.filter(f => !f.includes("AppRegistry")) // skip registry, kinda untestable
.filter(f => !f.includes("Routes")) // skip routes, kinda untestable
.filter(f => !f.includes("NativeModules")) // skip modules that are native, they are untestable
.filter(f => !f.includes("lib/relay/")) // skip modules that are native, they are untestable
.filter(f => !f.includes("Storybooks/")) // skip modules that are native, they are untestable
.filter(f => !f.includes("fixtures")) // skip modules that are native, they are untestable
.filter(f => !fs.existsSync(f))
if (testFilesThatDontExist.length > 0) {
const callout = acceptedNoTests ? warn : fail
const output = `Missing Test Files:
${testFilesThatDontExist.map(f => `- \`${f}\``).join("\n")}
If these files are supposed to not exist, please update your PR body to include "#skip_new_tests".`
callout(output)
}
// A component should have a corresponding story reference, so that we're consistent
// with how the web create their components
const reactComponentForPath = filePath => {
const content = fs.readFileSync(filePath).toString()
const match = content.match(/class (.*) extends React.Component/)
if (!match || match.length === 0) {
return null
}
return match[1]
}
// Start with a full list of all Components, then look
// through all story files removing them from the list if found.
// If any are left, leave a warning.
let componentsForFiles = uniq(compact(touchedComponents.map(reactComponentForPath)))
const storyFiles = allFiles.filter(f => f.includes("__stories__/") && f.includes(".story."))
storyFiles.forEach(story => {
const content = fs.readFileSync(story, "utf8")
componentsForFiles.forEach(component => {
if (content.includes(component)) {
componentsForFiles = componentsForFiles.filter(f => f !== component)
}
})
})
if (componentsForFiles.length) {
const components = componentsForFiles.map(c => `- \`${c}\``).join("\n")
warn(`Could not find stories using these components:
${components}
`)
}
// We'd like to improve visibility of whether someone has tested on a device,
// or run through the code at all. So, to look at improving this, we're going to try appending
// a checklist, and provide useful info on how to run the code yourself inside the PR.
const splitter = `<hr data-danger="yep"/>`
const userBody = pr.body.split(splitter)[0]
const localBranch = `${pr.user.login}-${pr.number}-checkout`
const bodyFooter = `
### Tested on Device?
- [ ] @${pr.user.login}
${pr.assignees.map(assignee => `- [ ] @${assignee.login}`).join("\n")}
<details>
<summary>How to get set up with this PR?</summary>
<p> </p>
<p><b>To run on your computer:</b></p>
<pre><code>git fetch origin pull/${pr.number}/head:${localBranch}
git checkout ${localBranch}
yarn install
cd example; pod install; cd ..
open -a Simulator
yarn start</code></pre>
</p>
<p>Then run <code>xcrun simctl launch booted net.artsy.Emission</code> once a the simulator has finished booting</p>
<p><b>To run inside Eigen (prod or beta) or Emission (beta):</b> Shake the phone to get the Admin menu.</p>
<p>If you see <i>"Use Staging React Env" </i> - click that and restart, then follow the next step.</p>
<p>Click on <i>"Choose an RN build" </i> - then pick the one that says: "X,Y,Z"</p>
<p>Note: this is a TODO for PRs, currently you can only do it on master commits.</p>
</details>
`
const newBody = userBody + splitter + "\n" + bodyFooter
// The individual state of a ticked/unticket item in a markdown list should not
// require Danger to submit a new body (and thus overwrite those changes.)
const neuterMarkdownTicks = /- \[*.\]/g
if (pr.body.replace(neuterMarkdownTicks, "-") !== newBody.replace(neuterMarkdownTicks, "-")) {
// See https://github.com/artsy/emission/issues/589
// danger.github.api.pullRequests.update({...danger.github.thisPR, body: newBody })
}
if (fs.existsSync("tsc_raw.log")) {
const log = fs.readFileSync("tsc_raw.log")
if (log.length) {
fail("TypeScript hasn't passed, see below for full logs")
markdown(`### TypeScript Fails\n\n\`\`\`${log}\`\`\``)
}
}
// Show TSLint errors inline
// Yes, this is a bit lossy, we run the linter twice now, but its still a short amount of time
// Perhaps we could indicate that tslint failed somehow the first time?
if (fs.existsSync("tslint-errors.json")) {
const tslintErrors = JSON.parse(fs.readFileSync("tslint-errors.json", "utf8")) as any[]
if (tslintErrors.length) {
const errors = tslintErrors.map(error => {
const format = error.ruleSeverity === "ERROR" ? ":no_entry_sign:" : ":warning:"
const linkToFile = danger.github.utils.fileLinks([error.name])
return `* ${format} ${linkToFile} - ${error.ruleName} -${error.failure}`
})
const tslintMarkdown = `
## TSLint Issues:
${errors.join("\n")}
`
markdown(tslintMarkdown)
}
}
// Show Jest fails in the PR
import jest from "danger-plugin-jest"
if (fs.existsSync("test-results.json")) {
jest({ testResultsJsonPath: "test-results.json" })
}
const AppDelegate = fs.readFileSync("Example/Emission/AppDelegate.m", "utf8")
if (
!AppDelegate.includes("static NSString *UserID = nil;") ||
!AppDelegate.includes("static NSString *UserAccessToken = nil;")
) {
fail(
"Sensitive user credentials have been left in this PR, please remove those and sqaush the commits so no trace " +
"of them is left behind."
)
}