Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
annndruha committed Jul 6, 2023
1 parent 567c725 commit 0a77a50
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 45 deletions.
77 changes: 77 additions & 0 deletions .github/workflows/build_and_publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Build, publish and deploy docker

on:
push:
branches: [ 'main' ]
tags:
- 'v*'

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build-and-push-image:
name: Build and push
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@master

- name: Log in to the Container registry
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=tag,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=raw,value=test,enable=true
- name: Build and push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

deploy-production:
name: Deploy Production
needs: build-and-push-image
if: startsWith(github.ref, 'refs/tags/v')
runs-on: [ self-hosted, Linux ]
environment:
name: Production
env:
CONTAINER_NAME: ${{ vars.DOCKER_CONTAINER_NAME }}
permissions:
packages: read
steps:
- name: Pull new version
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Run new version
run: |
docker stop ${{ env.CONTAINER_NAME }} || true && docker rm ${{ env.CONTAINER_NAME }} || true
docker run \
--detach \
--restart always \
--env BOT_TOKEN='${{ secrets.BOT_TOKEN }}' \
--env BOT_NICKNAME='${{ secrets.BOT_NICKNAME }}' \
--env GH_ACCOUNT_TOKEN='${{ secrets.GH_ACCOUNT_TOKEN }}' \
--env GH_ORGANIZATION_NICKNAME='${{ vars.GH_ORGANIZATION_NICKNAME }}' \
--env GH_SCRUM_STATE='${{ vars.GH_SCRUM_STATE }}' \
--env GH_SCRUM_ID='${{ vars.GH_SCRUM_ID }}' \
--env GH_SCRUM_FIELD_ID='${{ vars.GH_SCRUM_FIELD_ID }}' \
--env GH_SCRUM_FIELD_DEFAULT_STATE='${{ vars.GH_SCRUM_FIELD_DEFAULT_STATE }}' \
--name ${{ env.CONTAINER_NAME }} \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
60 changes: 58 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## Bot for creation GitHub issue from Telegram chat
Only for GitHub organizations (not for personal accounts)
Only for GitHub organizations repos issues (not for personal repos issues)

#### Setup

Expand All @@ -11,7 +11,11 @@ Only for GitHub organizations (not for personal accounts)
* `GH_ACCOUNT_TOKEN` - From step 2
4. and environment variables
* `GH_ORGANIZATION_NICKNAME` - Organization nickname for manage issue
* `DOCKER_CONTAINER_NAME` - Name docker for container
* `DOCKER_CONTAINER_NAME` - Name of docker container
* `GH_SCRUM_STATE` - Set to 0 if you doesn't want to automatically add new issue to scrum board, else read Scrum setup
* `GH_SCRUM_ID` - Value doesn't matter if GH_SCRUM_STATE=0
* `GH_SCRUM_FIELD_ID` - Value doesn't matter if GH_SCRUM_STATE=0
* `GH_SCRUM_FIELD_DEFAULT_STATE` - Value doesn't matter if GH_SCRUM_STATE=0

5. Run Docker Example:
```commandline
Expand All @@ -22,3 +26,55 @@ Only for GitHub organizations (not for personal accounts)
#### Example

![image](https://user-images.githubusercontent.com/51162917/225610117-0a5689ec-1742-4c11-8938-de8d098b5092.png)

#### Scrum setup
If you want to automatically add new issue to scrum board set `GH_SCRUM_STATE=1`
and set:
* `GH_SCRUM_ID` - this is identifier of scrum board
* `GH_SCRUM_FIELD_ID` - this is identifier of field in scrum board (e.g. columns)
* `GH_SCRUM_FIELD_DEFAULT_STATE` - this is default field(column) state (e.g. backlog)

You can find this id's with only with GraphQL requests ([use Explorer](https://docs.github.com/ru/graphql/overview/explorer)):

```graphql
# This query return scrum project's id's (GH_SCRUM_ID). Bot only can add issue only to one project
{organization(login: "GH_ORGANIZATION_NICKNAME") {
projectsV2(first: 100) {
edges {
node {
title
id
public
}}}}}

# Use GH_SCRUM_ID for next request.
# Usually, if you want to set column automatically, you need results from ProjectV2SingleSelectField
# GH_SCRUM_FIELD_ID is id
# GH_SCRUM_FIELD_DEFAULT_STATE is options id

{node(id: "GH_SCRUM_ID") {
... on ProjectV2 {
fields(first: 20) {
nodes {
... on ProjectV2Field {
id
name
}
... on ProjectV2IterationField {
id
name
configuration {
iterations {
startDate
id
}
}
}
... on ProjectV2SingleSelectField {
id
name
options {
id
name
}}}}}}}
```
43 changes: 20 additions & 23 deletions src/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,22 @@
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.requests import log as requests_logger
requests_logger.setLevel(logging.WARNING)
logging.getLogger("httpx").propagate = False


class Github:
def __init__(self, organization_nickname, token):
self.organization_nickname = organization_nickname
self.issue_url = 'https://api.github.com/repos/' + organization_nickname + '/{}/issues'
self.org_repos_url = f'https://api.github.com/orgs/{organization_nickname}/repos'
self.org_members_url = f'https://api.github.com/orgs/{organization_nickname}/members'
def __init__(self, settings):
self.to_scrum = True
self.settings = settings
self.organization_nickname = settings.GH_ORGANIZATION_NICKNAME
self.issue_url = 'https://api.github.com/repos/' + settings.GH_ORGANIZATION_NICKNAME + '/{}/issues'
self.org_members_url = f'https://api.github.com/orgs/{settings.GH_ORGANIZATION_NICKNAME}/members'

self.session = requests.Session()

self.headers = {
'Accept': 'application/vnd.github+json',
'Authorization': f'Bearer {token}',
'Authorization': f'Bearer {settings.GH_ACCOUNT_TOKEN}',
'X-GitHub-Api-Version': '2022-11-28',
'Content-Type': 'application/x-www-form-urlencoded'
}
Expand All @@ -43,27 +45,22 @@ def __read_queries(self):
self.q_get_repos = gql(f.read())
with open('src/graphql/add_to_scrum.graphql') as f:
self.q_add_to_scrum = gql(f.read())
with open('src/graphql/issue_actions.graphql') as f:
self.q_issue_actions = gql(f.read())

def open_issue(self, repo, title, comment):
payload = {'title': title, 'body': comment, 'projects': f'{self.organization_nickname}/7'}
r = self.session.post(self.issue_url.format(repo), headers=self.headers, json=payload)
if 'Issues are disabled for this repo' in r.text:
raise GithubIssueDisabledError
def open_issue(self, repo_id, title, body):
params = {'repositoryId': repo_id, 'title': title, 'body': body}
r = self.client.execute(self.q_issue_actions, operation_name='CreateIssue', variable_values=params)
return r

def old_get_repos(self, page=1):
data = {'sort': 'pushed', 'per_page': 9, 'page': page}
r = self.session.get(self.org_repos_url, headers=self.headers, params=data)
return r.json()

def get_repos(self, page_info):
params = {'gh_query': f'org:{self.organization_nickname} archived:false fork:true is:public sort:updated'}
if page_info == 'repos_start':
if page_info == 'repos_start': # start page
r = self.client.execute(self.q_get_repos, operation_name='getReposInit', variable_values=params)
elif page_info.startswith('repos_after'):
elif page_info.startswith('repos_after'): # next page
params['cursor'] = page_info.split('_')[2]
r = self.client.execute(self.q_get_repos, operation_name='getReposAfter', variable_values=params)
else: # repos_before
else: # previous page
params['cursor'] = page_info.split('_')[2]
r = self.client.execute(self.q_get_repos, operation_name='getReposBefore', variable_values=params)
return r['repos']
Expand Down Expand Up @@ -98,17 +95,17 @@ def set_assignee(self, issue_url, member_login, comment):

def add_to_scrum(self, node_id):
try:
params = {'projectId': 'PVT_kwDOBaPiZM4AFiz-',
params = {'projectId': self.settings.GH_SCRUM_ID,
'contentId': node_id}
r = self.client.execute(self.q_add_to_scrum, operation_name='addToScrum', variable_values=params)

item_id = r['addProjectV2ItemById']['item']['id']
logging.info(f'Node {node_id} successfully added to scrum with contentId= {item_id}')

params = {'projectId': 'PVT_kwDOBaPiZM4AFiz-',
params = {'projectId': self.settings.GH_SCRUM_ID,
'itemId': item_id,
'fieldId': 'PVTSSF_lADOBaPiZM4AFiz-zgDMeOc',
'value': '4a4a1bb5'} # BACKLOG_OPTION_ID
'fieldId': self.settings.GH_SCRUM_FIELD_ID,
'value': self.settings.GH_SCRUM_FIELD_DEFAULT_STATE} # backlog column
r = self.client.execute(self.q_add_to_scrum, operation_name='setScrumStatus', variable_values=params)
if 'errors' in r:
logging.warning(f'''itemId={item_id} not set status. Reason: {r['errors']}''')
Expand Down
3 changes: 3 additions & 0 deletions src/graphql/get_repos.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ query getReposInit($gh_query: String!) {
... on Repository {
name
url
id
}
}
}
Expand All @@ -32,6 +33,7 @@ query getReposAfter($gh_query: String!, $cursor: String!) {
... on Repository {
name
url
id
}
}
}
Expand All @@ -52,6 +54,7 @@ query getReposBefore($gh_query: String!, $cursor: String!) {
... on Repository {
name
url
id
}
}
}
Expand Down
34 changes: 34 additions & 0 deletions src/graphql/issue_actions.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
mutation CreateIssue ($repositoryId: ID!, $title: String!, $body: String!) {
createIssue(
input: {
repositoryId: $repositoryId,
title: $title,
body: $body
}
) {
issue {
number
body
url
id
}
}
}

mutation CreateIssueScrum ($repositoryId: ID!, $title: String!, $body: String!, $projectIds: [ID!]) {
createIssue(
input: {
repositoryId: $repositoryId,
title: $title,
body: $body,
projectIds: $projectIds
}
) {
issue {
number
body
url
id
}
}
}
35 changes: 15 additions & 20 deletions src/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
from telegram.ext import ContextTypes, CallbackContext
from telegram.constants import ParseMode

from gql.transport.exceptions import TransportError, TransportQueryError

from src.settings import Settings
from src.issue_message import TgIssueMessage
from src.github_api import Github, GithubIssueDisabledError
from src.answers import ans

settings = Settings()
github = Github(settings.GH_ORGANIZATION_NICKNAME, settings.GH_ACCOUNT_TOKEN)
github = Github(settings)


async def native_error_handler(update, context):
Expand Down Expand Up @@ -158,7 +160,7 @@ def __keyboard_repos(page_info):

buttons = []
for repo in repos_info['edges']:
buttons.append([InlineKeyboardButton(repo['node']['name'], callback_data='repo_' + repo['node']['name'])])
buttons.append([InlineKeyboardButton(repo['node']['name'], callback_data='repo_' + repo['node']['id'])])

buttons.append([])
if repos_info['pageInfo']['hasPreviousPage']:
Expand Down Expand Up @@ -193,35 +195,28 @@ def __keyboard_assign(page):


async def __create_issue(update: Update, context: ContextTypes.DEFAULT_TYPE):
repo_name = str(update.callback_query.data.split('_')[1])
repo_id = str(update.callback_query.data.split('_', 1)[1])
imessage = TgIssueMessage(update.callback_query.message.text_html)

link_to_msg = __get_link_to_telegram_message(update)
github_comment = imessage.comment + ans['issue_open'].format(update.callback_query.from_user.full_name, link_to_msg)

try:
r = github.open_issue(repo_name, imessage.issue_title, github_comment)
except GithubIssueDisabledError:
await context.bot.answer_callback_query(update.callback_query.id, 'У этого репозитория отключены Issue.')
logging.warning(f'{str_sender_info(update)} Try to open issue, but issue for {repo_name} disabled')
keyboard = InlineKeyboardMarkup([[InlineKeyboardButton('⚠️ Select repo to create',
callback_data='repos_start')]])
return keyboard, imessage.get_text()

if r.status_code == 201:
response = r.json()
imessage.set_issue_url(response['html_url'])
r = github.open_issue(repo_id, imessage.issue_title, github_comment)
imessage.set_issue_url(r['createIssue']['issue']['url'])

keyboard = InlineKeyboardMarkup([[InlineKeyboardButton('↩️', callback_data='quite'),
InlineKeyboardButton('👤', callback_data='assign_1'),
InlineKeyboardButton('❌', callback_data='close')]])
logging.info(f'{str_sender_info(update)} Succeeded open Issue: {response["html_url"]}')
threading.Thread(target=github.add_to_scrum, args=(response['node_id'], )).start()
logging.info(f'''{str_sender_info(update)} Succeeded open Issue: {r['createIssue']['issue']['url']}''')
if settings.GH_SCRUM_STATE:
threading.Thread(target=github.add_to_scrum, args=(r['createIssue']['issue']['id'], )).start()

else:
await context.bot.answer_callback_query(update.callback_query.id, f'Response code: {r.status_code}')
keyboard = InlineKeyboardMarkup([[InlineKeyboardButton('Настроить', callback_data='setup')]])
logging.error(f'{str_sender_info(update)} Failed to open Issue [{r.status_code}] {r.text}')
except TransportQueryError as err:
await context.bot.answer_callback_query(update.callback_query.id, f'''{err.errors[0]['message']}''')
keyboard = InlineKeyboardMarkup([[InlineKeyboardButton('⚠️ Select repo to create',
callback_data='repos_start')]])
logging.error(f'{str_sender_info(update)} Failed to open Issue: {err.args}')

return keyboard, imessage.get_text()

Expand Down
10 changes: 10 additions & 0 deletions src/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ class Settings(BaseSettings):
GH_ACCOUNT_TOKEN: str
GH_ORGANIZATION_NICKNAME: str

# This id's used for automatically add issue to scrum board
# and set default column such a backlog
# If you want to use scrum bard just set GH_SCRUM_STATE to True
# and pass project id, field if, and default field state id
GH_SCRUM_STATE: bool = False
GH_SCRUM_ID: str = 'PVT_kwDOBaPiZM4AFiz-'
GH_SCRUM_FIELD_ID: str = 'PVTSSF_lADOBaPiZM4AFiz-zgDMeOc'
GH_SCRUM_FIELD_DEFAULT_STATE: str = '4a4a1bb5'


class Config:
"""Pydantic BaseSettings config"""
case_sensitive = True
Expand Down

0 comments on commit 0a77a50

Please sign in to comment.