Unverified Commit 456621d6 authored by Adam Zerella's avatar Adam Zerella Committed by GitHub
Browse files

TS support and local dev hot-reloading (#7)

parent 683ee907
......@@ -19,6 +19,11 @@
"mocha/no-exclusive-tests": "error",
"mocha/no-pending-tests": "error"
}
},
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"]
}
],
"rules": {
......
......@@ -8,3 +8,4 @@ npm-debug.log
!mock-cert.pem
.env
.eslintcache
yarn-error.log
'use strict';
module.exports = {
require: '@babel/register',
spec: ['test/**/*.test.js'],
require: 'ts-node/register',
spec: ['test/**/*.test.ts'],
sort: true,
};
FROM node:12-alpine
FROM node:14-alpine
RUN apk -U upgrade --no-cache
WORKDIR /usr/src/app
COPY package.json ./
COPY package-lock.json ./
COPY yarn.lock ./
COPY babel.config.json ./
COPY index.js .
COPY tsconfig.json ./
COPY src/ ./src
RUN npm ci
RUN yarn install --frozen-lockfile
RUN npm run build
RUN yarn build
# Purge the devDeps required for building
RUN npm prune --production
RUN yarn install --production
ENV NODE_ENV="production"
CMD [ "node", "dist/index.js" ]
CMD [ "node", "dist/bot.js" ]
......@@ -23,8 +23,6 @@ Followed by a _comment_ on said pull request
/tip {small | medium | large}
```
## Local development 🔧
To use this bot, you'll need to have an `.env` file. Most of the options will
......@@ -37,23 +35,24 @@ A reference env file is placed at `.env.example` to copy over
$ cp .env.example .env
```
After registering and configuring the bot, we can run it like so
After registering and configuring the bot environment, we can run it. We use
[Nodemon](https://nodemon.io/) for hot-reloading, the `probot` package
automatically parses the relevant `.env` values.
```sh
$ npm start
$ yarn start
```
### Docker
To run the bot via Docker, we need to build and then run it like so
```sh
$ docker build -t substrate-tip-bot .
```
```sh
$ docker run \
$ docker run \
-e APP_ID=<app-id> \
-e PRIVATE_KEY=<pem-value> \
substrate-tip-bot
......
......@@ -7,7 +7,8 @@
"node": "current"
}
}
]
],
"@babel/preset-typescript"
],
"compact": true,
"comments": false
......
{
"verbose": false,
"watch": ["src/"],
"ignore": ["test/", "node_modules/", ".git"],
"ext": ".ts",
"exec": "node -r ts-node/register src/bot.ts"
}
......@@ -12,13 +12,13 @@
"probot-app"
],
"scripts": {
"start": "probot run ./index.js",
"start": "nodemon",
"test": "mocha",
"lint": "eslint index.js --cache",
"lint:fix": "eslint index.js --cache --fix",
"lint": "eslint src/**/* --cache",
"lint:fix": "eslint src/**/* --cache --fix",
"build": "run-s build:*",
"build:clean": "rimraf dist/",
"build:js": "babel index.js --extensions '.ts,.js' --out-dir dist/"
"build:js": "babel src/ --extensions '.ts,.js' --out-dir dist/"
},
"dependencies": {
"@polkadot/api": "^6.0.5",
......@@ -29,17 +29,25 @@
"@babel/cli": "^7.15.7",
"@babel/core": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/register": "^7.15.3",
"@babel/preset-typescript": "^7.15.0",
"@types/chai": "^4.2.22",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.3",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"chai": "^4.3.4",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"mocha": "^9.1.2",
"nock": "^13.0.5",
"nodemon": "^2.0.13",
"npm-run-all": "^4.1.5",
"prettier": "^2.4.1",
"rimraf": "^3.0.2",
"smee-client": "^1.2.2"
"smee-client": "^1.2.2",
"ts-node": "^10.3.0",
"typescript": "^4.4.3"
},
"engines": {
"node": ">= 10.13.0"
......
/**
* This is the main entrypoint to your Probot app
* @param {import('probot').Probot} app
*/
module.exports = (app) => {
// Your code here
app.log.info('Tip bot was loaded!');
app.on('issue_comment', async (context) => {
import { Probot, run } from 'probot';
import { ApiPromise, WsProvider, Keyring } from '@polkadot/api';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import { postComment } from './helpers/github';
// TODO add some kind of timeout then return an error
// TODO Unit tests
export async function tipUser(
address,
contributor,
network,
pullRequestNumber,
pullRequestRepo,
size
) {
await cryptoWaitReady();
const keyring = new Keyring({ type: 'sr25519' });
// Connect to the appropriate network.
let provider, account;
if (network == 'localtest') {
provider = new WsProvider('ws://localhost:9944');
account = keyring.addFromUri('//Alice', { name: 'Alice default' });
} else if (network == 'polkadot') {
provider = new WsProvider('wss://rpc.polkadot.io/');
account = keyring.addFromUri(process.env.ACCOUNT_SEED);
} else if (network == 'kusama') {
provider = new WsProvider('wss://kusama-rpc.polkadot.io/');
account = keyring.addFromUri(process.env.ACCOUNT_SEED);
} else {
return;
}
const api = await ApiPromise.create({ provider });
// Get general information about the node we are connected to
const [chain, nodeName, nodeVersion] = await Promise.all([
api.rpc.system.chain(),
api.rpc.system.name(),
api.rpc.system.version(),
]);
console.log(
`You are connected to chain ${chain} using ${nodeName} v${nodeVersion}`
);
const reason = `TO: ${contributor} FOR: ${pullRequestRepo}#${pullRequestNumber} (${size})`;
// TODO before submitting, check tip does not already exist via a storage query.
// TODO potentially prevent duplicates by also checking for reasons with the other sizes.
const unsub = await api.tx.tips
.reportAwesome(reason, address)
.signAndSend(account, (result) => {
console.log(`Current status is ${result.status}`);
if (result.status.isInBlock) {
console.log(`Tip included at blockHash ${result.status.asInBlock}`);
} else if (result.status.isFinalized) {
console.log(`Tip finalized at blockHash ${result.status.asFinalized}`);
unsub();
}
});
return true;
}
export default function bot(bot: Probot) {
bot.log.info('Tip bot was loaded!');
bot.on('issue_comment', async (context) => {
// Get all the relevant contextual information.
const commentText = context.payload.comment.body;
const pullRequestBody = context.payload.issue.body;
......@@ -127,72 +186,6 @@ module.exports = (app) => {
}
return;
});
};
// Simple helper function to post a comment on github.
function postComment(context, body) {
const issueComment = context.issue({
body: body,
});
return context.octokit.issues.createComment(issueComment);
}
var { ApiPromise, WsProvider, Keyring } = require('@polkadot/api');
var { cryptoWaitReady } = require('@polkadot/util-crypto');
// TODO add some kind of timeout then return an error
async function tipUser(
address,
contributor,
network,
pullRequestNumber,
pullRequestRepo,
size
) {
await cryptoWaitReady();
const keyring = new Keyring({ type: 'sr25519' });
// Connect to the appropriate network.
let provider, account;
if (network == 'localtest') {
provider = new WsProvider('ws://localhost:9944');
account = keyring.addFromUri('//Alice', { name: 'Alice default' });
} else if (network == 'polkadot') {
provider = new WsProvider('wss://rpc.polkadot.io/');
account = keyring.addFromUri(process.env.ACCOUNT_SEED);
} else if (network == 'kusama') {
provider = new WsProvider('wss://kusama-rpc.polkadot.io/');
account = keyring.addFromUri(process.env.ACCOUNT_SEED);
} else {
return;
}
const api = await ApiPromise.create({ provider });
// Get general information about the node we are connected to
const [chain, nodeName, nodeVersion] = await Promise.all([
api.rpc.system.chain(),
api.rpc.system.name(),
api.rpc.system.version(),
]);
console.log(
`You are connected to chain ${chain} using ${nodeName} v${nodeVersion}`
);
const reason = `TO: ${contributor} FOR: ${pullRequestRepo}#${pullRequestNumber} (${size})`;
// TODO before submitting, check tip does not already exist via a storage query.
// TODO potentially prevent duplicates by also checking for reasons with the other sizes.
const unsub = await api.tx.tips
.reportAwesome(reason, address)
.signAndSend(account, (result) => {
console.log(`Current status is ${result.status}`);
if (result.status.isInBlock) {
console.log(`Tip included at blockHash ${result.status.asInBlock}`);
} else if (result.status.isFinalized) {
console.log(`Tip finalized at blockHash ${result.status.asFinalized}`);
unsub();
}
});
return true;
}
run(bot);
export function postComment(context, body) {
const issueComment = context.issue({
body: body,
});
return context.octokit.issues.createComment(issueComment);
}
......@@ -5,8 +5,8 @@ import nock from 'nock';
import { Probot, ProbotOctokit } from 'probot';
import { assert } from 'chai';
import myProbotApp from '..';
import payload from './fixtures/issues.opened';
import myProbotApp from '../src/bot';
import payload from './fixtures/issues.opened.json';
describe('Substrate tip bot', () => {
let probot;
......@@ -51,7 +51,7 @@ describe('Substrate tip bot', () => {
// Test that a comment is posted
.post('/repos/hiimbex/testing-things/issues/1/comments', (body) => {
expect(body).toMatchObject(issueCreatedBody);
assert.deepEqual(body, issueCreatedBody);
return true;
})
.reply(200);
......
{
"compilerOptions": {
"moduleResolution": "node",
"target": "esnext",
"module": "commonjs",
"esModuleInterop": true,
"sourceMap": true,
"alwaysStrict": true,
"declaration": false,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"types": ["mocha", "chai", "node"]
},
"exclude": ["node_modules"]
}
This diff is collapsed.
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment