Skip to content

Commit

Permalink
Merge pull request #27 from danger/update_or_delete
Browse files Browse the repository at this point in the history
Update or delete comments
  • Loading branch information
orta committed Oct 28, 2016
2 parents 6ea13a9 + 3e30c44 commit a03a3e3
Show file tree
Hide file tree
Showing 7 changed files with 245 additions and 28 deletions.
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
"ClassDeclaration": false
}
}],
// Yeah, I know, but I was surprised to find that pretty clear, readable code
// would get caught by it.
"brace-style": [0],
"prefer-const": ["error", {
"destructuring": "any",
"ignoreReadBeforeAssign": false
}],
"space-before-function-paren": ["error", "never"],
"quotes": [1, "double", "avoid-escape"],
"flowtype/define-flow-type": 1,
"flowtype/require-parameter-type": 1,
"flowtype/require-parameter-type": [1, { "excludeArrowFunctions": true }],
"flowtype/require-return-type": [ 0,"always", { "annotateUndefined": "never" } ],
"flowtype/space-after-type-colon": [ 1, "always" ],
"flowtype/space-before-type-colon": [ 1, "never" ],
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ npm run flow

### Dev Life

We use quite a few semi-bleeding edge features of JS in Danger. Please see the [glossary for an overview](docs/js_glossary.md). Notably Flow, Interfaces, Async/Await and Typealiases.

You'll have a nicer experience as a developer if you use VS Code with Flow enabled, and if you install flow-typed.

``` sh
Expand All @@ -39,7 +41,10 @@ flow-typed install

( and maybe `flow-typed install jest@14`)

You can run the `danger` command globally from your dev build by running `npm run link`.
Tips:

* You can run the `danger` command globally from your dev build by running `npm run link`.
* If you're using VS Code, press Run to start an example run, should go through most of the process with a debugger attatched. Either use breakpoints, or add `debugger` to get a repl and context.

### What is the TODO?

Expand Down
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

// Add your own contribution below

* Danger edit an existing post, and delete it when it's not relevant - orta

### 0.0.3

* Danger will post a comment on a GitHub PR with any Fails - orta

### 0.0.2
Expand Down
146 changes: 146 additions & 0 deletions docs/js_glossary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
## New to ES6 + Flow?

I came to JS at a great point in time, a lot of the tools used in native code that offer so much programming safety are now available and easy to use in JS. Sometimes it feels like Ruby, but with useful editor feedback. 👍.

### Flow

[Flow](https://flowtype.org) is a fascinating tool that infers types through-out your codebase. Flow will give you realtime feedback on the health of your code, so make sure you have it installed in whatever editor you use.

Danger uses a lot of Flow, we have a lot of linters for it too, so instead of writing a function like:

```js
function getPlatformForEnv(env, source) {
return [...]
}
```

We would write it like this:

```js
function getPlatformForEnv(env: Env, source: CISource): ?Platform {
return [...]
}
```

This means that throughout the project we know the shape of the objects
that are being passed around. We do this via Interfaces as much as possible.

#### Interfaces

This is an interface:

```js
/** An API representation for a Code Review site */
export interface Platform {
/** Mainly for logging and error reporting */
name: string;
/** Used internally for getting PR/Repo metadata */
ciSource: CISource;
/** Pulls in the Code Review Metadata for inspection */
getReviewInfo: () => Promise<any>;
/** Pulls in the Code Review Diff, and offers a succinct user-API for it */
getReviewDiff: () => Promise<GitDSL>;
/** Creates a comment on the PR */
createComment: (body: string) => Promise<?Comment>;
/** Delete the main Danger comment */
deleteMainComment: () => Promise<bool>;
/** Replace the main Danger comment */
editMainComment: (newComment: string) => Promise<any>;
/** Replace the main Danger comment */
updateOrCreateComment: (newComment: string) => Promise<any>;
}
```

It defines the shape of an object, e.g. what functions/properties it will it have.
This means that you can expose the least about an object, but you can be certain that if someone refactors the object it will highlight the uses.

We use this a lot instead of object inheritence for things like CI Sources (e.g. Travis, Circle, Semaphor etc) they all have the same shape. Inside Danger we can only work against the shared interface instead of the private internals.

Here is an example of an object that conforms to that interface:

```js
export class GitHub {
token: APIToken
ciSource: CISource
name: string

constructor(token: APIToken, ciSource: CISource) {
this.token = token
this.ciSource = ciSource
this.name = "GitHub"
}

/**
* Get the Code Review description metadata
*
* @returns {Promise<any>} JSON representation
*/
async getReviewInfo() : Promise<any> {
const details = await this.getPullRequestInfo()
return await details.json()
}

[...]
```
It contains a `name` and a `ciSource`, but also a `token`. The constructor is also not inside the interface. The `token` and `constructor` are hidden to `GitHub` instances which are reffered to via the `Platform` interface. This is great.

Useful notes:

* You do not mention that a function is async here. They just need to wrap their return values.
* When an interface is changed, the Flow error message that your object does not conform to the interface will be placed on a LOC where the class is starting to be used as an interface. E.g. a place where a real instance is `new`'d and then returned as the Interface.

### Async Await

This took me a bit of time to get my head around. An Async function is just some useful syntax sugar around promises.

* You cannot use `await` inside a function that has not been declared `async`.
* Anything you do return will be implicitly wrapped in a Promise
* Non-async functions can just handle the promise an `async` function returns

So, a typical `async` function

``` js
async getReviewInfo() : Promise<any> { // this function will do async
const details = await this.getPullRequestInfo() // wait for the promise in getPullRequestInfo to resolve
return await details.json() // wait for the promise in json to resolve
} // return the json
```
And a non-async function, that is optimised for _using_ inside an async function. This happens when you have closure based callbacks, which need to be wrapped into promises.
```js
readFile(path: String): Promise<string> { // returns a promise with a string
return new Promise((resolve: any, reject: any) => { // create a new promise, with 2 callbacks
fs.readFile(path, "utf8", (err: Error, data: string) => { // do the work
if (err) { return reject(err) } // did it fail? reject the promise
resolve(data) // did it pass? resolve the promise
})
})
}
```
The `await` part of an `async` function will now wait on synchronous execution until the promise has resolved.
### Types Tricks
#### Importing
Flow comes with a unique way to import types.
```js
import type { Env } from "./ci_source"
```
This exposes the Env type to the current file, when compiled it be erased.
#### Typealias
If you have a special case example of a literal, you should consider using a typealias. Ash Furrow wrote a great example of why we came to use them a [lot in Swift](http://artsy.github.io/blog/2016/06/24/typealias-for-great-good/). Some examples from the current Danger codebase:
* An `Env` type, that maps to `any`.
* An `APIToken` type, that maps to `strings`.
#### Tests + Types
Realistically, it's a bit too early for me to be writing about that. Here as a stub for later.
98 changes: 74 additions & 24 deletions source/platforms/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import "babel-polyfill"
// so, I'm willing to give it a shot here.

export type APIToken = string;
export type GraphQLQuery = string;
export type GraphQLResponse = any;

/** This represent the GitHub API, and conforming to the Platform Interface */

Expand Down Expand Up @@ -61,6 +59,13 @@ export class GitHub {
}
}

async updateOrCreateComment(newComment: string): Promise<bool> {
const commentID = await this.getDangerCommentID()
if (commentID) { await this.updateCommentWithID(commentID, newComment) }
else { await this.createComment(newComment) }
return true
}

/**
* Returns the response for the new comment
*
Expand All @@ -71,16 +76,57 @@ export class GitHub {
return this.postPRComment(comment)
}

// In Danger RB we support a danger_id property,
// this should be handled at some point

/**
* Gets the list of comments on a PR
* Deletes the main Danger comment, used when you have
* fixed all your failures.
*
* @returns {Promise<bool>} did it work?
*/
async deleteMainComment(): Promise<bool> {
const commentID = await this.getDangerCommentID()
if (commentID) { await this.deleteCommentWithID(commentID) }
return commentID !== null
}

/**
* Updates the main Danger comment, when Danger has run
* more than once
*
* @param {string} comment updated text
*
* @returns {Promise<Comment[]>} comments
* @returns {Promise<bool>} did it work?
*/
// async getComments(): Promise<Comment[]> {
// // this.getPRComments(comment)
// }
async editMainComment(comment: string): Promise<bool> {
const commentID = await this.getDangerCommentID()
if (commentID) { await this.updateCommentWithID(commentID, comment) }
return commentID !== null
}

// The above is the API for Platform

async getDangerCommentID(): Promise<?number> {
const userID = await this.getUserID()
const allCommentsResponse = await this.getPullRequestComments()
const allComments: any[] = await allCommentsResponse.json()
const dangerComment = allComments.find((comment: any) => comment.user.id === userID)
return dangerComment ? dangerComment.id : null
}

async updateCommentWithID(id: number, comment: string): Promise<Response> {
const repo = this.ciSource.repoSlug
return this.patch(`repos/${repo}/issues/comments/${id}`, {}, {
body: comment
})
}

async deleteCommentWithID(id: number): Promise<Response> {
const repo = this.ciSource.repoSlug
return this.get(`repos/${repo}/issues/comments/${id}`, {}, {}, "DELETE")
}

async getUserID(): Promise<number> {
const info = await this.getUserInfo()
return info.id
Expand Down Expand Up @@ -109,13 +155,7 @@ export class GitHub {
getPullRequestComments(): Promise<Response> {
const repo = this.ciSource.repoSlug
const prID = this.ciSource.pullRequestID
return this.get(`repos/${repo}/pulls/${prID}/comments`)
}

getPRComments(): Promise<Response> {
const repo = this.ciSource.repoSlug
const prID = this.ciSource.pullRequestID
return this.get(`repos/${repo}/pulls/${prID}`)
return this.get(`repos/${repo}/issues/${prID}/comments`)
}

getPullRequestDiff(): Promise<Response> {
Expand All @@ -126,27 +166,37 @@ export class GitHub {
})
}

// maybe this can move into the stuff below
post(path: string, headers: any = {}, body: any = {}, method: string = "POST"): Promise<Response> {
console.log("posting: ")
console.log(JSON.stringify(body))

var myHeaders = new fetch.Headers()
myHeaders.append('Content-Type', 'application/json')
myHeaders.append('Authorization', `token ${this.token}`)
myHeaders.append('Accept', 'application/json')

return fetch(`https://api.github.com/${path}`, {
method: method,
body: JSON.stringify(body),
headers: myHeaders
headers: {
"Authorization": `token ${this.token}`,
"Content-Type": "application/json",
...headers }
})
}

get(path: string, headers: any = {}, body: any = {}, method: string = "GET"): Promise<Response> {
return fetch(`https://api.github.com/${path}`, {
method: method,
body: body,
headers: { "Authorization": `token ${this.token}`, ...headers }
headers: {
"Authorization": `token ${this.token}`,
"Content-Type": "application/json",
...headers }
})
}

patch(path: string, headers: any = {}, body: any = {}, method: string = "PATCH"): Promise<Response> {
return fetch(`https://api.github.com/${path}`, {
method: method,
body: JSON.stringify(body),
headers: {
"Authorization": `token ${this.token}`,
"Content-Type": "application/json",
...headers }
})
}
}
6 changes: 6 additions & 0 deletions source/platforms/platform.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ export interface Platform {
getReviewDiff: () => Promise<GitDSL>;
/** Creates a comment on the PR */
createComment: (body: string) => Promise<?Comment>;
/** Delete the main Danger comment */
deleteMainComment: () => Promise<bool>;
/** Replace the main Danger comment */
editMainComment: (newComment: string) => Promise<any>;
/** Replace the main Danger comment */
updateOrCreateComment: (newComment: string) => Promise<any>;
}

// /** Download all the comments in a PR */
Expand Down
7 changes: 5 additions & 2 deletions source/runner/Executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import DangerDSL from "../dsl/DangerDSL"
import { Platform } from "../platforms/platform"
import type { Violation } from "../platforms/messaging/violation"

// This is still badly named, maybe it really sbhould just be runner?
// This is still badly named, maybe it really should just be runner?

export default class Executor {
ciSource: CISource
Expand All @@ -24,8 +24,11 @@ export default class Executor {
if (results.fails.length) {
process.exitCode = 1
const fails = results.fails.map((fail: Violation) => fail.message)
this.platform.createComment(fails.join("<br/>"))
const comment = fails.join("<br/>")
this.platform.updateOrCreateComment(comment)
console.log("Failed.")
} else {
this.platform.deleteMainComment()
console.log("All Good.")
}
}
Expand Down

0 comments on commit a03a3e3

Please sign in to comment.