Automação de tarefas GitHub com ações sem servidor

10 min de leitura
Patrocinado
Imagem de: Automação de tarefas GitHub com ações sem servidor
Avatar do autor

Equipe TecMundo

Por: Steve Martinelli

Publicado em: 7 de outubro de 2019

As permissões do GitHub podem ficar complicadas. Existem nuances para entender o que um proprietário pode fazer e o que um membro pode fazer. Desse modo, à medida que sua organização cresce, mais restrições podem ser colocadas sobre o que os membros podem fazer. Aqui está um problema que encontramos no meu trabalho diário:

  • Temos uma presença pública no GitHub em github.com/xyz;

  • Os membros do xyz não têm permissão para criar repositórios – ou repos – por padrão;

  • Novas solicitações de repo foram enviadas por e-mail aos proprietários da organização e são feitas manualmente. (Buuu.)

Este tutorial mostra uma maneira bem fácil para requerer que novos repositórios sejam feitos para uma organização GitHub. Envolve pedir aos membros que solicitem novos repositórios, arquivando questões em um repositório específico (a solução do exemplo utiliza uma instância do GitHub Enterprise, mas qualquer repositório GitHub funcionaria da mesma forma). Isso permite que vários proprietários vejam a mesma fila e fornece um registro de quantas solicitações foram feitas. (Além disso, eu só queria mexer no serverless – no português, "sem servidor" –, então essa foi uma boa desculpa).

É assim que funciona em um alto nível:

Arquitetura de alto nível

  1. Um usuário registra de modo detalhado um problema no GitHub sobre um repo que deseja criar;

  2. Quando um problema é aprovado, uma payload (em português, carga útil) é enviada para uma ação serverless;

  3. A ação serverless aciona algum código Python;

  4. Utilizando o módulo de solicitações Python, você aciona APIs do GitHub.

Ao concluir este tutorial, você entenderá como:

  • Configurar uma ação com o IBM Cloud Functions;

  • Acionar uma ação do IBM Cloud Functions com um webhook (no português, "gancho web");

  • Interagir com a API do GitHub.

Pré-requisitos

Tempo estimado

A leitura deste tutorial deve levar cerca de 45 minutos.

Passos

Este tutorial é dividido em algumas partes:

  1. Gerar um token de acesso pessoal do GitHub;

  2. Criar um acionador e uma ação serverless;

  3. Configurar um repositório GitHub para webhooks;

  4. Executar;

  5. Dar uma olhada mais detalhada no código.

1. Gerar um token de acesso pessoal do GitHub

Gere um token de acesso pessoal para que você possa acionar APIs do GitHub programaticamente (a partir de seu código Python). Isso é amplamente documentado no GitHub, mas, resumindo, siga os seguintes passos para gerar um token de acesso pessoal:

  • Nas configurações de perfil do GitHub, clique em Configurações do Desenvolvedor (Developer Settings);

  • Selecione Tokens de acesso pessoais (Personal access tokens);

  • Clique em Gerar novo token (Generate new token);

  • Certifique-se de que a opção Repo esteja selecionada;

  • Clique em Gerar token (Generate token) na parte inferior.

Uma sequência aleatória de caracteres será gerada. Copie-a e cole-a em algum lugar seguro, pois você irá precisar dela nas próximas etapas. Repita esta etapa se estiver utilizando uma conta GitHub Enterprise em sua configuração.

2. Criar um acionador e uma ação serverless

Para começar a criar um acionador ou uma ação, você precisa efetuar o login no IBM Cloud e selecionar a opção Funções (Fuctions) no Menu de Navegação (Navigation Menu) ou acessá-la diretamente.

Crie um acionador personalizado

A primeira coisa que você precisa fazer no IBM Cloud é criar um acionador serverless. Ao criar um acionador, você terá uma URL para fornecer ao seu webhook do GitHub. O webhook do GitHub será acionado sempre que um evento (como um novo problema ou um novo pull request) acontecer e enviará uma payload (como uma interpretação JSON do evento) para a URL associada ao seu acionador serverless.

  • Na página de visão geral das Funções (Functions), selecione Criar Acionador (Create Trigger).

Criação de acionador

  • Escolha a opção Customizar Acionador (Custom Trigger).

Customização de acionador

  • Dê um nome ao novo acionador e insira uma descrição.

Configuração de novo acionador

  • Depois de criado, você precisa examinar o ponto de extremidade do acionador. Clique no ícone do olho para descobrir a URL completa com a chave e o segredo da API.

Endpoint do acionador

Novamente, salve esta URL em algum lugar, pois você precisará dela em breve!

Crie uma ação

Agora, você irá criar um código Python (uma Ação) que será executado quando seu acionador for, bem, acionado!

  • Na página de visão geral das Funções (Functions), escolha Criar Ação (Create Action).

Criação de ação

  • Dê um nome à nova ação, selecione o pacote padrão e escolha o Python 3 como o sistema de tempo de execução.

Selecionando Python 3

  • Assim que o editor for exibido, copie e cole o código abaixo no editor online. Nós analisaremos o código após as etapas do tutorial.

import json

import re

import requests

# The public org where new repos will be created.

PUBLIC_ORG = 'foo'

# The repo where folks will request new repos for the public org in the form of a comment

GHE_REQUEST_REPO = 'https://github.mycompany.com/api/v3/repos/myorg/myrepo'

def _get_info_from_body(body):

# "body": "## Essentials\r\n\r\n* name: foo\r\n* users: stevemar, stevemart\r\n* description: this is for fun\r\n* license: apache\r\n\r\n## Tips\r\n\r\n* Repo names CANNOT have spaces.\r\n* User IDs must be from public github, if you're not sure, go to https://github.com/ and login.\r\n"

m = re.search(r'\* name:(.*)(\r\n|$)', body)

repo_name = m.group(1).strip() if m else None

repo_name = repo_name.strip() if repo_name else None

m = re.search(r'\* users:(.*)(\r\n|$)', body)

users = m.group(1).strip() if m else []

users = [x.strip() for x in users.split(',')] if users else []

m = re.search(r'\* teams:(.*)(\r\n|$)', body)

teams = m.group(1).strip() if m else []

teams = [x.strip() for x in teams.split(',')] if teams else []

m = re.search(r'\* description:(.*)(\r\n|$)', body)

description = m.group(1).strip() if m else ''

m = re.search(r'\* license:(.*)(\r\n|$)', body)

license = m.group(1).strip() if m else 'apache-2.0'

license = license.strip() if license else 'apache-2.0'

return {'repo_name': repo_name, 'users': users,

'teams': teams, 'description': description, 'license': license}

def _post_a_comment(issue_number, message, ghe_token):

# post comment on issue -- https://developer.github.com/v3/issues/comments/#create-a-comment

headers = {'Authorization': 'token %s' % ghe_token}

url = GHE_REQUEST_REPO + '/issues/' + str(issue_number) + '/comments'

payload = {'body': message}

requests.post(url, headers=headers, data=json.dumps(payload))

def main(params):

if params['comment']['body'].strip() == '/approve':

sender = params['sender']['login']

ghe_token = params['GHE_TOKEN']

issue_number = params['issue']['number']

if sender == 'stevemar':

gh_token = params['GH_PUB_TOKEN']

else:

message = 'approve comment made by unauthorized user, please wait for an authorized user to approve'

_post_a_comment(issue_number, message, ghe_token)

return {'message': message}

# get info from the issue body

body = params['issue']['body']

info = _get_info_from_body(body)

# quit if we can't get a repo name

if info['repo_name'] is None:

message = 'could not find a repo name, please provide one'

_post_a_comment(issue_number, message, ghe_token)

return {'message': message}

# TODO: quit if we can't find at least one user or one team

# set up auth headers to call github APIs

headers = {'Authorization': 'token %s' % gh_token}

# create the repo -- https://developer.github.com/v3/repos/#create

url = 'https://api.github.com/orgs/' + PUBLIC_ORG + '/repos'

payload = {

'name': info['repo_name'],

'description': info['description'],

'license_template': info['license'],

'auto_init': 'true'

}

r = requests.post(url, headers=headers, data=json.dumps(payload))

if r.status_code != 201:

message = "could not create repo: " + r.text

_post_a_comment(issue_number, message, ghe_token)

return {'message': message}

else:

message = "created repo: " + info['repo_name'] + ". now adding users and teams"

_post_a_comment(issue_number, message, ghe_token)

# TODO: actually look up the user and handle errors

# add user to collaborate -- https://developer.github.com/v3/repos/collaborators/#add-user-as-a-collaborator

for user in info['users']:

url = 'https://api.github.com/repos/' + PUBLIC_ORG + '/' + info['repo_name'] + '/collaborators/' + user

payload = {

'permission': 'admin'

}

r = requests.put(url, headers=headers, data=json.dumps(payload))

# add teams if specified, first look up team name to get id, then add the repository

for team in info['teams']:

# https://developer.github.com/v3/teams/#get-team-by-name

url = 'https://api.github.com/orgs/' + PUBLIC_ORG + '/teams/' + team

r = requests.get(url, headers=headers)

team_id = str(r.json()['id'])

# https://developer.github.com/v3/teams/#add-or-update-team-repository

url = 'https://api.github.com/teams/' + team_id + '/repos/' + PUBLIC_ORG + '/' + info['repo_name']

payload = {

'permission': 'admin'

}

r = requests.put(url, headers=headers, data=json.dumps(payload))

# invite users to the org -- https://developer.github.com/v3/orgs/members/#add-or-update-organization-membership

for user in info['users']:

url = 'https://api.github.com/orgs/' + PUBLIC_ORG + '/memberships/' + user

payload = {

'role': 'member'

}

r = requests.put(url, headers=headers, data=json.dumps(payload))

# post comment on issue -- https://developer.github.com/v3/issues/comments/#create-a-comment

message = 'created repo https://github.com/%s/%s with admins (%s) and teams (%s)' % (PUBLIC_ORG, info['repo_name'], info['users'], info['teams'])

_post_a_comment(issue_number, message, ghe_token)

return {'message': message}

else:

        return {'message': 'not approved yet -- ignoring'}

  • Após a ação ter sido criada, você precisa associá-la ao acionador que foi criado na etapa anterior. Vá para o menu à esquerda e selecione a opção Acionadores Conectados (Connected Triggers).

Acionadores conectados

  • Clique em Adicionar Acionador (Add Trigger), selecione Customizar Acionador (Custom Trigger) e encontre aquele que acabou de ser criado.

  • Volte para a ação que foi criada e encontre o menu Parâmetros (Parameters). Você precisa adicionar duas variáveis de ambiente, GHE_TOKEN e GH_PUB_TOKEN, como parâmetros. Utilize os valores que foram gerados na Etapa 1.

  • Clique em Adicionar (Add).

Adicionando parâmetros

Está quase tudo pronto!

3. Configurar um repositório GitHub para webhooks

  • Crie um repositório GitHub no qual os usuários irão registrar problemas a fim de solicitar novos repositórios;

  • Dê aos usuários um modelo de problema a ser seguido. O que usei está abaixo. Ele solicita um nome de repo, descrição, licença e nomes de usuário para serem adicionados como administradores.

## Essentials

* name: repo_name

* users: user_1, user_2

* description: this is for fun, ain't it grand!

* license: apache-2.0

  • Nas configurações do repo, vá para o menu Ganchos (Hooks) para configurar um webhook que irá acionar seu acionador serverless.

Webhooks

  • Crie um novo webhook. Defina a URL de Payload (Payload URL) como o ponto de extremidade de acionamento da Etapa 2.

  • Defina o Tipo de Conteúdo (Content Type) como application/json.

  • Defina a verificação SSL como habilitada. A URL de payload deve se parecer com a seguinte:

https://KEY:SECRET@us-south.functions.cloud.ibm.com/api/v1/namespaces/your_namespace/triggers/your_trigger

Verificação SSL

  • Para a opção Quais eventos você gostaria de acionar este webhook? (Which events would you like to trigger this webhook?), escolha Permita-me selecionar eventos individuais (Let me select individual events).

  • Quando a lista for exibida, escolha Comentários do Problema (Issue Comments).

Comentários do problema

Agora que seu repo está configurado para conversar com a sua ação, sua ação está associada ao seu acionador. Seu acionador também possui tokens do GitHub para serem utilizados, então você finalmente poderá testar tudo!

4. Executar

  • Comece pedindo para que alguém registre um problema. Abaixo se encontra um exemplo. Você pode ver o nome e a descrição do repo, os nomes de usuário a serem adicionados e a licença de uso. Está tudo aí.

Problema original

  • Deixar qualquer comentário acionaria seu acionador, mas, em sua ação serverless, você verifica especificamente se há algum comentário /approve. Então, deixe essa mensagem no problema e observe a payload que é enviado do GitHub para o seu acionador.

Aprovação

  • Esta é a aparência da payload. Ela foi bastante cortada para facilitar a leitura, mas você pode ver o corpo do problema, o comentário do problema, a pessoa que fez o comentário e qual é o número do problema. Todo esse bloco é disponibilizado na variável params de sua ação serverless.

{

  "action": "created",

  "issue": {

    "number": 18,

    "title": "Repo for Studio Learning Path assets",

    "user": {

      "login": "Rich-Hagarty",

    },

    "body": "## Essentials\r\n\r\n* name: watson-studio-learning-path-assets\r\n* users: rhagarty\r\n* description: repo to store all assets (such as notebooks, data, etc) for Watson Studio Learning Path tutorials\r\n\r\n## Tips\r\n\r\n* Repo names **CANNOT** have spaces.\r\n* User IDs must be from **public github**, if you're not sure, go to https://github.com/ and login.\r\n*"

  },

  "comment": {

    "body": "/approve"

  },

  "sender": {

    "login": "stevemar"

  }

}

  • Finalmente, parte do script na ação serverless posta um comentário de acompanhamento (indicando que o repositório foi criado) e posta também uma URL.

Comentário de acompanhamento

5. Dar uma olhada mais detalhada no código

Todo o código-fonte utilizado está disponível no Gist. Vamos dar uma analisada em alguns trechos do código.

Verificando o remetente do comentário

Aqui está um procedimento simples de segurança, codificado para dois aprovadores, para garantir que quando um comentário /approve for deixado, seja realmente de alguém de confiança. (Não é o mais bonito, mas funciona.)

def main(params):

    if params['comment']['body'] == "/approve":

        sender = params['sender']['login']

        if sender == 'stevemar' or sender == 'chrisfer':

            print("proceeding with repo create")

        else:

            return { 'message': 'approve comment made by unauthorized user' }

Análise de remarcação

O corpo do problema veio no payload JSON, mas estava em redução bruta. Tive que ser um pouco esperto aqui para extrair o texto de todos os casos extremos e acabei criando vários testes de unidade a fim de garantir que os casos extremos fossem detectados.

def _get_info_from_body(body):

    m = re.search(r'\* name:(.*)(\r\n|$)', body)

    repo_name = m.group(1).strip() if m else None

    repo_name = repo_name.strip() if repo_name else None

    m = re.search(r'\* users:(.*)(\r\n|$)', body)

    users = m.group(1).strip() if m else []

    users = [x.strip() for x in users.split(',')] if users else []

    m = re.search(r'\* description:(.*)(\r\n|$)', body)

    description = m.group(1).strip() if m else ''

    m = re.search(r'\* license:(.*)(\r\n|$)', body)

    license = m.group(1).strip() if m else 'apache-2.0'

    license = license.strip() if license else 'apache-2.0'

    return {'repo_name': repo_name, 'users': users,

            'description': description, 'license': license}

Acionando APIs do GitHub

As APIs do GitHub são muito bem documentadas. Para acioná-las, utilizei a biblioteca de solicitações Python.

   gh_token = params['GH_PUB_TOKEN']

    # set up auth headers to call github APIs

    headers = {'Authorization': 'token %s' % gh_token}

    # create the repo -- https://developer.github.com/v3/repos/#create

    url = 'https://api.github.com/orgs/' + PUBLIC_ORG + '/repos'

    payload = {

        'name': info['repo_name'],

        'description': info['description'],

        'license_template': info['license'],

        'auto_init': 'true'

    }

    r = requests.post(url, headers=headers, data=json.dumps(payload))

Resumo

Espero que você tenha gostado de ler este tutorial tanto quanto eu gostei de escrevê-lo. Agora você sabe como configurar uma ação com o IBM Cloud Functions, acionar essa ação com um webhook e invocar APIs do GitHub utilizando código Python. Espero que você possa usar isso para beneficiar seu próprio fluxo de trabalho organizacional.

...

Quer ler mais conteúdo especializado de programação? Conheça o IBM Blue Profile e tenha acesso a matérias exclusivas, novas jornadas de conhecimento e testes personalizados. Confira agora mesmo, consiga as badges e dê um upgrade na sua carreira!

Você sabia que o TecMundo está no Facebook, Instagram, Telegram, TikTok, Twitter e no Whatsapp? Siga-nos por lá.