mirror of
https://github.com/tauri-apps/tauri-github-bot.git
synced 2026-01-31 00:35:20 +01:00
init
This commit is contained in:
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
17
.eslintrc
Normal 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
22
.github/workflows/run.yml
vendored
Normal 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
83
.gitignore
vendored
Normal 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
6
.husky/pre-commit
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
pnpm ts-check
|
||||
pnpm lint
|
||||
pnpm format
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"semi": false
|
||||
}
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
||||
41
package.json
Normal file
41
package.json
Normal 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
1449
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
src/auth.ts
Normal file
15
src/auth.ts
Normal 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
7
src/constants.ts
Normal 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
13
src/index.ts
Normal 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
109
src/task.ts
Normal 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
4
src/templates.ts
Normal 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
61
src/utils.ts
Normal 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
13
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user