Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bootstrap ShapeIt Github Bot #2

Merged
merged 18 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/ISSUE_TEMPLATE/pitch.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Pitch
description: 'Do you have an idea and a product enhancement? Time to pitch!'
title: '[PITCH]:'
labels: ['pitch']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this pitch! If you need some inspiration, you can check out these pitch examples: [Example 1](https://github.com/asyncapi/studio/issues/636) and [Example 2](https://github.com/asyncapi/studio/issues/703).
- type: textarea
id: problem
attributes:
label: Problem
description: "Please provide a detailed description of the problem that users are facing. Share the story or context that exposes the problem."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
description: "Describe your proposed solution to address the problem. You can include examples, mockups, or views to illustrate your solution."
validations:
required: true
- type: textarea
id: rabbit_holes
attributes:
label: Rabbit holes
description: "Identify potential technical, design, or other challenges that might arise during the implementation of the solution. Discuss any anticipated obstacles or complexities."
validations:
required: true
- type: textarea
id: scope
attributes:
label: Scope
description: "Outline a list of tasks or issues that need to be completed to bring the proposed solution to life. You don't need to provide an exhaustive list upfront."
placeholder: |
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3
validations:
required: true
- type: textarea
id: out-of-bounds
attributes:
label: Out of bounds
description: "Specify any elements or features that explicitly fall outside the scope of this pitch. Clarify what won't be included in the solution."
validations:
required: false
- type: textarea
id: success
attributes:
label: Success criteria
description: "Define the criteria that will determine the success of the proposed solution. Specify the specific outcomes or metrics that will indicate the effectiveness of the solution."
validations:
required: false
15 changes: 15 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Fly Deploy
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy app
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --config ./fly-production.toml --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ npm-debug.log
*.pem
!mock-cert.pem
.env
.env-prod
coverage
lib
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ GITHUB_CLIENT_ID=XXX
GITHUB_CLIENT_SECRET=XXXX
```

`WEBHOOK_PROXY_URL` is only required in local dev.

To create a Github app visit : https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app


Expand Down
17 changes: 17 additions & 0 deletions fly-production.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# fly.toml app configuration file generated for shapeit-bot on 2023-11-06T17:45:48+01:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = "shapeit-bot"
primary_region = "cdg"

[build]

[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]
92 changes: 41 additions & 51 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,49 @@
/**
* This is the main entrypoint to your Probot app
* @param {import('probot').Probot} app
*/
const { handleNewPitch, handleNewScope, getProjectNodeItem } = require('./lib/onboarding')
const { parseIssueDescription, addScopeToBet } = require('./lib/scopes')
const { handleProgress } = require('./lib/progress')

const ITEM_ISSUE_TYPE = "Issue"
const KIND_FIELD = "Kind"
const PITCH_KIND_LABEL = "Pitch"
const BET_KIND_LABEL = "Bet"

module.exports = (app) => {
// Your code here
app.log.info("Yay, the app was loaded!")
const commentHeader = 'Thanks for creating a new pitch'
const commentText = 'Thanks for creating a new pitch 🥳. You can now create or link existing scopes. To know how to link scopes, check out this guide: https://example.com'

app.on("issues.labeled", async (context) => {
if (context.payload.label?.name === 'pitch') {
const query = `query($owner: String!, $repo: String!, $issueNumber: Int!) {
user(login: $owner) {
repository(name: $repo) {
issue(number: $issueNumber) {
id
comments(first: 100) {
nodes {
id
body
}
}
}
}
}
}`
const data = await context.octokit.graphql(query,
{
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
issueNumber: context.payload.issue.number
})
const comments = data?.user?.repository?.issue?.comments?.nodes
const existingBotComment = comments.filter(comment => comment.body.includes(commentHeader))
if (existingBotComment.length === 0) {
const issueComment = context.issue({
body: commentText,
})
return context.octokit.issues.createComment(issueComment)
}
app.on(["issues.edited", "issues.opened"], async (context) => {
const issueNumber = context.payload.issue.number
const owner = context.payload.repository.owner.login
const repo = context.payload.repository.name
const issueNodeId = context.payload.issue.node_id
const description = await parseIssueDescription(context)
if (description) {
await addScopeToBet(context, description)
await handleNewScope(context, owner, repo, issueNodeId, issueNumber)
}
});

app.on("issues.edited", async (context) => {
console.log(context.payload)
console.log("test")
});

app.on("projects_v2_item.edited", async (context) => {
console.log(context.payload)
})

// For more information on building apps:
// https://probot.github.io/docs/
app.on(["projects_v2_item.edited", "projects_v2_item.created"], async (context) => {
const projectNodeId = context.payload.projects_v2_item.project_node_id
const itemType = context.payload.projects_v2_item.content_type
const itemNodeId = context.payload.projects_v2_item.node_id
if (itemType === ITEM_ISSUE_TYPE) {
const item = await getProjectNodeItem(context, projectNodeId, itemNodeId)
const pitchType = item.fieldValues.nodes.find(item => item?.field?.name === KIND_FIELD && item?.name === PITCH_KIND_LABEL)
const betType = item.fieldValues.nodes.find(item => item?.field?.name === KIND_FIELD && item?.name === BET_KIND_LABEL)
const owner = item.content.repository.owner.login
const repository = item.content.repository.name
const issueNodeId = item.content.id
const issueNumber = item.content.number
if (pitchType || betType) {
await handleNewPitch(context, owner, repository, issueNodeId, issueNumber)
}
}
})

// To get your app running against GitHub, see:
// https://probot.github.io/docs/development/
app.on("issue_comment.created", async(context) => {
const issueNumber = context.payload.issue.number
const owner = context.payload.repository.owner.login
const repo = context.payload.repository.name
await handleProgress(context, owner, repo, issueNumber)
})
};
90 changes: 90 additions & 0 deletions lib/comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const { findCommentsQuery, createCommentMutation, updateCommentMutation, minimizeCommentMutation } = require('./queries')

const addComment = async(context, owner, repo, issueNodeId, issueNumber, commentHeader, commentText) => {
const previousComment = await findPreviousComment(context, owner, repo, issueNumber, commentHeader)
const commentWithHeader = `${commentText}\n${headerComment(commentHeader)}`
const variables = {
input : {
subjectId: issueNodeId,
body: commentWithHeader
}
}

if (previousComment === null) {
try {
await context.octokit.graphql(createCommentMutation, variables)
return true
} catch (error) {
console.log(`Failed mutation for issue #${issueNodeId}`, error)
return false
}
}
}

const findPreviousComment = async(context, owner, repo, issueNumber, search, includeBots = true) => {
let after = null
let hasNextPage = true
let comments = []
let existingBotComment = null

while (hasNextPage) {
let data = await context.octokit.graphql(findCommentsQuery,
{
owner: owner,
repo: repo,
issueNumber: issueNumber,
after: after
})
comments = data['organization'].repository?.issue?.comments?.nodes
hasNextPage = data['organization'].repository?.issue?.comments?.pageInfo?.hasNextPage ?? false
after = data['organization'].repository?.issue?.comments?.pageInfo?.endCursor
existingBotComment = comments.find(comment => comment.body.includes(search))
if (existingBotComment) {
if (!includeBots && existingBotComment?.author?.login.endsWith('[bot]')) {
continue
}
return existingBotComment
}
}
return null
}

const headerComment = (header) => {
return `<!-- ShapeIt Bot Comment ${header} -->`
}

const updateComment = async(context, commentNodeId, newBody) => {
const variables = {
input : {
id: commentNodeId,
body: newBody
}
}
try {
await context.octokit.graphql(updateCommentMutation, variables)
} catch (error) {
console.log(`Failed mutation for comment ${commentNodeId}`, error)
}
}

const minimizeComment = async(context, commentNodeId) => {
const variables = {
input : {
subjectId: commentNodeId,
classifier: 'DUPLICATE'
}
}
try {
await context.octokit.graphql(minimizeCommentMutation, variables)
} catch (error) {
console.log(`Failed mutation for comment ${commentNodeId}`, error)
}
}



exports.addComment = addComment
exports.updateComment = updateComment
exports.headerComment = headerComment
exports.findPreviousComment = findPreviousComment
exports.minimizeComment = minimizeComment
70 changes: 70 additions & 0 deletions lib/onboarding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const { getIssue, itemProjectQuery } = require('./queries')
const { addComment, headerComment } = require('./comments')
const { matchRelatedIssue } = require('./scopes')


const handleNewPitch = async(context, owner, repo, issueNodeId, issueNumber) => {
const commentHeader = 'pitch'
const commentText = `Thanks for creating a new pitch 🥳. You can now create or link existing scopes.
You can create new scopes in two different ways:

**Option 1**
1. Edit the Pitch or Bet issue
2. Add your scope under the scope section

See this [example](https://github.com/asyncapi/studio/issues/748)

**Option 2**
1. Create a new issue
2. Add this keywork in the description \`related to #ISSUE_BET_NUMBER\`
Amzani marked this conversation as resolved.
Show resolved Hide resolved

See this [example](https://github.com/asyncapi/studio/issues/755)
`
await addComment(context, owner, repo, issueNodeId, issueNumber, commentHeader, commentText)
}

const getProjectNodeItem = async(context, projectNodeId, itemNodeId) => {
const data = await context.octokit.graphql(itemProjectQuery,
{
projectNodeId: projectNodeId
})
const items = data.node.items.nodes
const nodeItem = items.find(item => item.id === itemNodeId)
return nodeItem
}

const handleNewScope = async(context, owner, repo, issueNodeId, issueNumber) => {
const commentHeader = 'scope'
const commentText = `Thanks for creating a new scope 🥳. You can now communicate your progress
To do so, you have to leave a comment in this issue or pull request with the following syntax:

\`\`\`/progress <percentage> [message]\`\`\`

or

\`\`\`
/progress <percentage>

A multiline message.
It supports Markdown.
\`\`\`

Example

\`\`\`
/progress 50
A few notes:
* We got this figured out :tada:
* We\'re going to use [this library](#link-to-website) to avoid losing time implementing this algorithm.
* We decided to go for the quickest solution and will improve it if we got time at the end of the cycle.
\`\`\`
`
if (matchRelatedIssue(context, context.payload.issue.body)) {
await addComment(context, owner, repo, issueNodeId, issueNumber, commentHeader, commentText)
}
}


exports.handleNewPitch = handleNewPitch
exports.getProjectNodeItem = getProjectNodeItem
exports.handleNewScope = handleNewScope
Loading