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:
Um usuário registra de modo detalhado um problema no GitHub sobre um repo que deseja criar;
Quando um problema é aprovado, uma payload (em português, carga útil) é enviada para uma ação serverless;
A ação serverless aciona algum código Python;
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
Uma conta IBM Cloud de nível gratuito;
Uma conta GitHub;
Uma organização GitHub da qual você é o proprietário. Você pode criar sua própria organização se não tiver acesso a uma.
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
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).
Escolha a opção Customizar Acionador (Custom Trigger).
Dê um nome ao novo acionador e insira uma descrição.
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.
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).
Dê um nome à nova ação, selecione o pacote padrão e escolha o Python 3 como o sistema de tempo de execução.
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).
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).
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.
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
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).
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í.
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.
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.
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!