https://github.com/discord/endanger
Build Dangerfiles with ease.
https://github.com/discord/endanger
Last synced: 7 months ago
JSON representation
Build Dangerfiles with ease.
- Host: GitHub
- URL: https://github.com/discord/endanger
- Owner: discord
- License: mit
- Created: 2020-11-25T21:49:42.000Z (over 5 years ago)
- Default Branch: master
- Last Pushed: 2022-06-13T22:25:28.000Z (over 3 years ago)
- Last Synced: 2025-07-29T17:37:44.559Z (7 months ago)
- Language: TypeScript
- Homepage:
- Size: 68.4 KB
- Stars: 94
- Watchers: 144
- Forks: 11
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-list - endanger
README
# endanger
> Build [Dangerfiles](https://danger.systems/js/) with ease.
- Break your Danger code into "rules".
- Only run rules when a relevant file changes.
- Make adding new rules more accessible to non-JS developers.
## Setup
```sh
npm install --save-dev endanger
```
> **Note:** Endanger requires `danger@10.5.3` and above. Please update your `danger` dependency.
Create a file system like this:
```txt
package.json
dangerfile.ts
/danger/
myFirstRule.ts
mySecondRule.ts
myThirdRule.ts
```
Then use the `run(...rules)` function from `endanger` in your dangerfile:
```js
// dangerfile.ts
import { run } from "endanger"
import myFirstRule from "./danger/myFirstRule"
import mySecondRule from "./danger/mySecondRule"
import myThirdRule from "./danger/myThirdRule"
run(
myFirstRule(),
mySecondRule(),
myThirdRule({
someOption: "foo",
}),
myThirdRule({
someOption: "bar",
}),
)
```
Now let's write your first `endanger` rule.
```ts
import { Rule } from "endanger"
export default function myFirstRule() {
return new Rule({
match: {
// "Glob" patterns of files you want to look at in this rule.
files: ["scary-directory/**"],
},
// A map of strings for different warnings/failures/etc so you don't have to
// clutter your rule code.
messages: {
// Pro-tip: The indentation will automatically be stripped away :P
myFirstWarning: `
Hey you added a new file to "scary-directory/"!
`,
},
// And here goes your code for the rule...
async run({ files, context }) {
// You can explore the state of the files you matched with your glob patterns.
for (let file of files.created) {
// Then you can report a warning/failure/etc by referencing your message
// from the map of strings above. You can also optionally include a file
// and even a line number.
context.warn("myFirstWarning", { file })
}
},
})
}
```
This rule warns you whenever you create a new file in the `scary-directory/`. But endanger makes it
easy to write lots of other types of rules.
```ts
import { Rule } from "endanger"
export default function mySecondRule() {
return new Rule({
match: {
files: ["api/routes/*.py"],
},
messages: {
foundNewRouteWithoutRateLimit: `...`,
foundRemovedRateLimit: `...`,
foundAddedRateLimit: `...`,
},
// And here goes your code for the rule...
async run({ files, context }) {
// files.modifiedOrCreated will give you a list of all files created or modified
for (let file of files.modifiedOrCreated) {
// file.created will tell you if the current file was created in this diff
if (file.created) {
// file.contains() will tell you if the file contains a string or regex
if (!(await file.contains("@ratelimit("))) {
context.warn("foundNewRouteWithoutRateLimit", { file })
}
}
// file.modifiedOnly will tell you if the current file was created in this diff
if (file.modifiedOnly) {
// file.before() returns the state of the file before the changes (if it existed)
let before = await file.before()?.contains("@ratelimit(")
let after = await file.contains("@ratelimit(")
if (before && !after) {
context.fail("foundAddedRateLimit", { file })
} else if (!before && after) {
context.message("foundAddedRateLimit", { file })
}
}
}
},
})
}
```
You can have rules that fire on things other than `files`, you could also match commits like so:
```ts
import { Rule } from "endanger"
let TICKET_REGEX = /\b(JIRA-\d+)\b/
export default function mySecondRule() {
return new Rule({
match: {
commit: [TICKET_REGEX],
},
messages: {
jiraLink: `
[View linked ticket {ticket} on JIRA](https://jira.intranet.corp/{ticket})
`,
},
async run({ commits, context }) {
for (let commit of commits) {
let match = commit.message.match(TICKET_REGEX)
if (match) {
context.message("jiraLink", {}, { ticket: match[1] })
}
}
},
})
}
```
> **Important!** You can only access `files` or `commits` in your rule if you have a `match` filter
> defined for them. And you can only access files or commits which match your defined filter.
## API
### `run`
This should be in your Dangerfile, pass [`Rule`](#rule)'s run them.
```ts
import { run } from "endanger"
import rule1 from "./danger/rule1"
import rule2 from "./danger/rule2"
import rule3 from "./danger/rule3"
run(
rule1,
rule2,
rule3,
)
```
### `Rule`
```ts
import { Rule } from "endanger"
export default function myRule() {
return new Rule({
match: {
files: ["path/to/**", "{glob,patterns}"],
commits: ["messages that contain this string", /or match this regex/],
},
messages: {
myFirstWarning: `...`,
mySecondWarning: `...`,
},
async run({ files, commits, context }) {
// ...
},
})
}
```
> **Note:** It's recommended you wrap your rules with a function so you could add options to them
> later. For example, you could run the same rule twice on different directories provided as
> options.
### `Context`
```ts
context.warn("myMessage", location?, values?)
context.fail("myMessage", location?, values?)
context.message("myMessage", location?, values?)
// examples:
context.warn("myMessage")
context.warn("myMessage", { file })
context.warn("myMessage", { file, line })
context.warn("myMessage", { file, line }, { ...values })
```
Note: Your Rule's `messages` can have also have special `{values}` in them:
```ts
new Rule({
messages: {
myMessage: `
Hello {value}!
`,
},
async run(files, context) {
context.warn("myMessage", {}, { value: "World" }) // "Hello World!"
},
})
```
### `Bytes`
This represents some readable data whether it be a [`File`](#file), [`FileState`](#FileState), or
[`Diff`](#diff).
```ts
// Read the contents of this file/diff/etc.
await bytes.contents() // "line1/nline2"
// Does this file/diff/etc contain a string or match a regex?
await bytes.contains("string") // true/false
await bytes.contains(/regex/) // true/false
```
### `Line`
> (extends [`Bytes`](#bytes))
```ts
line.lineNumber // 42
```
### `FileState`
> (extends [`Bytes`](#bytes))
```ts
// Get the file path (relative to repo root)
file.path // "path/to/file.ext"
// Get the file's name
file.name // "file.ext"
// Get the file dirname (relative to repo root)
file.dirname // "path/to"
// Get the file basename
file.basename // "file"
// Get the file extension
file.extension // ".ext"
// Does the file path match a set of glob patterns?
file.matches("path/to/**", "{glob,patterns}") // true/false
// Parse the file as JSON
await file.json() // { ... }
// Parse the file as YAML
await file.yaml() // { ... }
// Read this file line by line
await file.lines() // [Line (1), Line (2), Line (3)]
await file.lines({ after: line1 }) // [Line (2), Line (3)]
await file.lines({ before: line3 }) // [Line (1), Line (2)]
await file.lines({ after: line1, before: line3 }) // [Line (2)]
```
### `DiffLine`
> (extends [`Bytes`](#bytes))
```ts
// Has this diff line's content been addedd?
diffLine.added // true | false
// Has this diff line's content been removed?
diffLine.removed // true | false
// Has this diff line's content been changed (added or removed)?
diffLine.changed // true | false
// Is this diff line's content unchanged?
diffLine.unchanged // true | false
// What is the line number before the change?
diffLine.lineNumberBefore // number | null
// What is the line number after the change?
diffLine.lineNumberAfter // number | null
```
### `Diff`
```ts
// Only the added lines
await diff.added() // [DiffLine, DiffLine]
// Only the removed lines
await diff.removed() // [DiffLine, DiffLine]
// All of the changed lines
await diff.changed() // [DiffLine, DiffLine, DiffLine, DiffLine]
// All of the changed lines with several lines of surrounding context
await diff.unified() // [DiffLine, DiffLine, DiffLine, DiffLine, DiffLine, ...]
// Returns a JSONDiff of the file (assuming the file is JSON)
await diff.jsonDiff() // JSONDiff { ... }
// Returns a JSONPatch of the file (assuming the file is JSON)
await diff.jsonPatch() // JSONPatch { ... }
// Get stats on the diff (number of changed/added/removed/etc lines)
await diff.stats() // { changed: 5, added: 3, removed: 2, before: 2, after: 3 }
// Test if the diff contains changes greater than one of these thresholds
// (Thresholds are 0-1 as percentages)
await diff.changedBy({ total: 0.5 }) // true/false
await diff.changedBy({ added: 0.3 }) // true/false
await diff.changedBy({ removed: 0.2 }) // true/false
await diff.changedBy({ added: 0.3, removed: 0.2 }) // true/false
```
### `File`
> (extends [`FileState`](#filestate))
```ts
// Has the file been created?
file.created // true/false
// Has the file been deleted?
file.deleted // true/false
// Has the file been modified? (This doesn't include created files)
file.modifiedOnly // true/false
// Has the file been modified or created?
file.modifiedOrCreated // true/false
// Has the file been touched (created, modified, or deleted)?
file.touched // true/false
// Has the file been moved from another location?
await file.moved() // true/false
// Get the state of the file before all the changes made.
file.before() // File | null
// Get information about the diff of the file
file.diff() // Diff
```
### `Files`
> (extends [`Bytes`](#bytes))
```ts
// Get all of the created files.
files.created // [File, File, ...]
// Get all of the deleted files.
files.deleted // [File, File, ...]
// Get all of the modified (not including created) files.
files.modifiedOnly // [File, File, ...]
// Get all of the modified and created files.
files.modifiedOrCreated // [File, File, ...]
// Get all of the touched (created, modified, or deleted) files.
files.touched // [File, File, ...]
// Get all of the untouched files.
files.untouched // [File, File, ...]
// Get all files regardless of if they have been touched or not.
files.all // [File, File, ...]
// Get a specific file. (throws if it doesn't exist)
files.get("path/to/file.ext") // File
// Filter files by a set of glob patterns
files.matches("path/to/**", "{glob,patterns}") // Files
```