Unverified Commit 051268c4 authored by Mak's avatar Mak Committed by GitHub
Browse files

Merge branch 'master' into will-tip-gif

parents ec60d054 5dc2cc2d
Pipeline #217306 failed with stages
in 35 seconds
dist
node_modules
const {
getConfiguration,
} = require("opstooling-js-style/src/eslint/configuration")
module.exports = getConfiguration({ typescript: { rootDir: __dirname } })
{
"extends": ["eslint:recommended", "prettier"],
"parserOptions": {
"ecmaVersion": "latest"
},
"env": {
"node": true,
"commonjs": true,
"es6": true
},
"overrides": [
{
"files": ["**/*.test.js"],
"env": {
"mocha": true
},
"plugins": ["mocha"],
"rules": {
"mocha/no-exclusive-tests": "error",
"mocha/no-pending-tests": "error"
}
},
{
"files": ["**/*.ts"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"]
}
],
"rules": {
"prefer-const": "warn"
}
}
......@@ -3,6 +3,9 @@ coverage/
lib/
dist/
#IDE
.idea
npm-debug.log
*.pem
!mock-cert.pem
......
stages:
- test
- build
- staging
- production
default:
interruptible: true
retry:
max: 2
when:
- runner_system_failure
- unknown_failure
- api_failure
variables:
KUBE_NAMESPACE: "substrate-tip-bot"
CI_REGISTRY: "docker.io/paritytech"
GIT_STRATEGY: fetch
CI_IMAGE: node:16.10
DOCKERHUB_REPO: "paritytech"
IMAGE_NAME: docker.io/$DOCKERHUB_REPO/substrate-tip-bot
DOCKER_TAG: "${CI_COMMIT_SHORT_SHA}"
VAULT_ADDR: "https://vault.parity-mgmt-vault.parity.io"
VAULT_AUTH_PATH: "gitlab-parity-io-jwt"
VAULT_AUTH_ROLE: "cicd_gitlab_parity_${CI_PROJECT_NAME}"
HELM_SECRETS_DRIVER: vals
.common-refs: &common-refs
rules:
- if: $CI_PIPELINE_SOURCE == "web"
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_COMMIT_REF_NAME == "master"
- if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs
.test-refs: &test-refs
rules:
- if: $CI_COMMIT_REF_NAME =~ /^[0-9]+$/ # PRs
# Deploy on production goes only manually
.deploy-prod-refs: &deploy-prod-refs
rules:
- if: $CI_COMMIT_REF_NAME == "master" # on commits to main branch
when: manual
# Publish docker image and deploy it on staging
.publish-deploy-stg-refs: &publish-deploy-stg-refs
rules:
- if: $CI_COMMIT_REF_NAME == "master" # on commits to main branch
.kubernetes-env: &kubernetes-env
image: $CI_IMAGE
tags:
- kubernetes-parity-build
# template task for building and pushing an image
.build-push-docker-image: &build-push-docker-image
image: quay.io/buildah/stable
script:
- test "$Docker_Hub_User_Parity" -a "$Docker_Hub_Pass_Parity" ||
( echo "no docker credentials provided"; exit 1 )
- buildah bud
--format=docker
--build-arg VCS_REF="${CI_COMMIT_SHA}"
--build-arg BUILD_DATE="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
--build-arg VERSION="${DOCKER_TAG}"
--build-arg PROJECT_NAME="${CI_PROJECT_NAME}"
--tag "$IMAGE_NAME:${DOCKER_TAG}"
--tag "$IMAGE_NAME:latest"
--file "$DOCKERFILE" .
- echo "$Docker_Hub_Pass_Parity" |
buildah login --username "$Docker_Hub_User_Parity" --password-stdin docker.io
- buildah info
- buildah push --format=v2s2 "$IMAGE_NAME:${DOCKER_TAG}"
- buildah push --format=v2s2 "$IMAGE_NAME:latest"
after_script:
- buildah logout --all
# test that docker image can build
.build-only-docker-image: &build-only-docker-image
image: quay.io/buildah/stable
script:
- buildah bud
--format=docker
--build-arg VCS_REF="${CI_COMMIT_SHA}"
--build-arg BUILD_DATE="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
--build-arg PROJECT_NAME="${CI_PROJECT_NAME}"
--tag "$IMAGE_NAME:latest"
--file "$DOCKERFILE" .
check-linting:
stage: test
<<: *common-refs
<<: *kubernetes-env
script:
- yarn --immutable
- yarn lint
build-docker-bot:
stage: build
<<: *test-refs
<<: *kubernetes-env
<<: *build-only-docker-image
variables:
DOCKERFILE: "Dockerfile"
publish-docker-bot:
stage: build
<<: *publish-deploy-stg-refs
<<: *kubernetes-env
<<: *build-push-docker-image
variables:
DOCKERFILE: "Dockerfile"
#### stage: deploy
.deploy: &deploy-k8s
script:
# https://docs.gitlab.com/ee/ci/examples/authenticating-with-hashicorp-vault/#example
- export VAULT_TOKEN="$(vault write -field=token auth/$VAULT_AUTH_PATH/login role=$VAULT_AUTH_ROLE jwt=$CI_JOB_JWT)"
- helm dependency update helm/
- helm secrets --version
- helm secrets upgrade
--install
--atomic
--timeout 300s
--namespace ${CI_PROJECT_NAME}
--values helm/values.yaml
--values helm/values-$ENVIRONMENT.yaml
--set common.image.tag="$DOCKER_TAG"
$CI_PROJECT_NAME helm/
- kubectl get pods -n ${CI_PROJECT_NAME}
deploy-stg:
stage: staging
<<: *deploy-k8s
<<: *kubernetes-env
<<: *publish-deploy-stg-refs
variables:
CI_IMAGE: "paritytech/kubetools:3.5.3"
ENVIRONMENT: parity-stg
environment:
name: parity-stg
deploy-prod:
stage: production
<<: *deploy-k8s
<<: *kubernetes-env
<<: *deploy-prod-refs
variables:
CI_IMAGE: "paritytech/kubetools:3.5.3"
ENVIRONMENT: parity-prod
environment:
name: parity-prod
'use strict';
module.exports = {
require: 'ts-node/register',
spec: ['test/**/*.test.ts'],
sort: true,
};
dist
node_modules
{
"trailingComma": "es5",
"tabWidth": 2,
"singleQuote": true,
"semi": true,
"bracketSpacing": true
}
module.exports = require("opstooling-js-style/src/prettier/configuration")
# Lists some code owners.
#
# A codeowner just oversees some part of the codebase. If an owned file is changed then the
# corresponding codeowner receives a review request. An approval of the codeowner might be
# required for merging a PR (depends on repository settings).
#
# For details about syntax, see:
# https://help.github.com/en/articles/about-code-owners
# But here are some important notes:
#
# - Glob syntax is git-like, e.g. `/core` means the core directory in the root, unlike `core`
# which can be everywhere.
# - Multiple owners are supported.
# - Either handle (e.g, @github_user or @github_org/team) or email can be used. Keep in mind,
# that handles might work better because they are more recognizable on GitHub,
# you can use them for mentioning unlike an email.
# - The latest matching rule, if multiple, takes precedence.
# Global code owners
* @paritytech/opstooling
# CI
/.gitlab-ci.yml @paritytech/ci
/.github @paritytech/ci
FROM node:14-alpine
FROM node:16-alpine
RUN apk -U upgrade --no-cache
# metadata
ARG VCS_REF=master
ARG BUILD_DATE=""
ARG REGISTRY_PATH=docker.io/paritytech
ARG PROJECT_NAME=""
LABEL io.parity.image.authors="cicd-team@parity.io" \
io.parity.image.vendor="Parity Technologies" \
io.parity.image.title="${REGISTRY_PATH}/${PROJECT_NAME}" \
io.parity.image.description="Substrate Tip bot" \
io.parity.image.source="https://github.com/paritytech/${PROJECT_NAME}/blob/${VCS_REF}/Dockerfile" \
io.parity.image.documentation="https://github.com/paritytech/${PROJECT_NAME}/blob/${VCS_REF}/README.md" \
io.parity.image.revision="${VCS_REF}" \
io.parity.image.created="${BUILD_DATE}"
RUN apk -U upgrade --no-cache && apk add --no-cache git
WORKDIR /usr/src/app
......@@ -10,7 +25,7 @@ COPY babel.config.json ./
COPY tsconfig.json ./
COPY src/ ./src
RUN yarn install --frozen-lockfile
RUN yarn install --immutable
RUN yarn build
......
......@@ -23,6 +23,8 @@ 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
......@@ -35,6 +37,42 @@ A reference env file is placed at `.env.example` to copy over
$ cp .env.example .env
```
### Run polkadot or substrate `localtest` network locally
- Follow readme in https://github.com/paritytech/polkadot#development to run local network.
- Among all dependencies, main steps are (from repo):
- Compile `cargo b -r`
- Run `./target/release/polkadot --dev`
- [Create 2 accounts: for "bot" & for "contributor"](https://polkadot.js.org/apps/?rpc=ws%3A%2F%2F127.0.0.1%3A9944#/accounts)
- Save the seeds & passwords somewhere
- Set `ACCOUNT_SEED` as bot's seed in `.env` file
- Transfer some meaningful amount from test accounts (like Alice) to a new bot account (from which bot will be send tip to the contributor)
### Create GitHub application for testing
- Note: During app creation save according env variables to `.env` file
- Read [Getting-started](https://gitlab.parity.io/groups/parity/opstooling/-/wikis/Bots/Development/Getting-started) doc to get a sense of how to work with bots
- Follow [creating app](https://gitlab.parity.io/groups/parity/opstooling/-/wikis/Bots/Development/Create-a-new-GitHub-App)
and [installing app](https://gitlab.parity.io/groups/parity/opstooling/-/wikis/Bots/Development/Installing-the-GitHub-App)
guidance
- `WEBHOOK_PROXY_URL` you can generate via https://smee.io/new
#### Github app permissions
##### Repository permissions:
- **Issues**: Read-only
- Allows for interacting with the comments API
- **Pull Requests**: Read & write
- Allows for posting comments on pull requests
##### Organization permissions
- **Members**: Read-only
- Related to $ALLOWED_ORGANIZATIONS: this permission enables the bot to request the organization membership of the command's requester even if their membership is private
##### Event subscriptions
- **Issue comment**
- Allows for receiving events for pull request comments
### Start a bot
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.
......@@ -43,6 +81,12 @@ automatically parses the relevant `.env` values.
$ yarn start
```
### Create a PR and test it
You'll need 2 gh users: contributor and maintainer (since it's not allowed for contributors to send a tip to themselves)
- From contributor GH account: create a PR and add into PR description `localtest address: <contributor polkadot address>`
- From maintainer GH account: write `/tip small` in comments so the bot sends funds to <contributor polkadot address>
### Docker
To run the bot via Docker, we need to build and then run it like so
......
# Helm dependency files
Chart.lock
charts/
apiVersion: v2
name: matrix-admin-bot
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.0.0"
dependencies:
- name: common
version: "0.2.2"
repository: "https://paritytech.github.io/helm-charts/"
common:
env:
APP_HOST: "https://substrate-tip-bot.parity-prod.parity.io"
secrets:
WEBHOOK_SECRET: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-prod#WEBHOOK_SECRET
PRIVATE_KEY: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-prod#PRIVATE_KEY
GITHUB_CLIENT_SECRET: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-prod#GITHUB_CLIENT_SECRET
ACCOUNT_SEED: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-prod#ACCOUNT_SEED
ALLOWED_USERS: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-prod#ALLOWED_USERS
APP_ID: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-prod#APP_ID
ingress:
annotations:
external-dns.alpha.kubernetes.io/target: traefik-external.parity-prod.parity.io.
rules:
- host: substrate-tip-bot.parity-prod.parity.io
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: substrate-tip-bot
port:
name: http
tls:
- hosts:
- substrate-tip-bot.parity-prod.parity.io
secretName: substrate-tip-bot.parity-prod.parity.io
common:
env:
APP_HOST: "https://substrate-tip-bot.parity-stg.parity.io"
DATABASE_NAME: "substrate-tip-bot"
secrets:
WEBHOOK_SECRET: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-stg#WEBHOOK_SECRET
PRIVATE_KEY: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-stg#PRIVATE_KEY
GITHUB_CLIENT_SECRET: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-stg#GITHUB_CLIENT_SECRET
ACCOUNT_SEED: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-stg#ACCOUNT_SEED
ALLOWED_USERS: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-stg#ALLOWED_USERS
APP_ID: ref+vault://kv/cicd/gitlab/parity/mirrors/substrate-tip-bot/parity-stg#APP_ID
ingress:
annotations:
external-dns.alpha.kubernetes.io/target: traefik-external.parity-stg.parity.io.
rules:
- host: substrate-tip-bot.parity-stg.parity.io
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: substrate-tip-bot
port:
name: http
tls:
- hosts:
- substrate-tip-bot.parity-stg.parity.io
secretName: substrate-tip-bot.parity-stg.parity.io
common:
fullnameOverride: "substrate-tip-bot"
extraLabels:
team: "opstooling"
serviceAccount:
create: false
image:
repository: paritytech/substrate-tip-bot
envFrom:
- secretRef:
name: substrate-tip-bot
service:
ports:
- name: http
protocol: TCP
port: 80
targetPort: 3000
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-dns01
kubernetes.io/ingress.class: traefik-external
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
traefik.ingress.kubernetes.io/router.tls: "true"
# Enable after endpoint is created
# livenessProbe:
# httpGet:
# path: /health
# port: 8000
# scheme: HTTP
# readinessProbe:
# httpGet:
# path: /health
# port: 8000
# scheme: HTTP
......@@ -12,17 +12,18 @@
"probot-app"
],
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "yarn eslint --quiet '{*,**/*}.{js,ts}' && yarn prettier --check '{*,**/*}.json' && yarn typecheck",
"fix:eslint": "eslint --fix",
"fix:prettier": "prettier --write",
"fix": "yarn fix:eslint '{*,**/*}.{js,ts}' && yarn fix:prettier '{*,**/*}.json'",
"start": "nodemon",
"test": "mocha",
"lint": "eslint src/**/* --cache",
"lint:fix": "eslint src/**/* --cache --fix",
"build": "run-s build:*",
"build:clean": "rimraf dist/",
"build:js": "babel src/ --extensions '.ts,.js' --out-dir dist/"
"build": "rimraf dist; babel src/ --extensions '.ts,.js' --out-dir dist/"
},
"dependencies": {
"@polkadot/api": "^6.11.1",
"@polkadot/util-crypto": "^8.1.2",
"@polkadot/api": "^9.0.1",
"@polkadot/util-crypto": "^10.1.2",
"opstooling-js": "https://github.com/paritytech/opstooling-js#v0.0.3",
"probot": "^11.0.1"
},
"devDependencies": {
......@@ -30,20 +31,13 @@
"@babel/core": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/preset-typescript": "^7.15.0",
"@types/chai": "^4.2.22",
"@types/mocha": "^9.0.0",
"@resolritter/tsc-files": "^1.1.4",
"@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",
"dotenv": "^16.0.1",
"eslint-plugin-prettier": "^4.1.0",
"nodemon": "^2.0.13",
"npm-run-all": "^4.1.5",
"prettier": "^2.4.1",
"opstooling-js-style": "https://github.com/paritytech/opstooling-js-style#master",
"prettier": "^2.7.1",
"rimraf": "^3.0.2",
"smee-client": "^1.2.2",
"ts-node": "^10.3.0",
......
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;
import { displayError, envVar } from "opstooling-js"
import { Probot, run } from "probot"
import { isPullRequest } from "./github"
import { getTipSize, parseContributorAccount, tipUser } from "./tip"
import { IssueCommentCreatedContext, State } from "./types"
const onIssueComment = async (
state: State,
context: IssueCommentCreatedContext,
tipRequester: string,
) => {
const { allowedTipRequesters, bot } = state
const commentText = context.payload.comment.body
const pullRequestBody = context.payload.issue.body
const pullRequestUrl = context.payload.issue.html_url
const contributorLogin = context.payload.issue.user.login
const pullRequestNumber = context.payload.issue.number
const pullRequestRepo = context.payload.repository.name
const [botMention, tipSizeInput] = commentText.split(" ") as (
| string
| undefined
)[]
// The bot only triggers on creation of a new comment on a pull request.
if (
!isPullRequest(context) ||
context.payload.action !== "created" ||
!botMention?.startsWith("/tip")
) {
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();
}
});
if (tipRequester === contributorLogin) {
return "Contributor and tipper cannot be the same person!"
}