This commit is contained in:
amrbashir
2021-10-02 13:29:11 +02:00
commit 100983834c
17 changed files with 1879 additions and 0 deletions

9
.editorconfig Normal file
View File

@@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

17
.eslintrc Normal file
View File

@@ -0,0 +1,17 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"plugins": ["@typescript-eslint"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
}
}

22
.github/workflows/run.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Run
on:
schedule:
# every 15 mins
- cron: 0/15 * * * *
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '14'
registry-url: 'https://registry.npmjs.org'
- run: npm i -g pnpm
- run: pnpm i --prod
- run: pnpm start
env:
TAURI_BOT_TOKEN: ${{ secrets.TAURI_BOT_TOKEN }}

83
.gitignore vendored Normal file
View File

@@ -0,0 +1,83 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE
.idea
store.json

6
.husky/pre-commit Normal file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm ts-check
pnpm lint
pnpm format

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": false
}

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# Tauri Bot
A repo for tauri-bot scripts

41
package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "tauri-bot",
"version": "0.0.0",
"description": "",
"license": "MIT",
"files": [
"dist"
],
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"sideEffects": false,
"engines": {
"node": ">=14"
},
"scripts": {
"prepare": "husky install",
"dev": "esno src/index.ts dev",
"start": "esno src/index.ts",
"lint": "eslint src/**/*.ts",
"ts-check": "tsc --noEmit",
"format": "prettier --write --end-of-line=auto \"./**/*.{js,ts,json}\" --ignore-path .gitignore"
},
"dependencies": {
"@octokit/rest": "^18.5.3",
"chalk": "^4.1.2",
"esno": "^0.5.0",
"p-filter": "^3.0.0",
"winston": "^3.3.3"
},
"devDependencies": {
"@types/node": "^16.10.2",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"husky": "^7.0.2",
"prettier": "^2.3.2",
"typescript": "^4.4.2"
}
}

1449
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

15
src/auth.ts Normal file
View File

@@ -0,0 +1,15 @@
import { BOT_NAME, octokit } from './constants'
export async function checkBot(): Promise<void> {
try {
const { data: user } = await octokit.users.getAuthenticated()
if (!user || user.login !== BOT_NAME) {
console.error('Invalid GITHUB_TOKEN provided.')
process.exit(1)
}
} catch (e) {
console.error(e)
process.exit(1)
}
}

7
src/constants.ts Normal file
View File

@@ -0,0 +1,7 @@
import { Octokit } from '@octokit/rest'
export const BOT_NAME = 'tauri-bot'
export const ORG_NAME = 'tauri-apps'
export const octokit = new Octokit({
auth: process.env.TAURI_BOT_TOKEN,
})

13
src/index.ts Normal file
View File

@@ -0,0 +1,13 @@
import { checkBot } from './auth'
import { getNewTasks, runUpstreamTasks } from './task'
async function run() {
await checkBot()
const tasks = await getNewTasks()
await runUpstreamTasks(tasks)
}
run().catch((e) => {
console.error(e)
process.exit(1)
})

109
src/task.ts Normal file
View File

@@ -0,0 +1,109 @@
import { BOT_NAME, octokit, ORG_NAME } from './constants'
import {
getCommentIdFromUrl,
getIssueFromUrl,
isIssueOpen,
isTauriOrgMember,
logger,
} from './utils'
import pFilter from 'p-filter'
import chalk from 'chalk'
import { issueUpstreamedComment, upstreamIssueBody } from './templates'
interface Task {
issue: {
number: number
body: string
title: string
url: string
}
upstreamRepo: string
originalRepo: string
}
const REGEX_UPSTREAM = new RegExp(`@${BOT_NAME} upstream (tao|wry)`, 'i')
export async function getNewTasks(): Promise<Task[]> {
logger.info('checking notifications...')
const notifications = await pFilter(
(
await octokit.activity.listNotificationsForAuthenticatedUser({
all: false,
})
).data,
async (e) =>
e.reason === 'mention' &&
e.subject.type === 'Issue' &&
e.repository.owner.login === ORG_NAME &&
(await isIssueOpen(e.subject.url))
)
logger.info(`found ${chalk.blue(notifications.length)} valid notifications.`)
octokit.activity.markNotificationsAsRead()
const tasks: Task[] = []
for (const i of notifications) {
const issue = await getIssueFromUrl(i.subject.url)
const comment_id = getCommentIdFromUrl(i.subject.latest_comment_url)
if (!issue || !comment_id) continue
const {
data: { body = '', user },
} = await octokit.issues.getComment({
comment_id,
owner: issue.repository?.owner.login ?? ORG_NAME,
repo: issue.repository?.name ?? 'tauri',
})
if (!user || (await isTauriOrgMember(user.login))) continue
logger.info(
`comment received on ${chalk.green(
`${issue.repository?.owner.login}/${issue.repository?.name}#${issue.number}(@${user.login})`
)} ${chalk.blue(body)}`
)
const matches = body.match(REGEX_UPSTREAM)
if (!matches) continue
tasks.push({
originalRepo: issue.repository?.name ?? 'tauri',
upstreamRepo: matches[0].split(' ')[2],
issue: {
number: issue.number,
title: issue.title,
body: issue.body ?? '',
url: issue.html_url,
},
})
}
return tasks
}
export async function runUpstreamTasks(tasks: Task[]): Promise<void> {
if (tasks.length === 0) return
for (const t of tasks) {
const newIssue = (
await octokit.issues.create({
owner: ORG_NAME,
repo: t.upstreamRepo,
title: t.issue.title,
body: upstreamIssueBody(t.issue.url, t.issue.body),
})
).data
await octokit.issues.createComment({
owner: ORG_NAME,
repo: t.originalRepo,
body: issueUpstreamedComment(newIssue.html_url),
issue_number: t.issue.number,
})
await octokit.issues.addLabels({
owner: ORG_NAME,
repo: t.originalRepo,
issue_number: t.issue.number,
labels: ['status: upstream'],
})
}
}

4
src/templates.ts Normal file
View File

@@ -0,0 +1,4 @@
export const upstreamIssueBody = (ogIssueUrl: string, body: string): string =>
`This issue has been upstream from ${ogIssueUrl}\n ${body}`
export const issueUpstreamedComment = (url: string): string =>
`I have created an upstream issue at ${url}, I will notify you once it is fixed.`

61
src/utils.ts Normal file
View File

@@ -0,0 +1,61 @@
import { RestEndpointMethodTypes } from '@octokit/rest'
import { createLogger, transports, format } from 'winston'
import { octokit, ORG_NAME } from './constants'
export const logger = createLogger({
level: 'info',
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
format.splat(),
format.timestamp({ format: 'MM/DD HH:mm' }),
format.printf(
(info) =>
`${info.level} ${info.timestamp} | ${info.message}${
info.splat !== undefined ? `${info.splat}` : ' '
}`
)
),
}),
new transports.File({
format: format.json(),
filename: 'error.log',
level: 'error',
}),
],
})
export async function isIssueOpen(url: string): Promise<boolean> {
return (await octokit.request(url)).data.state === 'open'
}
export async function getIssueFromUrl(
url: string
): Promise<
RestEndpointMethodTypes['issues']['get']['response']['data'] | undefined
> {
const matches =
/api\.github\.com\/repos\/(.+?)\/(.+?)\/pulls\/([0-9]+)$/.exec(url)
if (!matches) return
const [, owner, repo, issue_number] = matches
if (!+issue_number) return
return (
await octokit.issues.get({ issue_number: +issue_number, owner, repo })
).data
}
export function getCommentIdFromUrl(url: string): number {
return +url.split('/').splice(-1)[0]
}
export async function isTauriOrgMember(user: string): Promise<boolean> {
const members = (await octokit.orgs.listMembers({ org: ORG_NAME })).data.map(
(i) => i.login
)
console.log(members)
return members.includes(user)
}

13
tsconfig.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"lib": ["esnext"],
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"strictNullChecks": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts"]
}