232 lines
9.0 KiB
TypeScript
232 lines
9.0 KiB
TypeScript
import { danger, fail, markdown, message, warn } from 'danger'
|
||
|
||
// Other ideas:
|
||
// - verify TODO have work items linked
|
||
|
||
function getIndicesOf(searchStr: string, str: string): number[] {
|
||
var searchStrLen = searchStr.length;
|
||
if (searchStrLen == 0) {
|
||
return [];
|
||
}
|
||
var startIndex = 0, index, indices: number[] = [];
|
||
while ((index = str.indexOf(searchStr, startIndex)) > -1) {
|
||
indices.push(index);
|
||
startIndex = index + searchStrLen;
|
||
}
|
||
return indices;
|
||
}
|
||
|
||
async function processAddChanges() {
|
||
const updatedTsFiles = danger.git.modified_files
|
||
.concat(danger.git.created_files)
|
||
.filter((file) => (file.endsWith('.ts') || file.endsWith('.tsx')) && !file.includes('dangerfile.ts'))
|
||
|
||
const changes = (await Promise.all(updatedTsFiles.flatMap(async (file) => {
|
||
const structuredDiff = await danger.git.structuredDiffForFile(file);
|
||
|
||
return (structuredDiff?.chunks || []).flatMap((chunk) => {
|
||
return chunk.changes.filter((change) => change.type === 'add')
|
||
})
|
||
}))).flatMap((x) => x)
|
||
|
||
// Checks for any logging and reminds the developer not to log sensitive data
|
||
if (changes.some((change) => change.content.includes('logMessage') || change.content.includes('logger.'))) {
|
||
warn('You are logging data. Please confirm that nothing sensitive is being logged!')
|
||
}
|
||
|
||
// Check for direct logging calls
|
||
if (changes.some((change) => change.content.includes('analytics.sendEvent'))) {
|
||
warn(`You are using the direct analytics call. Please use the typed wrapper for your given surface if possible!`)
|
||
}
|
||
|
||
// Check for UI package imports that are longer than needed
|
||
const validLongerImports = [`'ui/src'`, `'ui/src/theme'`, `'ui/src/loading'`]
|
||
const longestImportLength = Math.max(...validLongerImports.map((i) => i.length))
|
||
changes.forEach((change) => {
|
||
const indices = getIndicesOf(`from 'ui/src/`, change.content)
|
||
|
||
indices.forEach((idx) => {
|
||
const potentialSubstring = change.content.substring(idx, Math.min(change.content.length, idx + longestImportLength + 6 + 1))
|
||
if (!validLongerImports.some((validImport) => potentialSubstring.includes(validImport))) {
|
||
const endOfImport = change.content.indexOf(`'`, idx + 6) // skipping the "from '"
|
||
warn(`It looks like you have a longer import from 'ui/src' than needed ('${change.content.substring(idx + 6, endOfImport)}'). Please use one of [${validLongerImports.join(', ')}] when possible!`)
|
||
}
|
||
})
|
||
})
|
||
|
||
// Check for non-recommended sentry usage
|
||
if (changes.some((change) => /logger\.error\(\s*new Error\(/.test(change.content))) {
|
||
warn(`It appears you may be manually logging a Sentry error. Please log the error directly if possible. If you need to use a custom error message, ensure the error object is added to the 'cause' property.`)
|
||
}
|
||
if (changes.some((change) => /logger\.error\(\s*['`"]/.test(change.content))) {
|
||
warn(`Please log an error, not a string!`)
|
||
}
|
||
|
||
// Check for incorrect usage of `createSelector`
|
||
if (changes.some((change) => change.content.includes(`createSelector(`))) {
|
||
warn("You've added a new call to `createSelector()`. This is Ok, but please make sure you're using it correctly and you're not creating a new selector on every render. See PR #5172 for details.")
|
||
}
|
||
if (changes.some((change) => /(useAppSelector|appSelect|select)\(\s*makeSelect/.test(change.content))) {
|
||
fail(`It appears you may be creating a new selector on every render. See PR #5172 for details on how to fix this.`)
|
||
}
|
||
}
|
||
|
||
async function checkCocoaPodsVersion() {
|
||
const updatedPodFileLock = danger.git.modified_files.find((file) => file.includes('ios/Podfile.lock'))
|
||
if (updatedPodFileLock) {
|
||
const structuredDiff = await danger.git.structuredDiffForFile(updatedPodFileLock);
|
||
const changedLines = (structuredDiff?.chunks || []).flatMap((chunk) => {
|
||
return chunk.changes.filter((change) => change.type === 'add')
|
||
})
|
||
const changedCocoaPodsVersion = changedLines.some((change) => change.content.includes('COCOAPODS: '))
|
||
if (changedCocoaPodsVersion) {
|
||
fail(`You're changing the Podfile version! Ensure you are using the correct version. If this change is intentional, you should ignore this check and merge anyways.`)
|
||
}
|
||
}
|
||
}
|
||
|
||
async function checkApostrophes() {
|
||
const updatedTranslations = danger.git.modified_files.find((file) => file.includes('en-US.json'))
|
||
if (updatedTranslations) {
|
||
const structuredDiff = await danger.git.structuredDiffForFile(updatedTranslations);
|
||
const changedLines = (structuredDiff?.chunks || []).flatMap((chunk) => {
|
||
return chunk.changes.filter((change) => change.type === 'add')
|
||
})
|
||
changedLines.forEach((line) => {
|
||
if (line.content.includes("'")) {
|
||
fail("You added a string using the ' character. Please use the ’ character instead!")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
/* Warn about storing credentials in GH and uploading env.local to 1Password */
|
||
const envChanged = danger.git.modified_files.includes('.env.defaults')
|
||
if (envChanged) {
|
||
warn(
|
||
'Changes were made to .env.defaults. Confirm that no sensitive data is in the .env.defaults file. Sensitive data must go in .env (web) or .env.defaults.local (mobile) and then run `yarn upload-env-local` to store it in 1Password.'
|
||
)
|
||
}
|
||
|
||
// Run checks on added changes
|
||
processAddChanges()
|
||
|
||
// Check for cocoapods version change
|
||
checkCocoaPodsVersion()
|
||
|
||
// check translations use the correct apostrophes
|
||
checkApostrophes()
|
||
|
||
// Stories for new components
|
||
const createdComponents = danger.git.created_files.filter(
|
||
(f) =>
|
||
f.includes('components/buttons') ||
|
||
f.includes('components/input') ||
|
||
f.includes('components/layout/') ||
|
||
f.includes('components/text')
|
||
)
|
||
const hasCreatedComponent = createdComponents.length > 0
|
||
const createdStories = createdComponents.filter((filepath) => filepath.includes('stories/'))
|
||
const hasCreatedStories = createdStories.length > 0
|
||
if (hasCreatedComponent && !hasCreatedStories) {
|
||
warn(
|
||
'There are new primitive components, but not stories. Consider documenting the new component with Storybook'
|
||
)
|
||
}
|
||
|
||
// Warn when there is a big PR
|
||
const bigPRThreshold = 500
|
||
if (danger.github.pr.additions + danger.github.pr.deletions > bigPRThreshold) {
|
||
warn(':exclamation: Big PR')
|
||
markdown(
|
||
'> Pull Request size seems relatively large. If PR contains multiple changes, split each into separate PRs for faster, easier reviews.'
|
||
)
|
||
}
|
||
|
||
// No PR is too small to warrant a paragraph or two of summary
|
||
if (danger.github.pr.body.length < 50) {
|
||
warn(
|
||
'The PR description is looking sparse. Please consider explaining more about this PRs goal and implementation decisions.'
|
||
)
|
||
}
|
||
|
||
// Congratulate when code was deleted
|
||
if (danger.github.pr.additions < danger.github.pr.deletions) {
|
||
message(
|
||
`✂️ Thanks for removing ${danger.github.pr.deletions - danger.github.pr.additions} lines!`
|
||
)
|
||
}
|
||
|
||
// Stories congratulations
|
||
const stories = danger.git.fileMatch('**/*stories*')
|
||
if (stories.edited) {
|
||
message('🙌 Thanks for keeping stories up to date!')
|
||
}
|
||
|
||
// GraphQL update warnings
|
||
const updatedGraphQLfile = danger.git.modified_files.find((file) =>
|
||
file.includes('__generated__/types-and-hooks.ts')
|
||
)
|
||
|
||
if (updatedGraphQLfile) {
|
||
warn(
|
||
'You have updated the GraphQL schema. Please ensure that the Swift GraphQL Schema generation is valid by running `yarn mobile ios` and rebuilding for iOS. ' +
|
||
'You may need to add or remove generated files to the project.pbxproj. For more information see `apps/mobile/ios/WidgetsCore/MobileSchema/README.md`'
|
||
)
|
||
}
|
||
|
||
// Migrations + schema warnings
|
||
const updatedSchemaFile = danger.git.modified_files.find((file) =>
|
||
file.includes('src/app/schema.ts')
|
||
)
|
||
|
||
const updatedMigrationsFile = danger.git.modified_files.find((file) =>
|
||
file.includes('src/app/migrations.ts')
|
||
)
|
||
|
||
const updatedMigrationsTestFile = danger.git.modified_files.find((file) =>
|
||
file.includes('src/app/migrations.test.ts')
|
||
)
|
||
|
||
const createdSliceFile = danger.git.created_files.find((file) =>
|
||
file.toLowerCase().includes('slice')
|
||
)
|
||
|
||
const modifiedSliceFile = danger.git.modified_files.find((file) =>
|
||
file.toLowerCase().includes('slice')
|
||
)
|
||
|
||
const deletedSliceFile = danger.git.deleted_files.find((file) =>
|
||
file.toLowerCase().includes('slice')
|
||
)
|
||
|
||
if (modifiedSliceFile && (!updatedSchemaFile || !updatedMigrationsFile)) {
|
||
warn(
|
||
'You modified a slice file. If you added, renamed, or deleted required properties from state, then make sure to define a new schema and a create a migration.'
|
||
)
|
||
}
|
||
|
||
if (updatedSchemaFile && !updatedMigrationsFile) {
|
||
warn(
|
||
'You updated the schema file but not the migrations file. Make sure to also define a migration.'
|
||
)
|
||
}
|
||
|
||
if (!updatedSchemaFile && updatedMigrationsFile) {
|
||
warn(
|
||
'You updated the migrations file but not the schema. Schema always needs to be updated when a new migration is defined.'
|
||
)
|
||
}
|
||
|
||
if ((createdSliceFile || deletedSliceFile) && (!updatedSchemaFile || !updatedMigrationsFile)) {
|
||
warn('You created or deleted a slice file. Make sure to create check schema and migration is updated if needed.')
|
||
|
||
|
||
}
|
||
|
||
if (updatedMigrationsFile && !updatedMigrationsTestFile) {
|
||
fail(
|
||
'You updated the migrations file but did not write any new tests. Each migration must have a test!'
|
||
)
|
||
}
|