Introducción
En este tutorial se muestra cómo crear un servidor de integración continua (CI) que ejecuta pruebas en código nuevo que se inserta en un repositorio. En el tutorial se muestra cómo compilar y configurar GitHub App para que actúe como un servidor que recibe eventos de webhook check_run
y check_suite
y responde a ellos mediante la API REST de GitHub.
En este tutorial, vas a usar el equipo o codespace como servidor mientras desarrollas la aplicación. Una vez que la aplicación esté lista para su uso en producción, debes implementarla en un servidor dedicado.
En este tutorial se usa Ruby, pero puedes usar cualquier lenguaje de programación que se pueda ejecutar en el servidor.
Este tutorial se divide en dos partes:
- En la primera parte, aprenderás a configurar el marco para un servidor de CI con la API REST de GitHub, crear nuevas ejecuciones de comprobación para pruebas de CI cuando un repositorio recibe confirmaciones recién insertadas y volver a ejecutar la comprobación cuando un usuario solicita esa acción en GitHub.
- En la segunda parte, agregarás funcionalidad a la prueba de CI agregando una prueba de linter al servidor de CI. También crearás anotaciones que se muestran en la pestaña Comprobaciones y Archivos modificados de una solicitud de incorporación de cambios y corregirás automáticamente las recomendaciones de linter mediante la exposición de un botón "Corregir esto" en la pestaña Comprobaciones de la solicitud de incorporación de cambios.
Acerca de la integración continua (CI)
La IC es una práctica de software que requiere código confirmado frecuente en un repositorio compartido. El código de confirmación generar errores de manera temprana frecuentemente, así como reduce la cantidad de código que necesita un desarrollador para hacer depuraciones cuando encuentra la fuente de un error. Las actualizaciones frecuentes de código facilitan también la fusión de cambios de diferentes miembros de un equipo de desarrollo de software. Esto es excelente para los desarrolladores, que pueden dedicar más tiempo a escribir el código y menos tiempo a depurar errores o resolver conflictos de fusión.
Un servidor de IC hospeda código que ejecuta pruebas de IC, tal como los limpíadores de código (que revisan el formato del estilo), revisiones de seguridad, cobertura de código, y otras verificaciones contra las confirmaciones de código nuevas que hay en un repositorio. Los servidores de IC incluso pueden crear y desplegar código en los servidores de pruebas y en los productivos. Para obtener ejemplos de los tipos de pruebas de CI que puedes crear con GitHub App, consulta las aplicaciones de integración continua que están disponibles en GitHub Marketplace.
Acerca de las comprobaciones
La API REST de GitHub te permite configurar pruebas de CI (comprobaciones) que se ejecutan automáticamente en cada confirmación de código de un repositorio. La API proporciona información detallada sobre cada comprobación en la pestaña Comprobaciones de la solicitud de incorporación de cambios en GitHub. Puedes utilizar las inserciones en un repositorio para determinar cuando una confirmación de código introduce errores.
Las comprobaciones incluyen ejecuciones de comprobación, conjuntos de comprobaciones y estados de confirmación.
- Una ejecución de comprobación es una prueba de CI individual que se ejecuta en una confirmación.
- Un conjunto de comprobaciones es un grupo de ejecuciones de comprobación.
- Un estado de confirmación marca el estado de una confirmación, por ejemplo
error
,failure
,pending
osuccess
, y está visible en una solicitud de incorporación de cambios en GitHub. Tanto los conjuntos de comprobaciones como las ejecuciones de comprobación contienen estados de confirmación.
GitHub crea automáticamente eventos check_suite
para nuevas confirmaciones de código en un repositorio mediante el flujo predeterminado, aunque puedes cambiar la configuración predeterminada. Para obtener más información, vea «Puntos de conexión de la API de REST para conjuntos de comprobación». Aquí te mostramos cómo funciona el flujo predeterminado:
- Cuando alguien inserta código en el repositorio, GitHub envía automáticamente el evento
check_suite
con una acción derequested
a todos los GitHub Apps instalados en el repositorio que tienen el permisochecks:write
. Este evento permite a las aplicaciones saber que el código se ha insertado en el repositorio, y que GitHub ha creado automáticamente un nuevo conjunto de comprobaciones. - Cuando la aplicación recibe este evento, puede agregar ejecuciones de comprobación a ese conjunto.
- Las ejecuciones de comprobación pueden incluir anotaciones que se muestran en líneas de código específicas. Las anotaciones están visibles en la pestaña Comprobaciones. Cuando se crean anotaciones para un archivo que forma parte de la solicitud de incorporación de cambios, las anotaciones también se muestran en la pestaña Archivos modificados. Para obtener más información, consulta el objeto
annotations
en "Puntos de conexión de la API de REST para ejecuciones de comprobación".
Para obtener más información sobre las comprobaciones, consulta "Puntos de conexión de la API de REST para comprobaciones" y "Uso de la API REST para interactuar con comprobaciones".
Requisitos previos
En este tutorial se supone que tiene conocimientos básicos del lenguaje de programación Ruby.
Antes de empezar, quizá debas familiarizarte con los conceptos siguientes:
Las comprobaciones también están disponibles para su uso con GraphQL API, pero este tutorial se centra en la API REST. Para obtener más información sobre los objetos GraphQL, consulta Conjunto de comprobaciones y Ejecución de comprobación en la documentación de GraphQL.
Configurar
Las secciones siguientes le guiarán a través de la configuración de los siguientes componentes:
- Un repositorio para almacenar el código de la aplicación.
- una manera de recibir webhooks localmente.
- Una instancia de GitHub App que está suscrita a los eventos de webhook "Conjunto de comprobaciones" y "Ejecución de comprobación" tiene permiso de escritura para las comprobaciones y utiliza una dirección URL de webhook que puedes recibir localmente.
Creación de un repositorio para almacenar código para GitHub App
-
Crea un repositorio para almacenar el código de la aplicación. Para obtener más información, vea «Crear un repositorio nuevo».
-
Clona el repositorio del paso anterior. Para obtener más información, vea «Clonar un repositorio». Puedes usar un clon local o GitHub Codespaces.
-
En un terminal, ve al directorio donde se almacena el clon.
-
Crea un archivo de Ruby denominado
server.rb
. Este archivo contendrá todo el código para la aplicación. Agregará contenido a este archivo más adelante. -
Si el directorio aún no incluye un archivo
.gitignore
, agrega un archivo.gitignore
. Agregará contenido a este archivo más adelante. Para más información sobre los archivos.gitignore
, consulta "Ignorar archivos". -
Cree un archivo llamado
Gemfile
. Este archivo describirá las dependencias de gema que necesita el código de Ruby. Agrega el siguiente contenido aGemfile
:Ruby source 'http://rubygems.org' gem 'sinatra', '~> 2.0' gem 'jwt', '~> 2.1' gem 'octokit', '~> 4.0' gem 'puma' gem 'rubocop' gem 'dotenv' gem 'git'
source 'http://rubygems.org' gem 'sinatra', '~> 2.0' gem 'jwt', '~> 2.1' gem 'octokit', '~> 4.0' gem 'puma' gem 'rubocop' gem 'dotenv' gem 'git'
-
Cree un archivo llamado
config.ru
. Este archivo configurará el servidor de Sinatra para que se ejecute. Agrega el siguiente contenido al archivoconfig.ru
:Ruby require './server' run GHAapp
require './server' run GHAapp
Obtención de una dirección URL del proxy de webhook
Para desarrollar la aplicación localmente, puedes usar una dirección URL del proxy de webhook para reenviar webhooks de GitHub al equipo o codespace. En este tutorial se usa Smee.io para proporcionar una dirección URL del proxy de webhook y reenviar eventos.
-
En un terminal, ejecuta el siguiente comando para instalar el cliente Smee:
Shell npm install --global smee-client
npm install --global smee-client
-
Abra el explorador y vaya a https://smee.io/.
-
Haz clic en Iniciar un nuevo canal.
-
Copia la dirección URL completa en "Dirección URL del proxy de webhook".
-
En el terminal, ejecuta el siguiente comando para iniciar el cliente Smee. Reemplaza
YOUR_DOMAIN
por la dirección URL del proxy de webhook que copiaste en el paso anterior.Shell smee --url YOUR_DOMAIN --path /event_handler --port 3000
smee --url YOUR_DOMAIN --path /event_handler --port 3000
El resultado debe ser parecido a lo siguiente:
Forwarding https://smee.io/YOUR_DOMAIN to http://127.0.0.1:3000/event_handler Connected https://smee.io/YOUR_DOMAIN
El comando smee --url https://smee.io/YOUR_DOMAIN
indica a Smee que reenvíe todos los eventos de webhook recibidos por el canal Smee al cliente Smee que se ejecuta en el equipo. La opción --path /event_handler
reenvía eventos a la ruta /event_handler
. La opción --port 3000
especifica el puerto 3000, que es el puerto en el que indicarás al servidor que escuche, cuando agregues más código más adelante en el tutorial. Si utilizas Smee, tu máquina no necesita estar abierta al internet público para recibir webhooks de GitHub. También puedes abrir la URL de Smee en tu buscador para inspeccionar las cargas útiles de los webhooks como vayan llegando.
Te recomendamos dejar abierta esta ventana de terminal y mantener a Smee conectado mientras completas el resto de los pasos de esta guía. Aunque puedes desconectar el cliente de Smee y volverlo a conectar sin perder el dominio único, es posible que te resulte más sencillo dejarlo conectado y realizar otras tareas de la línea de comandos en otra ventana de terminal.
Registro de una instancia de GitHub App
Para este tutorial, debes registrar una instancia de GitHub App que:
- Tenga webhooks activos
- Use una dirección URL de webhook que puedas recibir localmente.
- Tenga el permiso de repositorio "Comprobaciones".
- Se suscriba a los eventos de webhook "Conjunto de comprobaciones" y "Ejecución de comprobación".
Los pasos siguientes le guiarán a través de la configuración de una GitHub App con estas opciones. Para más información sobre la configuración de GitHub Apps, consulta "Registro de una instancia de GitHub App".
- En la esquina superior derecha de cualquier página en GitHub Enterprise Server, haga clic en su fotografía de perfil.
- Navega a la configuración de tu cuenta.
- Para una aplicación propiedad de una cuenta personal, haga clic en Configuración.
- Para una aplicación propiedad de una organización:
- Haga clic en Sus organizaciones.
- A la derecha de la organización, haga clic en Configuración.
- En la barra lateral izquierda, haz clic en Configuración del desarrollador.
- En la barra lateral de la izquierda, haga clic en GitHub Apps .
- Haga clic en New GitHub App (Nueva aplicación GitHub).
- En "Nombre de la aplicación de GitHub", escribe un nombre para la aplicación. Por ejemplo,
USERNAME-ci-test-app
dondeUSERNAME
es el nombre de usuario de GitHub. - En "URL de la página principal", escribe la dirección URL de la aplicación. Por ejemplo, puedes usar la dirección URL del repositorio que creaste para almacenar el código de la aplicación.
- Omite las secciones "Identificación y autorización de usuarios" y "Pasos posteriores a la instalación" de este tutorial.
- Asegúrate de que Activo esté seleccionado en "Webhooks".
- En "Dirección URL de webhook", escribe la dirección URL del proxy de webhook anterior. Para más información, consulta "Obtención de una dirección URL del proxy de webhook".
- En "Secreto de webhook", escribe una cadena aleatoria. Este secreto se usa para comprobar que los webhooks se envían por GitHub. Guarda esta cadena; la usarás más adelante.
- En "Permisos de repositorio", junto a "Comprobaciones", selecciona Lectura y escritura.
- En "Suscribirse a eventos", selecciona Conjunto de comprobaciones y Ejecución de comprobación.
- En "¿Dónde se puede instalar esta aplicación de GitHub?", selecciona Solo en esta cuenta. Puedes cambiar esto más adelante si quieres publicar la aplicación.
- Haga clic en Create GitHub App (Crear aplicación de GitHub).
Almacenamiento de la información de identificación y las credenciales de la aplicación
En este tutorial se muestra cómo almacenar las credenciales y la información de identificación de la aplicación como variables de entorno en un archivo .env
. Cuando implementes la aplicación, deberías cambiar la forma en que se almacenan las credenciales. Para más información, consulta "Implementación de la aplicación".
Asegúrate de que estás en una máquina segura antes de realizar estos pasos, ya que vas a almacenar las credenciales localmente.
-
En el terminal, ve al directorio donde se almacena el clon.
-
Cree un archivo llamado
.env
en el nivel superior de este directorio. -
Agregue
.env
al archivo.gitignore
. Esto evitará que confirmes accidentalmente las credenciales de la aplicación. -
Agrega el siguiente contenido al archivo
.env
: ReemplazaYOUR_HOSTNAME
por el nombre de tu instancia de GitHub Enterprise Server. Actualizarás los demás valores en un paso posterior.Shell GITHUB_APP_IDENTIFIER="YOUR_APP_ID" GITHUB_WEBHOOK_SECRET="YOUR_WEBHOOK_SECRET" GITHUB_PRIVATE_KEY="YOUR_PRIVATE_KEY"
GITHUB_APP_IDENTIFIER="YOUR_APP_ID" GITHUB_WEBHOOK_SECRET="YOUR_WEBHOOK_SECRET" GITHUB_PRIVATE_KEY="YOUR_PRIVATE_KEY"
-
Ve a la página "Configuración" de la aplicación:
-
En la esquina superior derecha de cualquier página en GitHub Enterprise Server, haga clic en su fotografía de perfil.
-
Navega a la configuración de tu cuenta.
- Para una aplicación propiedad de una cuenta personal, haga clic en Configuración.
- Para una aplicación propiedad de una organización:
- Haga clic en Sus organizaciones.
- A la derecha de la organización, haga clic en Configuración.
-
En la barra lateral izquierda, haz clic en Configuración del desarrollador.
-
En la barra lateral de la izquierda, haga clic en GitHub Apps .
-
Junto al nombre de la aplicación, haz clic en Editar.
-
-
En la página de configuración de la aplicación, junto a "Id. de la aplicación", busca el identificador de la aplicación.
-
En el archivo
.env
, reemplazaYOUR_APP_ID
por el identificador de la aplicación. -
En el archivo
.env
, reemplazaYOUR_WEBHOOK_SECRET
por el secreto de webhook de la aplicación. Si has olvidado el secreto de webhook, en "Secreto de webhook (opcional)", haz clic en Cambiar secreto. Especifica un nuevo secreto y, a continuación, haz clic en Guardar cambios. -
En la página de configuración de la aplicación, en "Claves privadas", haz clic en Generar una clave privada. Verás un archivo
.pem
de clave privada que se descarga en tu equipo. -
Abre el archivo
.pem
con un editor de texto o usa el siguiente comando en la línea de comandos para mostrar el contenido del archivo:cat PATH/TO/YOUR/private-key.pem
. -
Copia y pega todo el contenido del archivo en tu archivo
.env
como valor deGITHUB_PRIVATE_KEY
y agrega comillas dobles alrededor de todo el valor.Este es un archivo .env de ejemplo:
GITHUB_APP_IDENTIFIER=12345 GITHUB_WEBHOOK_SECRET=your webhook secret GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- ... HkVN9... ... -----END RSA PRIVATE KEY-----"
Adición de código para GitHub App
En esta sección se muestra cómo agregar algún código de plantilla básico para GitHub App, y se explicará lo que hace el código. Más adelante en el tutorial, aprenderás a modificar y agregar elementos a este código para crear la funcionalidad de la aplicación.
Agrega el código siguiente al archivo server.rb
:
require 'sinatra/base' # Use the Sinatra web framework require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API require 'dotenv/load' # Manages environment variables require 'json' # Allows your app to manipulate JSON data require 'openssl' # Verifies the webhook signature require 'jwt' # Authenticates a GitHub App require 'time' # Gets ISO 8601 representation of a Time object require 'logger' # Logs debug statements # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. class GHAapp < Sinatra::Application # Sets the port that's used when starting the web server. set :port, 3000 set :bind, '0.0.0.0' # Expects the private key in PEM format. Converts the newlines. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a webhook secret. # The secret is used to verify that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] # The GitHub App's identifier (type integer). APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER'] # Turn on Sinatra's verbose logging during development configure :development do set :logging, Logger::DEBUG end # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) verify_webhook_signature # If a repository name is provided in the webhook, validate that # it consists only of latin alphabetic characters, `-`, and `_`. unless @payload['repository'].nil? halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil? end authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do # ADD EVENT HANDLING HERE # 200 # success status end helpers do # ADD CREATE_CHECK_RUN HELPER METHOD HERE # # ADD INITIATE_CHECK_RUN HELPER METHOD HERE # # ADD CLONE_REPOSITORY HELPER METHOD HERE # # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE # # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) # request.body is an IO or StringIO object # Rewind in case someone already read it request.body.rewind # The raw text of the body is required for webhook signature verification @payload_raw = request.body.read begin @payload = JSON.parse @payload_raw rescue => e fail 'Invalid JSON (#{e}): #{@payload_raw}' end end # Instantiate an Octokit client authenticated as a GitHub App. # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app and not altered by # a malicious third party. def authenticate_app payload = { # The time that this JWT was issued, _i.e._ now. iat: Time.now.to_i, # JWT expiration time (10 minute maximum) exp: Time.now.to_i + (10 * 60), # Your GitHub App's identifier number iss: APP_IDENTIFIER } # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end # Instantiate an Octokit client, authenticated as an installation of a # GitHub App, to run API operations. def authenticate_installation(payload) @installation_id = payload['installation']['id'] @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to # create the hash signature sent in the `X-HUB-Signature` header of each # webhook. This code computes the expected hash signature and compares it to # the signature sent in the `X-HUB-Signature` header. If they don't match, # this request is an attack, and you should reject it. GitHub uses the HMAC # hexdigest to compute the signature. The `X-HUB-Signature` looks something # like this: 'sha1=123456'. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw) halt 401 unless their_digest == our_digest # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end end # Finally some logic to let us run this server directly from the command line, # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the # Sinatra run method run! if __FILE__ == $0 end
require 'sinatra/base' # Use the Sinatra web framework
require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API
require 'dotenv/load' # Manages environment variables
require 'json' # Allows your app to manipulate JSON data
require 'openssl' # Verifies the webhook signature
require 'jwt' # Authenticates a GitHub App
require 'time' # Gets ISO 8601 representation of a Time object
require 'logger' # Logs debug statements
# This code is a Sinatra app, for two reasons:
# 1. Because the app will require a landing page for installation.
# 2. To easily handle webhook events.
class GHAapp < Sinatra::Application
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
# Expects the private key in PEM format. Converts the newlines.
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
# Your registered app must have a webhook secret.
# The secret is used to verify that webhooks are sent by GitHub.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
# The GitHub App's identifier (type integer).
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# If a repository name is provided in the webhook, validate that
# it consists only of latin alphabetic characters, `-`, and `_`.
unless @payload['repository'].nil?
halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
end
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
helpers do
# ADD CREATE_CHECK_RUN HELPER METHOD HERE #
# ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
# ADD CLONE_REPOSITORY HELPER METHOD HERE #
# ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
# Saves the raw payload and converts the payload to JSON format
def get_payload_request(request)
# request.body is an IO or StringIO object
# Rewind in case someone already read it
request.body.rewind
# The raw text of the body is required for webhook signature verification
@payload_raw = request.body.read
begin
@payload = JSON.parse @payload_raw
rescue => e
fail 'Invalid JSON (#{e}): #{@payload_raw}'
end
end
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app and not altered by
# a malicious third party.
def authenticate_app
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,
# JWT expiration time (10 minute maximum)
exp: Time.now.to_i + (10 * 60),
# Your GitHub App's identifier number
iss: APP_IDENTIFIER
}
# Cryptographically sign the JWT.
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
# Create the Octokit client, using the JWT as the auth token.
@app_client ||= Octokit::Client.new(bearer_token: jwt)
end
# Instantiate an Octokit client, authenticated as an installation of a
# GitHub App, to run API operations.
def authenticate_installation(payload)
@installation_id = payload['installation']['id']
@installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
@installation_client = Octokit::Client.new(bearer_token: @installation_token)
end
# Check X-Hub-Signature to confirm that this webhook was generated by
# GitHub, and not a malicious third party.
#
# GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
# create the hash signature sent in the `X-HUB-Signature` header of each
# webhook. This code computes the expected hash signature and compares it to
# the signature sent in the `X-HUB-Signature` header. If they don't match,
# this request is an attack, and you should reject it. GitHub uses the HMAC
# hexdigest to compute the signature. The `X-HUB-Signature` looks something
# like this: 'sha1=123456'.
def verify_webhook_signature
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
method, their_digest = their_signature_header.split('=')
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
halt 401 unless their_digest == our_digest
# The X-GITHUB-EVENT header provides the name of the event.
# The action value indicates the which action triggered the event.
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
end
end
# Finally some logic to let us run this server directly from the command line,
# or with Rack. Don't worry too much about this code. But, for the curious:
# $0 is the executed file
# __FILE__ is the current file
# If they are the same—that is, we are running this file directly, call the
# Sinatra run method
run! if __FILE__ == $0
end
En el resto de esta sección se explica lo que hace el código de la plantilla. No hay algún paso que necesites completar en esta sección. Si ya estás familiarizado con el código de plantilla, puedes pasar a "Inicio del servidor".
Descripción del código de plantilla
Abra el archivo server.rb
en un editor de texto. Verás los comentarios a lo largo de este archivo, que proporcionan contexto adicional para el código de la plantilla. Te recomendamos leer estos comentarios cuidadosamente, e incluso, agregar tus propios comentarios para complementar el código que escribas.
Debajo de la lista de archivos necesarios, el primer código que verás es la declaración class GHApp < Sinatra::Application
. Escribirás todo el código de GitHub App dentro de esta clase. En las secciones siguientes se explica con detalle lo que hace el código dentro de esta clase.
- Establecimiento del puerto
- Lee las variables de entorno
- Active el registro.
- Definición de un filtro
before
- Define el controlador de ruta
- Definición de métodos auxiliares
Establecimiento del puerto
Lo primero que verás dentro de la declaración class GHApp < Sinatra::Application
es set :port 3000
. Esto establece el puerto usado al iniciar el servidor web, para que coincida con el puerto al que redirigió las cargas de webhook a en "Obtención de una dirección URL del proxy de webhook".
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
Lee las variables de ambiente
A continuación, esta clase lee las tres variables de entorno establecidas en "Almacenamiento de la información de identificación y las credenciales de la aplicación" y las almacena en variables para usarlas más adelante.
# Expects the private key in PEM format. Converts the newlines.
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
# Your registered app must have a webhook secret.
# The secret is used to verify that webhooks are sent by GitHub.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
# The GitHub App's identifier (type integer).
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
Active el registro.
Posteriormente, hay un bloqueo de código que habilita el inicio de sesión durante el desarrollo, el cual es el ambiente predeterminado en Sinatra. Este código activa el registro en el nivel DEBUG
para mostrar una salida útil en el terminal mientras desarrollas la aplicación.
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
Definición de un filtro before
Sinatra usa filtros before
que permiten ejecutar código antes del controlador de ruta. El bloque before
de la plantilla llama a cuatro métodos auxiliares: get_payload_request
, verify_webhook_signature
, authenticate_app
y authenticate_installation
. Para más información, consulta "Filtros" y "Auxiliares" en la documentación de Sinatra.
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# If a repository name is provided in the webhook, validate that
# it consists only of latin alphabetic characters, `-`, and `_`.
unless @payload['repository'].nil?
halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
end
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
Cada uno de estos métodos auxiliares se define más adelante en el código, en el bloque de código que comienza por helpers do
. Para obtener más información, consulta "Definición de los métodos auxiliares".
En verify_webhook_signature
, el código que comienza por unless @payload
es una medida de seguridad. Si se proporciona un nombre de repositorio con una carga de webhook, este código valida que el nombre del repositorio solo contiene caracteres alfabéticos latinos, guiones y caracteres de subrayado. Se garantiza así que un actor incorrecto no intente ejecutar comandos arbitrarios ni insertar nombres de repositorio falsos. Más adelante, en el bloque de código que comienza por helpers do
, el método auxiliar verify_webhook_signature
también valida las cargas de webhook entrantes como una medida de seguridad adicional.
Define el gestor de la ruta
Se incluye una ruta vacía en el código de la plantilla. Este código controla todas las solicitudes POST
a la ruta /event_handler
. Más adelante agregarás más código.
post '/event_handler' do
end
Definición de métodos auxiliares
Se llama a cuatro métodos auxiliares en el bloque before
del código de plantilla. El bloque de código helpers do
define cada uno de estos métodos auxiliares.
Gestionar la carga útil del webhok
El primer método auxiliar get_payload_request
captura la carga de webhook y la convierte a formato JSON, lo que facilita mucho el acceso a los datos de la carga.
Verificar la firma del webhook
El segundo método auxiliar verify_webhook_signature
realiza la comprobación de la firma de webhook para asegurarse de que GitHub ha generado el evento. Para obtener más información sobre el código del método auxiliar verify_webhook_signature
, consulta "Validación de entregas de webhook". Si los webhooks son seguros, este método registrará todas las cargas útiles entrantes en tu terminal. El código de registro es útil para verificar que tu servidor web esté funcionando.
Autenticarse como una GitHub App
El tercer método auxiliar authenticate_app
permite que GitHub App se autentique, por lo que puede solicitar un token de instalación.
Para realizar llamadas API, usará la biblioteca Octokit. Para utilizar esta biblioteca de manera interesante, será necesario que GitHub App se autentique. Para obtener más información sobre la biblioteca de Octokit, consulta la documentación de Octokit.
GitHub Apps tienen tres métodos de autenticación:
- Autenticarse como GitHub App mediante JSON Web Token (JWT).
- Autenticarse como una instalación específica de GitHub App mediante un token de acceso de instalación.
- Autenticarse en nombre de un usuario. Este tutorial no usará este método de autenticación.
Obtendrás información sobre la autenticación como una instalación en la sección siguiente, "Autenticación como una instalación".
El autenticarte como una GitHub App te permite hacer un par de cosas:
- Puedes recuperar información administrativa de alto nivel acerca de tu GitHub App.
- Puedes solicitar tokens de acceso para una instalación de la app.
Por ejemplo, te podrías autenticar como GitHub App para solicitar una lista de las cuentas (de organización y de persona) que han instalado tu aplicación. Pero este método de autenticación no te permite hacer mucho con la API. Para acceder a los datos del repositorio y realizar operaciones en nombre de la instalación, necesitas autenticarte como una instalación. Para hacerlo, primero necesitarás autenticarte como GitHub App para solicitar un token de acceso a la instalación. Para obtener más información, vea «Acerca de la autenticación con una aplicación de GitHub».
A fin de poder usar la biblioteca Octokit.rb para realizar llamadas API, tendrás que inicializar un cliente de Octokit autenticado como GitHub App con un método auxiliar authenticate_app
.
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app an not altered by
# a malicious third party.
def authenticate_app
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,
# JWT expiration time (10 minute maximum)
exp: Time.now.to_i + (10 * 60),
# Your GitHub App's identifier number
iss: APP_IDENTIFIER
}
# Cryptographically sign the JWT
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
# Create the Octokit client, using the JWT as the auth token.
@app_client ||= Octokit::Client.new(bearer_token: jwt)
end
El código anterior genera un JSON Web Token (JWT) y lo usa, junto con la clave privada de la aplicación, para inicializar el cliente de Octokit. GitHub revisa la autenticación de una solicitud verificando el token con la llave pública almacenada en la app. Para obtener más información sobre el funcionamiento de este código, consulta "Generación de un JSON Web Token (JWT) para una aplicación de GitHub".
Autenticarse como una instalación
El cuarto y último método auxiliar, authenticate_installation
, inicializa un cliente de Octokit autenticado como una instalación, que puedes usar para realizar llamadas autenticadas a la API.
Una instalación hace referencia a cualquier cuenta de usuario u organización que haya instalado la aplicación. Incluso si alguien concede a la aplicación acceso a más de un repositorio en esa cuenta, solo cuenta como una instalación porque está dentro de la misma cuenta.
# Instantiate an Octokit client authenticated as an installation of a
# GitHub App to run API operations.
def authenticate_installation(payload)
installation_id = payload['installation']['id']
installation_token = @app_client.create_app_installation_access_token(installation_id)[:token]
@installation_client = Octokit::Client.new(bearer_token: installation_token)
end
El método create_app_installation_access_token
de Octokit crea un token de instalación. Para obtener más información, consulta "create_installation_access_token" en la documentación de Octokit.
Este método acepta dos argumentos:
- Instalación (entero): identificador de una instalación de GitHub App.
- Opciones (hash, el valor predeterminado es
{}
): un conjunto personalizable de opciones
Cada vez que GitHub App recibe un webhook, incluye un objeto installation
con una instancia de id
. Mediante el cliente autenticado como GitHub App, este identificador se pasa al método create_app_installation_access_token
a fin de generar un token de acceso para cada instalación. Ya que no estás pasando ninguna opción al método, ésta será un hash vacío predeterminadamente. La respuesta de create_app_installation_access_token
incluye dos campos: token
y expired_at
. El código de la plantilla selecciona al token en la respuesta e inicializa un cliente de instalación.
Una vez teniendo listo este métido, cada vez que tu app reciba una nueva carga útil de un webhook, este creará un cliente para la instalación que activó dicho evento. Este proceso de autenticación habilita a tu GitHub App para que trabaje para todas las instalaciones en cualquier cuenta.
Inicio del servidor
La aplicación todavía no hace nada, pero en este punto, puede hacer que se ejecute en el servidor.
-
En el terminal, asegúrate de que Smee todavía está en ejecución. Para más información, consulta "Obtención de una dirección URL del proxy de webhook".
-
Abre una nueva pestaña en el terminal y
cd
en el directorio donde clonaste el repositorio que creaste anteriormente en el tutorial. Para obtener más información, consulta "Creación de un repositorio para almacenar código para la aplicación de GitHub". El código de Ruby de este repositorio iniciará un servidor web de Sinatra. -
Instala las dependencias ejecutando los dos comandos siguientes uno después del otro:
Shell gem install bundler
gem install bundler
Shell bundle install
bundle install
-
Después de instalar las dependencias, inicia el servidor ejecutando este comando:
Shell bundle exec ruby server.rb
bundle exec ruby server.rb
Debería obtener una respuesta similar a la siguiente:
> == Sinatra (v2.2.3) has taken the stage on 3000 for development with backup from Puma > Puma starting in single mode... > * Puma version: 6.3.0 (ruby 3.1.2-p20) ("Mugi No Toki Itaru") > * Min threads: 0 > * Max threads: 5 > * Environment: development > * PID: 14915 > * Listening on http://0.0.0.0:3000 > Use Ctrl-C to stop
Si ve un error, asegúrese de que ha creado el archivo
.env
en el directorio que contieneserver.rb
. -
Para probar el servidor, ve con el explorador a
http://localhost:3000
.Si ves una página de error que indica que Sinatra no conoce este problema, la aplicación funciona según lo previsto. Aunque es una página de error, es una página de error de Sinatra, lo que significa que la aplicación está conectada al servidor de la forma esperada. Estás viendo este mensaje porque no le has dado nada más que mostrar a la app.
Prueba de que el servidor escucha la aplicación
Puedes probar que el servidor está escuchando a tu app si activas un evento para que lo reciba. Para ello, instalarás la aplicación en un repositorio de pruebas, que enviará el evento installation
a la aplicación. Si la aplicación lo recibe, deberías ver alguna salida en la pestaña de terminal donde estás ejecutando server.rb
.
-
Crea un repositorio que se usará para probar el código del tutorial. Para obtener más información, vea «Crear un repositorio nuevo».
-
Instala GitHub App en el repositorio que acabas de crear. Para obtener más información, vea «Instalación de tu propia instancia de GitHub App». Durante el proceso de instalación, elige Seleccionar solo repositorios y selecciona el repositorio que creaste en el paso anterior.
-
Después de hacer clic en Instalar, examina la salida en la pestaña de terminal donde se ejecuta
server.rb
. Deberíamos ver algo parecido a lo siguiente:> D, [2023-06-08T15:45:43.773077 #30488] DEBUG -- : ---- received event installation > D, [2023-06-08T15:45:43.773141 #30488]] DEBUG -- : ---- action created > 192.30.252.44 - - [08/Jun/2023:15:45:43 -0400] "POST /event_handler HTTP/1.1" 200 - 0.5390
Si ves una salida similar a esta, la aplicación recibió una notificación de que se instaló en la cuenta de GitHub. La aplicación se ejecuta en el servidor según lo esperado.
Si no ves la salida, asegúrate de que Smee se ejecuta correctamente en otra pestaña de terminal. Si tienes que reiniciar Smee, recuerda que también tendrás que desinstalar la aplicación y volverla a instalarla para enviar el evento
installation
a la aplicación de nuevo y ver la salida en el terminal.
Si te preguntas de dónde procede la salida del terminal anterior, está escrita en el código de plantilla de aplicación que agregaste a server.rb
en "Adición de código para GitHub App".
Parte 1. Crear la interface de la API de Verificaciones
En esta parte, agregarás el código necesario para recibir eventos del webhook check_suite
y para crear y actualizar las ejecuciones de comprobación. También aprenderás cómo crear ejecuciones de comprobación cuando se vuelva a solicitar una verificación en GitHub. Al final de esta sección, podrás ver la ejecución de comprobación que creaste en una solicitud de incorporación de cambios de GitHub.
En esta sección, tu ejecución de verificación comprobación no realizará ninguna verificación de código. Agregarás esa funcionalidad en la "parte 2: Creación de una prueba de CI".
Ya deberías haber configurado el canal de Smee que reenviará las cargas útiles del webhook a tu servidor local. Tu servidor deberá estar funcionando y también estar conectado con la GitHub App que registraste e instalaste en un repositorio de prueba.
Estos son los pasos que completarás en la Parte 1:
- Agregar la gestión de eventos
- Crear una ejecución de comprobación
- Actualizar una ejecución de comprobación
Paso 1.1. Agregar la gestión de eventos
Ahora que la aplicación está suscrita a los eventos Conjunto de comprobaciones y Ejecución de comprobación, comenzarás a recibir los webhooks check_suite
y check_run
. GitHub envía cargas de webhook como solicitudes POST
. Dado que ha reenviado las cargas de webhook de Smee a http://localhost:3000/event_handler
, el servidor recibirá las cargas de solicitud POST
en la ruta post '/event_handler'
.
Abre el archivo server.rb
que creaste en "Adición de código para GitHub App" y busca el código siguiente. Se incluye una ruta post '/event_handler'
vacía en el código de la plantilla. La ruta vacía se ve así:
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
En el bloque de código que comienza por post '/event_handler' do
, donde dice # ADD EVENT HANDLING HERE #
, agrega el código siguiente. Esta ruta controlará el evento check_suite
.
# Get the event type from the HTTP_X_GITHUB_EVENT header case request.env['HTTP_X_GITHUB_EVENT'] when 'check_suite' # A new check_suite has been created. Create a new check run with status queued if @payload['action'] == 'requested' || @payload['action'] == 'rerequested' create_check_run end # ADD CHECK_RUN METHOD HERE # end
# Get the event type from the HTTP_X_GITHUB_EVENT header
case request.env['HTTP_X_GITHUB_EVENT']
when 'check_suite'
# A new check_suite has been created. Create a new check run with status queued
if @payload['action'] == 'requested' || @payload['action'] == 'rerequested'
create_check_run
end
# ADD CHECK_RUN METHOD HERE #
end
Cada evento que GitHub envía incluye un encabezado de solicitud denominado HTTP_X_GITHUB_EVENT
, que indica el tipo de evento en la solicitud POST
. En este momento, solo le interesan los eventos de tipo check_suite
, que se emiten cuando se crea un nuevo conjunto de comprobaciones. Cada evento tiene un campo action
adicional que indica el tipo de acción que ha activado los eventos. Para check_suite
, el campo action
puede ser requested
, rerequested
o completed
.
La acción requested
solicita una ejecución de comprobación cada vez que el código se inserta en el repositorio, mientras que la acción rerequested
solicita que se vuelva a ejecutar una comprobación para el código que ya existe en el repositorio. Dado que las acciones requested
y rerequested
requieren la creación de una ejecución de comprobación, llamará a un asistente denominado create_check_run
. Vamos a escribir ese método ahora.
Paso 1.2. Crear una ejecución de comprobación
Agregará este nuevo método como asistente de Sinatra en caso de que quiera que otras rutas también lo usen.
En el bloque de código que comienza por helpers do
, donde dice # ADD CREATE_CHECK_RUN HELPER METHOD HERE #
, agrega el código siguiente:
# Create a new check run with status "queued" def create_check_run @installation_client.create_check_run( # [String, Integer, Hash, Octokit Repository object] A GitHub repository. @payload['repository']['full_name'], # [String] The name of your check run. 'Octo RuboCop', # [String] The SHA of the commit to check # The payload structure differs depending on whether a check run or a check suite event occurred. @payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'], # [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use. accept: 'application/vnd.github+json' ) end
# Create a new check run with status "queued"
def create_check_run
@installation_client.create_check_run(
# [String, Integer, Hash, Octokit Repository object] A GitHub repository.
@payload['repository']['full_name'],
# [String] The name of your check run.
'Octo RuboCop',
# [String] The SHA of the commit to check
# The payload structure differs depending on whether a check run or a check suite event occurred.
@payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'],
# [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use.
accept: 'application/vnd.github+json'
)
end
Este código llama al punto de conexión POST /repos/{owner}/{repo}/check-runs
mediante el método create_check_run de Octokit. Para obtener más información sobre el punto de conexión, consulta "Puntos de conexión de la API de REST para ejecuciones de comprobación".
Para crear una ejecución de comprobación, solo se requieren dos parámetros de entrada: name
y head_sha
. En este código, asignamos el nombre "Octo RuboCop" a la comprobación porque usaremos RuboCop para implementar la prueba de CI más adelante en el tutorial. Pero puedes elegir cualquier nombre que quieras para la ejecución de comprobación. Para más información sobre RuboCop, consulta la documentación.
Ahora mismo, solo estás proporcionando los parámetros requeridos para echar a andar la funcionalidad básica, pero actualizarás la ejecución de verificación más adelante mientras recolectes más información acerca de la ejecución de verificación. De forma predeterminada, GitHub establece status
en queued
.
GitHub crea una ejecución de comprobación para un SHA de confirmación específico, por lo que head_sha
es un parámetro necesario. Puedes encontrar el SHA de la confirmación en la carga útil del webhook. Aunque solo va a crear una ejecución de comprobación para el evento check_suite
en este momento, es bueno saber que head_sha
se incluye tanto en los objetos check_suite
como check_run
en las cargas de eventos.
En el código anterior, se usa el operador ternario, que funciona como una instrucción if/else
, para comprobar si la carga contiene un objeto check_run
. Si lo contiene, debe leer head_sha
desde el objeto check_run
; de lo contrario, lo leerá desde el objeto check_suite
.
Prueba del código
Los pasos siguientes te mostrarán cómo probar que el código funciona y que crea correctamente una nueva ejecución de comprobación.
-
Ejecuta el comando siguiente para reiniciar el servidor desde el terminal. Si el servidor ya se está ejecutando, escribe primero
Ctrl-C
en el terminal para detener el servidor y, a continuación, ejecuta el siguiente comando para iniciar el servidor de nuevo.Shell ruby server.rb
ruby server.rb
-
Crea una solicitud de incorporación de cambios en el repositorio de pruebas que creaste en "Prueba de que el servidor escucha la aplicación". Este es el repositorio al que concediste acceso a la aplicación.
-
En la solicitud de incorporación de cambios que acabas de crear, ve a la pestaña Comprobaciones. Deberías ver una ejecución de comprobación con el nombre "Octo RuboCop" o el nombre que elegiste anteriormente para la ejecución de comprobación.
Si ves otras aplicaciones en la pestaña Comprobaciones, significa que tienes otras aplicaciones instaladas en el repositorio que tienen acceso de lectura y escritura a las comprobaciones y están suscritas a los eventos Conjunto de comprobaciones y Ejecución de comprobación. También puede significar que tienes flujos de trabajo de GitHub Actions en el repositorio desencadenados por el evento pull_request
o pull_request_target
.
Hasta ahora le has dicho a GitHub que cree una comprobación. El estado de la ejecución de la comprobación en la solicitud de incorporación de cambios se establece en cola con un icono amarillo. En el paso siguiente, esperarás a que GitHub cree la ejecución de comprobación y actualice su estado.
Paso 1.3. Actualizar una ejecución de comprobación
Cuando tu método create_check_run
se ejecuta, pide a GitHub que cree una nueva ejecución de comprobación. Cuando GitHub termine de crear la ejecución de comprobación, recibirás el evento de webhook check_run
con la acción created
. Este evento es tu señal para comenzar a ejecutar la verificación.
Actualizarás el controlador de eventos para buscar la acción created
. Mientras actualiza el controlador de eventos, puede agregar un condicional para la acción rerequested
. Cuando alguien vuelve a ejecutar una única prueba en GitHub haciendo clic en el botón "Volver a ejecutar", GitHub envía el evento de ejecución de comprobación rerequested
a tu aplicación. Si una ejecución de comprobación tiene el estado rerequested
, comienza de nuevo todo el proceso y crea una nueva ejecución de comprobación. Para ello, incluirás una condición para el evento check_run
en la ruta post '/event_handler'
.
En el bloque de código que comienza por post '/event_handler' do
, donde dice # ADD CHECK_RUN METHOD HERE #
, agrega el código siguiente:
when 'check_run' # Check that the event is being sent to this app if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER case @payload['action'] when 'created' initiate_check_run when 'rerequested' create_check_run # ADD REQUESTED_ACTION METHOD HERE # end end
when 'check_run'
# Check that the event is being sent to this app
if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
case @payload['action']
when 'created'
initiate_check_run
when 'rerequested'
create_check_run
# ADD REQUESTED_ACTION METHOD HERE #
end
end
GitHub envía todos los eventos de las ejecuciones de comprobación con el estado created
a todas las aplicaciones instaladas en un repositorio que tengan los permisos de comprobación necesarios. Esto significa que tu app recibirá las ejecuciones de verificación que creen otras apps. Una ejecución de comprobación created
es un poco diferente de un conjunto de comprobaciones requested
o rerequested
, y GitHub solo la envía a las aplicaciones a las que se solicita que ejecuten una comprobación. El código anterior busca la ID de aplicación de la ejecución de verificación. Esto filtra todas las ejecuciones de verificación para otras apps en el repositorio.
Después, escribirá el método initiate_check_run
, que es donde actualizará el estado de la ejecución de comprobación y donde se preparará para iniciar la prueba de CI.
En esta sección, aún no va a lanzar la prueba de CI, pero le mostraremos cómo actualizar el estado de la ejecución de comprobación de queued
a pending
y, después, de pending
a completed
para ver el flujo general de una ejecución de comprobación. En la parte 2: "Creación de una prueba de CI", agregará el código que ejecuta realmente la prueba de CI.
Vamos a crear el método initiate_check_run
y a actualizar el estado de la ejecución de comprobación.
En el bloque de código que comienza por helpers do
, donde dice # ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
, agrega el código siguiente:
# Start the CI process def initiate_check_run # Once the check run is created, you'll update the status of the check run # to 'in_progress' and run the CI process. When the CI finishes, you'll # update the check run status to 'completed' and add the CI results. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'in_progress', accept: 'application/vnd.github+json' ) # ***** RUN A CI TEST ***** # Mark the check run as complete! @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'completed', conclusion: 'success', accept: 'application/vnd.github+json' ) end
# Start the CI process
def initiate_check_run
# Once the check run is created, you'll update the status of the check run
# to 'in_progress' and run the CI process. When the CI finishes, you'll
# update the check run status to 'completed' and add the CI results.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'in_progress',
accept: 'application/vnd.github+json'
)
# ***** RUN A CI TEST *****
# Mark the check run as complete!
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: 'success',
accept: 'application/vnd.github+json'
)
end
El código anterior llama al punto de conexión PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
mediante el update_check_run
método Octokit y actualiza la ejecución de comprobación que ya has creado. Para obtener más información sobre el punto de conexión, consulta "Puntos de conexión de la API de REST para ejecuciones de comprobación".
Te explicamos lo que hace este código. En primer lugar, actualiza el estado de la ejecución de comprobación a in_progress
y establece implícitamente la hora de started_at
en la hora actual. En la parte 2 de este tutorial, agregarás código que inicia una prueba de CI real en ***** RUN A CI TEST *****
. Por el momento, dejarás esta sección como un marcador de posición para que el código subsecuente simplemente estimule el éxito del proceso de IC y que todas las pruebas pasen. Por último, el código vuelve a actualizar el estado de la ejecución de comprobación a completed
.
Cuando usas la API REST para proporcionar un estado de comprobación de ejecución de completed
, se requieren los parámetros conclusion
y completed_at
. El objeto conclusion
resume el resultado de una ejecución de comprobación y puede ser success
, failure
, neutral
, cancelled
, timed_out
, skipped
o action_required
. Por tanto, establecerá el estado de la conclusión en success
, la hora de completed_at
en la hora actual y el estado en completed
.
También puedes proporcionar más detalles sobre lo que está haciendo tu verificación, pero eso lo abordaremos en la siguiente sección.
Prueba del código
Los pasos siguientes te mostrarán cómo probar que el código funciona y que el nuevo botón "Volver a ejecutar todo" que creaste funciona.
-
Ejecuta el comando siguiente para reiniciar el servidor desde el terminal. Si el servidor ya se está ejecutando, escribe primero
Ctrl-C
en el terminal para detener el servidor y, a continuación, ejecuta el siguiente comando para iniciar el servidor de nuevo.Shell ruby server.rb
ruby server.rb
-
Crea una solicitud de incorporación de cambios en el repositorio de pruebas que creaste en "Prueba de que el servidor escucha la aplicación". Este es el repositorio al que concediste acceso a la aplicación.
-
En la solicitud de incorporación de cambios que acabas de crear, ve a la pestaña Comprobaciones. Deberías ver un botón "Volver a ejecutar todo".
-
Haz clic en el botón "Volver a ejecutar todo" en la esquina superior derecha. La prueba debería ejecutarse de nuevo y terminar con
success
.
Parte 2. Creación de una prueba de CI
Ahora que tienes la interfaz que se ha creado para recibir eventos de la API de comprobaciones y para crear ejecuciones de comprobación, puedes crear una ejecución de comprobación que implemente una prueba de CI.
RuboCop es un linter de código de Ruby y un formateador. Comprueba el código de Ruby para asegurarse de que cumple con la Guía de estilo de Ruby. Para obtener más información, consulta la documentación de RuboCop.
RuboCop tiene tres funciones prncipales:
- Limpiar para revisar el estilo del código
- Formateo del código
- Reemplazar las funcionalidades nativas de linting de Ruby mediante
ruby -w
Tu aplicación ejecutará RuboCop en el servidor de CI y creará ejecuciones de verificación (en este caso, pruebas de CI) que informen de los resultados que RuboCop envía a GitHub.
La API REST te permite notificar detalles enriquecidos acerca de cada ejecución de verificación, incluyendo los estados, las imágenes, los resúmenes y las acciones solicitadas.
Las anotaciones son información acerca de líneas de código específicas en un repositorio. Una anotación te permite identificar y visualizar las partes exactas del código para las cuales quieres mostrar información adicional. Por ejemplo, podría mostrar esa información como comentario, error o advertencia en una línea de código específica. Este tutorial utiliza anotaciones para visualizar los errores de RuboCop.
Para aprovechar las acciones solicitadas, los desarrolladores de aplicaciones pueden crear botones en la pestaña Checks (Comprobaciones) de las solicitudes de incorporación de cambios. Cuando alguien hace clic en uno de estos botones, se envía un evento requested_action
check_run
a GitHub App. El desarrollador de la app puede configurar íntegramente la acción que ésta toma. Este tutorial te mostrará cómo agregar un botón que permitirá a los usuarios solicitar que RuboCop corrija los errores que encuentre. RuboCop admite la corrección automática de errores mediante el uso de una opción de línea de comandos, por lo que configurará requested_action
para aprovechar esta opción.
Estos son los pasos que tendrás que completar en esta sección:
- Adición de un archivo de Ruby
- Permitir que RuboCop clone el repositorio de pruebas
- Ejecución de RuboCop
- Recopilación de errores de RuboCop
- Actualización de la ejecución de comprobación con resultados de la prueba de CI
- Corrección automática de los errores de RuboCop
Paso 2.1. Adición de un archivo de Ruby
Puedes pasar archivos específicos o directorios completos para que los revise RuboCop. En este tutorial, ejecutarás RuboCop en un directorio completo. RuboCop solo comprueba el código de Ruby. Para probar GitHub App, deberás agregar un archivo de Ruby en el repositorio que contenga errores para que RuboCop los encuentre. Después de agregar el siguiente archivo de Ruby al repositorio, actualizarás la comprobación de CI para ejecutar RuboCop en el código.
-
Ve al repositorio de pruebas que creaste en "Prueba de que el servidor escucha la aplicación". Este es el repositorio al que concediste acceso a la aplicación.
-
Cree un nuevo archivo llamado
myfile.rb
. Para obtener más información, vea «Crear nuevos archivos». -
Agregue el contenido siguiente a
myfile.rb
:Ruby # frozen_string_literal: true # The Octocat class tells you about different breeds of Octocat class Octocat def initialize(name, *breeds) # Instance variables @name = name @breeds = breeds end def display breed = @breeds.join("-") puts "I am of #{breed} breed, and my name is #{@name}." end end m = Octocat.new("Mona", "cat", "octopus") m.display
# frozen_string_literal: true # The Octocat class tells you about different breeds of Octocat class Octocat def initialize(name, *breeds) # Instance variables @name = name @breeds = breeds end def display breed = @breeds.join("-") puts "I am of #{breed} breed, and my name is #{@name}." end end m = Octocat.new("Mona", "cat", "octopus") m.display
-
Si creaste el archivo localmente, asegúrate de confirmar e insertar el archivo en el repositorio en GitHub.
Paso 2.2. Permitir que RuboCop clone el repositorio de pruebas
RuboCop se encuentra disponible como una utilidad de línea de comandos. Esto significa que, si deseas ejecutar RuboCop en un repositorio, GitHub App deberá clonar una copia local del repositorio en el servidor de CI para que RuboCop pueda analizar los archivos. Para ello, el código tendrá que poder ejecutar operaciones de Git y GitHub App tendrá que tener los permisos correctos para clonar un repositorio.
Concesión de permiso a las operaciones de Git
Para ejecutar operaciones de Git en la aplicación de Ruby, puede usar la gema ruby-git. El elemento Gemfile
que creaste en "Configuración" ya incluye la gema ruby-git, y la instalaste cuando ejecutaste bundle install
en "Inicio del servidor".
Ahora, en la parte superior del archivo server.rb
, debajo de los demás elementos require
, agrega el código siguiente:
require 'git'
require 'git'
Actualización de los permisos de la aplicación
A continuación, deberás actualizar los permisos de GitHub App. Tu aplicación necesita el permiso de lectura para "Contenido" si quieres que clone un repositorio. Y más adelante en este tutorial, necesitarás permiso de escritura para insertar contenido en GitHub. Para actualizar los permisos de tu app:
- Selecciona la aplicación en la página de configuración de la aplicación y haz clic en Permisos y eventos en la barra lateral.
- En "Permisos de repositorio", junto a "Contenido", selecciona Lectura y escritura.
- Haga clic en Save changes (Guardar cambios) en la parte inferior de la página.
- Si instalaste la app en tu cuenta, revisa tu correo electrónico y sigue el enlace para aceptar los permisos nuevos. Cada que cambias los permisos o los webhooks de tu app, los usuarios que la hayan instalado (incluyéndote a ti mismo) necesitarán aceptar los permisos nuevos antes de que los cambios surtan efecto. También puedes aceptar los nuevos permisos; para ello, ve a la página de instalaciones. Verás un vínculo bajo el nombre de la aplicación que te informará de que la aplicación está solicitando permisos diferentes. Haz clic en "Revisar solicitud" y, a continuación, haz clic en "Aceptar nuevos permisos".
Adición de código para clonar un repositorio
Para clonar un repositorio, el código usará los permisos de GitHub App y el SDK de Octokit para crear un token de instalación para la aplicación (x-access-token:TOKEN
) y lo usará en el siguiente comando clone:
git clone https://x-access-token:TOKEN@github.com/OWNER/REPO.git
El comando anterior clona un repositorio a través de HTTP. Éste necesita el nombre íntegro del repositorio, lo cual incluye al propietario del mismo (usuario u organización) y el nombre de éste. Por ejemplo, el nombre completo del repositorio Hello-World de octocat es octocat/hello-world
.
Abrir el archivo server.rb
. En el bloque de código que comienza por helpers do
, donde dice # ADD CLONE_REPOSITORY HELPER METHOD HERE #
, agrega el código siguiente:
# Clones the repository to the current working directory, updates the # contents using Git pull, and checks out the ref. # # full_repo_name - The owner and repo. Ex: octocat/hello-world # repository - The repository name # ref - The branch, commit SHA, or tag to check out def clone_repository(full_repo_name, repository, ref) @git = Git.clone("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", repository) pwd = Dir.getwd() Dir.chdir(repository) @git.pull @git.checkout(ref) Dir.chdir(pwd) end
# Clones the repository to the current working directory, updates the
# contents using Git pull, and checks out the ref.
#
# full_repo_name - The owner and repo. Ex: octocat/hello-world
# repository - The repository name
# ref - The branch, commit SHA, or tag to check out
def clone_repository(full_repo_name, repository, ref)
@git = Git.clone("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", repository)
pwd = Dir.getwd()
Dir.chdir(repository)
@git.pull
@git.checkout(ref)
Dir.chdir(pwd)
end
El código anterior usa la gema ruby-git
para clonar el repositorio mediante el token de instalación de la aplicación. Clona el código en el mismo directorio que server.rb
. Para ejecutar los comandos de Git en el repositorio, el código necesita cambiar el directorio del repositorio. Antes de cambiar los directorios, el código almacena el directorio de trabajo actual en una variable (pwd
) para recordar a dónde debe volver antes de salir del método clone_repository
.
Desde el directorio del repositorio, este código obtiene y combina los últimos cambios (@git.pull
), y extrae del repositorio la referencia Git específica (@git.checkout(ref)
). El código para hacer todo esto encaja perfectamente en su propio método. Para llevar a cabo estas operaciones, el método necesita el nombre y nombre completo del repositorio y la ref de salida. La ref puede ser el SHA de una confirmación, una rama, o una etiqueta. Cuando hayas terminado, el código vuelve a cambiar el directorio al directorio de trabajo original (pwd
).
Ahora tiene un método que clona un repositorio y extrae una referencia. A continuación, debe agregar código para obtener los parámetros de entrada necesarios y llamar al nuevo método clone_repository
.
En el bloque de código que comienza por helpers do
, en el método auxiliar initiate_check_run
, donde dice # ***** RUN A CI TEST *****
, agrega el código siguiente:
full_repo_name = @payload['repository']['full_name'] repository = @payload['repository']['name'] head_sha = @payload['check_run']['head_sha'] clone_repository(full_repo_name, repository, head_sha) # ADD CODE HERE TO RUN RUBOCOP #
full_repo_name = @payload['repository']['full_name']
repository = @payload['repository']['name']
head_sha = @payload['check_run']['head_sha']
clone_repository(full_repo_name, repository, head_sha)
# ADD CODE HERE TO RUN RUBOCOP #
El código anterior obtiene el nombre completo del repositorio y el SHA de encabezado de la confirmación desde la carga útil del webhook check_run
.
Paso 2.3. Ejecución de RuboCop
Hasta ahora, el código clona el repositorio y crea ejecuciones de comprobación mediante el servidor de CI. Ahora te centrarás en los detalles del linter RuboCop y de las anotaciones de las comprobaciones.
En primer lugar, agregarás código para ejecutar RuboCop y guardarás los errores de código de estilo en formato JSON.
En el bloque de código que comienza por helpers do
, busca el método auxiliar initiate_check_run
. Dentro de ese método auxiliar, en clone_repository(full_repo_name, repository, head_sha)
, donde dice # ADD CODE HERE TO RUN RUBOCOP #
, agrega el código siguiente:
# Run RuboCop on all files in the repository @report = `rubocop '#{repository}' --format json` logger.debug @report `rm -rf #{repository}` @output = JSON.parse @report # ADD ANNOTATIONS CODE HERE #
# Run RuboCop on all files in the repository
@report = `rubocop '#{repository}' --format json`
logger.debug @report
`rm -rf #{repository}`
@output = JSON.parse @report
# ADD ANNOTATIONS CODE HERE #
Este código utiliza RuboCop en todos los archivos dentro del directorio del repositorio. La opción --format json
guarda una copia de los resultados de linting en un formato analizable para una máquina. Para obtener más información y un ejemplo del formato JSON, consulta "Formateador JSON" en la documentación de RuboCop. Este código también analiza el código JSON para que puedas acceder fácilmente a las claves y los valores de GitHub App mediante la variable @output
.
Después de ejecutar RuboCop y guardar los resultados de linting, este código ejecuta el comando rm -rf
para eliminar la salida del repositorio. Dado que este código almacena los resultados de RuboCop en una variable @report
, puede eliminar la salida del repositorio con seguridad.
El comando rm -rf
no se puede deshacer. Para mantener la seguridad de la aplicación, el código de este tutorial comprueba los webhooks entrantes en busca de comandos malintencionados insertados que podrían usarse para quitar un directorio diferente del previsto por la aplicación. Por ejemplo, si un actor malintencionado envía un webhook con el nombre de repositorio ./
, la aplicación quitará el directorio raíz. El método verify_webhook_signature
valida el remitente del webhook. El controlador de eventos verify_webhook_signature
también comprueba que el nombre del repositorio es válido. Para obtener más información, consulta "Definición de un filtro before
".
Prueba del código
En los pasos siguientes se muestra cómo probar que el código funciona y ver los errores notificados por RuboCop.
-
Ejecuta el comando siguiente para reiniciar el servidor desde el terminal. Si el servidor ya se está ejecutando, escribe primero
Ctrl-C
en el terminal para detener el servidor y, a continuación, ejecuta el siguiente comando para iniciar el servidor de nuevo.Shell ruby server.rb
ruby server.rb
-
En el repositorio donde agregaste el archivo
myfile.rb
, crea una nueva solicitud de incorporación de cambios. -
En la pestaña del terminal donde se ejecuta el servidor, deberías ver la salida de depuración que contiene errores de linting. Los errores de linting se imprimen sin ningún formato. Puedes copiar y pegar la salida de depuración en una herramienta web como formateador JSON, para dar formato a la salida JSON como en el ejemplo siguiente:
{ "metadata": { "rubocop_version": "0.60.0", "ruby_engine": "ruby", "ruby_version": "2.3.7", "ruby_patchlevel": "456", "ruby_platform": "universal.x86_64-darwin18" }, "files": [ { "path": "Octocat-breeds/octocat.rb", "offenses": [ { "severity": "convention", "message": "Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.", "cop_name": "Style/StringLiterals", "corrected": false, "location": { "start_line": 17, "start_column": 17, "last_line": 17, "last_column": 22, "length": 6, "line": 17, "column": 17 } }, { "severity": "convention", "message": "Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.", "cop_name": "Style/StringLiterals", "corrected": false, "location": { "start_line": 17, "start_column": 25, "last_line": 17, "last_column": 29, "length": 5, "line": 17, "column": 25 } } ] } ], "summary": { "offense_count": 2, "target_file_count": 1, "inspected_file_count": 1 } }
Paso 2.4. Recopilación de errores de RuboCop
La variable @output
contiene los resultados JSON analizados del informe de RuboCop. Tal como se ha mostrado en el ejemplo de salida del paso anterior, los resultados contienen una sección summary
que el código puede usar para determinar rápidamente si hay algún error. El código siguiente establecerá el estado de la conclusión de la ejecución de comprobación en success
cuando no se notifiquen errores. RuboCop notifica los errores para cada archivo de la matriz files
, así que, si hay errores, necesitará extraer algunos datos del objeto de archivo.
Los puntos de conexión de la API REST para administrar las ejecuciones de comprobación permiten crear anotaciones para líneas de código específicas. Cuando creas o actualizas una ejecución de verificación, puedes agregar anotaciones. En este tutorial, actualizarás la ejecución de comprobación con anotaciones mediante el punto de conexión PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
. Para obtener más información sobre el punto de conexión, consulta "Puntos de conexión de la API de REST para ejecuciones de comprobación".
La API limita la cantidad de anotaciones a un máximo de 50 por solicitud. Para crear más de 50 anotaciones, deberás realizar varias solicitudes al punto de conexión "Actualización de una ejecución de comprobación". Por ejemplo, para crear 105 anotaciones, tendrías que realizar tres solicitudes independientes a la API. Las primeras dos contarían por 50 anotaciones cada una, y la tercera incluiría las cinco restantes. Cada vez que actualices la ejecución de verificación, se adjuntan las anotaciones a la lista de anotaciones existente para la ejecución de verificación.
Una ejecución de verificación espera encontrar las anotaciones en una matriz de objetos. Cada objeto de anotación debe incluir los parámetros path
, start_line
, end_line
, annotation_level
y message
. RuboCop también proporciona los parámetros adicionales start_column
y end_column
, por lo que puede incluirlos en la anotación. Las anotaciones solo admiten los parámetros start_column
y end_column
en la misma línea. Para obtener más información, consulta el objeto annotations
en "Puntos de conexión de la API de REST para ejecuciones de comprobación".
Ahora agregarás código para extraer la información necesaria de RuboCop necesaria para crear cada anotación.
En el código que agregaste en el paso anterior, donde dice # ADD ANNOTATIONS CODE HERE #
, agrega el código siguiente:
annotations = [] # You can create a maximum of 50 annotations per request to the Checks # API. To add more than 50 annotations, use the "Update a check run" API # endpoint. This example code limits the number of annotations to 50. # See /rest/reference/checks#update-a-check-run # for details. max_annotations = 50 # RuboCop reports the number of errors found in "offense_count" if @output['summary']['offense_count'] == 0 conclusion = 'success' else conclusion = 'neutral' @output['files'].each do |file| # Only parse offenses for files in this app's repository file_path = file['path'].gsub(/#{repository}\//,'') annotation_level = 'notice' # Parse each offense to get details and location file['offenses'].each do |offense| # Limit the number of annotations to 50 next if max_annotations == 0 max_annotations -= 1 start_line = offense['location']['start_line'] end_line = offense['location']['last_line'] start_column = offense['location']['start_column'] end_column = offense['location']['last_column'] message = offense['message'] # Create a new annotation for each error annotation = { path: file_path, start_line: start_line, end_line: end_line, start_column: start_column, end_column: end_column, annotation_level: annotation_level, message: message } # Annotations only support start and end columns on the same line if start_line == end_line annotation.merge({start_column: start_column, end_column: end_column}) end annotations.push(annotation) end end end # ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
annotations = []
# You can create a maximum of 50 annotations per request to the Checks
# API. To add more than 50 annotations, use the "Update a check run" API
# endpoint. This example code limits the number of annotations to 50.
# See /rest/reference/checks#update-a-check-run
# for details.
max_annotations = 50
# RuboCop reports the number of errors found in "offense_count"
if @output['summary']['offense_count'] == 0
conclusion = 'success'
else
conclusion = 'neutral'
@output['files'].each do |file|
# Only parse offenses for files in this app's repository
file_path = file['path'].gsub(/#{repository}\//,'')
annotation_level = 'notice'
# Parse each offense to get details and location
file['offenses'].each do |offense|
# Limit the number of annotations to 50
next if max_annotations == 0
max_annotations -= 1
start_line = offense['location']['start_line']
end_line = offense['location']['last_line']
start_column = offense['location']['start_column']
end_column = offense['location']['last_column']
message = offense['message']
# Create a new annotation for each error
annotation = {
path: file_path,
start_line: start_line,
end_line: end_line,
start_column: start_column,
end_column: end_column,
annotation_level: annotation_level,
message: message
}
# Annotations only support start and end columns on the same line
if start_line == end_line
annotation.merge({start_column: start_column, end_column: end_column})
end
annotations.push(annotation)
end
end
end
# ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
Este código limita la cantidad total de anotaciones a 50. Pero puedes modificarlo para actualizar la ejecución de verificación para cada lote de 50 anotaciones. El código anterior incluye la variable max_annotations
, la cual establece el límite en 50 y se usa en el bucle que recorre en iteración las infracciones.
Cuando offense_count
es cero, el resultado de la prueba de CI es success
. Si hay errores, este código establece la conclusión en neutral
para evitar que se apliquen estrictamente los errores de los linters de código. Pero puede cambiar la conclusión a failure
si quiere asegurarse de que se produce un error en el conjunto de comprobaciones cuando hay errores de linting.
Cuando se notifican errores, el código anterior recorre en iteración la matriz files
del informe de RuboCop. Para cada archivo, extrae la ruta de acceso del mismo y establece el nivel de anotación en notice
. Puedes ir aún más allá y establecer niveles de advertencia específicos para cada tipo de RuboCop Cop, pero para simplificar las cosas en este tutorial, todos los errores se establecen en un nivel notice
.
Este código también recorre en iteración cada error de la matriz offenses
y recopila la ubicación del mensaje de error y de la infracción. Después de extraer la información necesaria, el código crea una anotación para cada error y la almacena en la matriz annotations
. Dado que las anotaciones solo admiten columnas de inicio y finalización en la misma línea, los elementos start_column
y end_column
solo se agregan al objeto annotation
si los valores iniciales y finales de la línea son los mismos.
Este código aún no crea una anotación para la ejecución de verificación. Agregarás dicho código en la siguiente sección.
Paso 2.5. Actualización de la ejecución de comprobación con resultados de la prueba de CI
Cada ejecución de comprobación de GitHub contiene un objeto output
que incluye title
, summary
, text
, annotations
y images
. summary
y title
son los únicos parámetros necesarios para output
, pero por sí solos no dan mucha información, por lo que en este tutorial también se agregan text
y annotations
.
En el caso de summary
, en este ejemplo se usa la información de resumen de RuboCop y se agregan líneas nuevas (\n
) para dar formato a la salida. Puede personalizar la información que agrega al parámetro text
, pero en este ejemplo se incluye en el parámetro text
la versión de RuboCop. El siguiente código establece summary
y text
.
En el código que agregaste en el paso anterior, donde dice # ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
, agrega el código siguiente:
# Updated check run summary and text parameters summary = "Octo RuboCop summary\n-Offense count: #{@output['summary']['offense_count']}\n-File count: #{@output['summary']['target_file_count']}\n-Target file count: #{@output['summary']['inspected_file_count']}" text = "Octo RuboCop version: #{@output['metadata']['rubocop_version']}"
# Updated check run summary and text parameters
summary = "Octo RuboCop summary\n-Offense count: #{@output['summary']['offense_count']}\n-File count: #{@output['summary']['target_file_count']}\n-Target file count: #{@output['summary']['inspected_file_count']}"
text = "Octo RuboCop version: #{@output['metadata']['rubocop_version']}"
Ahora el código debe tener toda la información que necesita para actualizar la ejecución de la comprobación. En el paso 1.3. "Actualización de una ejecución de comprobación", has agregado código para establecer el estado de la ejecución de comprobación en success
. Deberá actualizar ese código para usar la variable conclusion
establecida en función de los resultados de RuboCop (en success
o neutral
). Este es el código que agregaste anteriormente al archivo server.rb
:
# Mark the check run as complete!
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: 'success',
accept: 'application/vnd.github+json'
)
Reemplaza esa línea por el código siguiente:
# Mark the check run as complete! And if there are warnings, share them. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'completed', conclusion: conclusion, output: { title: 'Octo RuboCop', summary: summary, text: text, annotations: annotations }, actions: [{ label: 'Fix this', description: 'Automatically fix all linter notices.', identifier: 'fix_rubocop_notices' }], accept: 'application/vnd.github+json' )
# Mark the check run as complete! And if there are warnings, share them.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: conclusion,
output: {
title: 'Octo RuboCop',
summary: summary,
text: text,
annotations: annotations
},
actions: [{
label: 'Fix this',
description: 'Automatically fix all linter notices.',
identifier: 'fix_rubocop_notices'
}],
accept: 'application/vnd.github+json'
)
Ahora que el código establece una conclusión con base en el estado de la prueba de IC y agrega la salida de los resultados de RuboCop, has creado una prueba de CI.
El código anterior también agrega una característica al servidor de CI denominada acciones solicitadas por medio del objeto actions
. Para más información, consulta "Solicitud de más acciones de una ejecución de comprobación". Las acciones solicitadas agregan un botón en la pestaña Comprobaciones de GitHub que permite a alguien solicitar la ejecución de comprobación para realizar acciones adicionales. Tu app puede configurar la acción adicional en su totalidd. Por ejemplo, ya que RuboCop tiene una característica para corregir automáticamente los errores que encuentre en el código de Ruby, tu servidor de IC puede utilizar un botón de acciones solicitadas para ayudar a que las personas soliciten correcciónes de errores automáticas. Cuando alguien hace clic en el botón, la aplicación recibe el evento check_run
con una acción requested_action
. Cada acción solicitada tiene un objeto identifier
que la aplicación usa para determinar en qué botón se ha hecho clic.
El código anterior aún no hace que RuboCop corrija los errores automáticamente. Lo agregarás más adelante en el tutorial.
Prueba del código
En los pasos siguientes se muestra cómo probar que el código funciona y ver la prueba de CI que acabas de crear.
-
Ejecuta el comando siguiente para reiniciar el servidor desde el terminal. Si el servidor ya se está ejecutando, escribe primero
Ctrl-C
en el terminal para detener el servidor y, a continuación, ejecuta el siguiente comando para iniciar el servidor de nuevo.Shell ruby server.rb
ruby server.rb
-
En el repositorio donde agregaste el archivo
myfile.rb
, crea una nueva solicitud de incorporación de cambios. -
En la solicitud de incorporación de cambios que acabas de crear, ve a la pestaña Comprobaciones. Deberías ver anotaciones para cada uno de los errores encontrados por RuboCop. Observa el botón "Corregir esto" que creaste al agregar la acción solicitada.
Paso 2.6. Corrección automática de los errores de RuboCop
Hasta ahora has creado una prueba de CI. En esta sección vas a agregar una característica más que utiliza a RuboCop para corregir automáticamente los errores que encuentre. Ya has agregado el botón "Corregir esto" en el paso 2.5. "Actualización de la ejecución de comprobación con los resultados de la prueba de CI". Ahora agregarás el código para controlar el evento de ejecución de comprobación requested_action
que se desencadena cuando alguien hace clic en el botón "Corregir esto".
La herramienta RuboCop ofrece la opción de línea de comandos --auto-correct
para corregir automáticamente los errores que encuentra. Para obtener más información, consulta "Corrección automática de infracciones" en la documentación de RuboCop. Cuando se usa la característica --auto-correct
, las actualizaciones se aplican en los archivos locales del servidor. Deberás insertar los cambios en GitHub después de que RuboCop realice las correcciones.
Para insertar un repositorio, la aplicación debe tener permisos de escritura para "Contenido" en un repositorio. Ya has establecido ese permiso en Lectura y escritura en el "paso 2.2. Permitir que RuboCop clone el repositorio".
Para confirmar archivos, Git debe saber qué nombre de usuario y dirección de correo electrónico se asociará a la confirmación. A continuación, agregarás variables de entorno para almacenar el nombre y la dirección de correo electrónico que usará la aplicación cuando realice confirmaciones de Git.
-
Abre el archivo
.env
que has creado antes en este tutorial. -
Agrega las siguientes variables de entorno al archivo
.env
. ReemplazaAPP_NAME
por el nombre de la aplicación yEMAIL_ADDRESS
por cualquier correo electrónico que quiera usar para este ejemplo.Shell GITHUB_APP_USER_NAME="APP_NAME" GITHUB_APP_USER_EMAIL="EMAIL_ADDRESS"
GITHUB_APP_USER_NAME="APP_NAME" GITHUB_APP_USER_EMAIL="EMAIL_ADDRESS"
A continuación, deberás agregar código para leer las variables de entorno y establecer la configuración de Git. Pronto agregarás este código.
Cuando alguien hace clic en el botón "Fix this" (Corregir esto), la aplicación recibe el webhook de ejecución de comprobación con el tipo de acción requested_action
.
En el paso 1.3. "Actualización de una ejecución de comprobación", has actualizado event_handler
en el archivo server.rb
para que busque acciones en el evento check_run
. Ya tiene una instrucción "case" para controlar los tipos de acción created
y rerequested
:
when 'check_run'
# Check that the event is being sent to this app
if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
case @payload['action']
when 'created'
initiate_check_run
when 'rerequested'
create_check_run
# ADD REQUESTED_ACTION METHOD HERE #
end
end
Después del caso rerequested
, donde dice # ADD REQUESTED_ACTION METHOD HERE #
, agrega el código siguiente:
when 'requested_action' take_requested_action
when 'requested_action'
take_requested_action
Este código llama a un nuevo método que controlará todos los eventos requested_action
de la aplicación.
En el bloque de código que comienza por helpers do
, donde dice # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
, agrega el siguiente método auxiliar:
# Handles the check run `requested_action` event # See /webhooks/event-payloads/#check_run def take_requested_action full_repo_name = @payload['repository']['full_name'] repository = @payload['repository']['name'] head_branch = @payload['check_run']['check_suite']['head_branch'] if (@payload['requested_action']['identifier'] == 'fix_rubocop_notices') clone_repository(full_repo_name, repository, head_branch) # Sets your commit username and email address @git.config('user.name', ENV['GITHUB_APP_USER_NAME']) @git.config('user.email', ENV['GITHUB_APP_USER_EMAIL']) # Automatically correct RuboCop style errors @report = `rubocop '#{repository}/*' --format json --auto-correct` pwd = Dir.getwd() Dir.chdir(repository) begin @git.commit_all('Automatically fix Octo RuboCop notices.') @git.push("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", head_branch) rescue # Nothing to commit! puts 'Nothing to commit' end Dir.chdir(pwd) `rm -rf '#{repository}'` end end
# Handles the check run `requested_action` event
# See /webhooks/event-payloads/#check_run
def take_requested_action
full_repo_name = @payload['repository']['full_name']
repository = @payload['repository']['name']
head_branch = @payload['check_run']['check_suite']['head_branch']
if (@payload['requested_action']['identifier'] == 'fix_rubocop_notices')
clone_repository(full_repo_name, repository, head_branch)
# Sets your commit username and email address
@git.config('user.name', ENV['GITHUB_APP_USER_NAME'])
@git.config('user.email', ENV['GITHUB_APP_USER_EMAIL'])
# Automatically correct RuboCop style errors
@report = `rubocop '#{repository}/*' --format json --auto-correct`
pwd = Dir.getwd()
Dir.chdir(repository)
begin
@git.commit_all('Automatically fix Octo RuboCop notices.')
@git.push("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", head_branch)
rescue
# Nothing to commit!
puts 'Nothing to commit'
end
Dir.chdir(pwd)
`rm -rf '#{repository}'`
end
end
El código anterior clona un repositorio, igual que el código que has agregado en el paso 2.2. "Permitir que RuboCop clone el repositorio". Una instrucción if
comprueba que el identificador de la acción solicitada coincide con el identificador del botón de RuboCop (fix_rubocop_notices
). Cuando coinciden, el código clona el repositorio, establece el nombre de usuario y el correo electrónico de Git y ejecuta RuboCop con la opción --auto-correct
. La opción --auto-correct
aplica los cambios a los archivos locales del servidor de CI automáticamente.
Los archivos se cambian de manera local, pero aún necesitarás cargarlos a GitHub. Usarás la gema ruby-git
para confirmar todos los archivos. Git tiene un único comando que almacena provisionalmente todos los archivos modificados o eliminados y los confirma: git commit -a
. Para hacer lo mismo con ruby-git
, el código anterior usa el método commit_all
. Después, el código inserta los archivos confirmados en GitHub mediante el token de instalación con el mismo método de autenticación que el comando clone
de Git. Por último, elimina el directorio del repositorio para garantizar que el directorio de trabajo está preparado para el siguiente evento.
El código que has escrito ahora completa el servidor de integración continua que creaste con GitHub App y comprobaciones. Para ver el código final completo de la aplicación, consulta "Ejemplo de código completo".
Prueba del código
Los pasos siguientes te mostrarán cómo probar que el código funciona y que RuboCop puede corregir automáticamente los errores que encuentra.
-
Ejecuta el comando siguiente para reiniciar el servidor desde el terminal. Si el servidor ya se está ejecutando, escribe primero
Ctrl-C
en el terminal para detener el servidor y, a continuación, ejecuta el siguiente comando para iniciar el servidor de nuevo.Shell ruby server.rb
ruby server.rb
-
En el repositorio donde agregaste el archivo
myfile.rb
, crea una nueva solicitud de incorporación de cambios. -
En la nueva solicitud de incorporación de cambios que has creado, ve a la pestaña Comprobaciones y haz clic en el botón "Corregir esto" para corregir automáticamente los errores que RuboCop encontró.
-
Ve a la pestaña Confirmaciones. Deberías ver una nueva confirmación con el nombre de usuario que has establecido en la configuración de Git. Puede que necesites actualizar tu buscador para ver esto.
-
Ve a la pestaña Comprobaciones. Deberías ver un nuevo conjunto de comprobaciones para Octo RuboCop. Pero esta vez no debería haber errores, porque RuboCop los corrigió todos.
Ejemplo de código completo
Este es el aspecto que debe tener el código final en server.rb
, después de haber seguido todos los pasos de este tutorial. También hay comentarios en todo el código que proporcionan contexto adicional.
require 'sinatra/base' # Use the Sinatra web framework require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API require 'dotenv/load' # Manages environment variables require 'json' # Allows your app to manipulate JSON data require 'openssl' # Verifies the webhook signature require 'jwt' # Authenticates a GitHub App require 'time' # Gets ISO 8601 representation of a Time object require 'logger' # Logs debug statements # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. class GHAapp < Sinatra::Application # Sets the port that's used when starting the web server. set :port, 3000 set :bind, '0.0.0.0' # Expects the private key in PEM format. Converts the newlines. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a webhook secret. # The secret is used to verify that webhooks are sent by GitHub. WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] # The GitHub App's identifier (type integer). APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER'] # Turn on Sinatra's verbose logging during development configure :development do set :logging, Logger::DEBUG end # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) verify_webhook_signature # If a repository name is provided in the webhook, validate that # it consists only of latin alphabetic characters, `-`, and `_`. unless @payload['repository'].nil? halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil? end authenticate_app # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do # Get the event type from the HTTP_X_GITHUB_EVENT header case request.env['HTTP_X_GITHUB_EVENT'] when 'check_suite' # A new check_suite has been created. Create a new check run with status queued if @payload['action'] == 'requested' || @payload['action'] == 'rerequested' create_check_run end when 'check_run' # Check that the event is being sent to this app if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER case @payload['action'] when 'created' initiate_check_run when 'rerequested' create_check_run when 'requested_action' take_requested_action end end end 200 # success status end helpers do # Create a new check run with status "queued" def create_check_run @installation_client.create_check_run( # [String, Integer, Hash, Octokit Repository object] A GitHub repository. @payload['repository']['full_name'], # [String] The name of your check run. 'Octo RuboCop', # [String] The SHA of the commit to check # The payload structure differs depending on whether a check run or a check suite event occurred. @payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'], # [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use. accept: 'application/vnd.github+json' ) end # Start the CI process def initiate_check_run # Once the check run is created, you'll update the status of the check run # to 'in_progress' and run the CI process. When the CI finishes, you'll # update the check run status to 'completed' and add the CI results. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'in_progress', accept: 'application/vnd.github+json' ) full_repo_name = @payload['repository']['full_name'] repository = @payload['repository']['name'] head_sha = @payload['check_run']['head_sha'] clone_repository(full_repo_name, repository, head_sha) # Run RuboCop on all files in the repository @report = `rubocop '#{repository}' --format json` logger.debug @report `rm -rf #{repository}` @output = JSON.parse @report annotations = [] # You can create a maximum of 50 annotations per request to the Checks # API. To add more than 50 annotations, use the "Update a check run" API # endpoint. This example code limits the number of annotations to 50. # See /rest/reference/checks#update-a-check-run # for details. max_annotations = 50 # RuboCop reports the number of errors found in "offense_count" if @output['summary']['offense_count'] == 0 conclusion = 'success' else conclusion = 'neutral' @output['files'].each do |file| # Only parse offenses for files in this app's repository file_path = file['path'].gsub(/#{repository}\//,'') annotation_level = 'notice' # Parse each offense to get details and location file['offenses'].each do |offense| # Limit the number of annotations to 50 next if max_annotations == 0 max_annotations -= 1 start_line = offense['location']['start_line'] end_line = offense['location']['last_line'] start_column = offense['location']['start_column'] end_column = offense['location']['last_column'] message = offense['message'] # Create a new annotation for each error annotation = { path: file_path, start_line: start_line, end_line: end_line, start_column: start_column, end_column: end_column, annotation_level: annotation_level, message: message } # Annotations only support start and end columns on the same line if start_line == end_line annotation.merge({start_column: start_column, end_column: end_column}) end annotations.push(annotation) end end end # Updated check run summary and text parameters summary = "Octo RuboCop summary\n-Offense count: #{@output['summary']['offense_count']}\n-File count: #{@output['summary']['target_file_count']}\n-Target file count: #{@output['summary']['inspected_file_count']}" text = "Octo RuboCop version: #{@output['metadata']['rubocop_version']}" # Mark the check run as complete! And if there are warnings, share them. @installation_client.update_check_run( @payload['repository']['full_name'], @payload['check_run']['id'], status: 'completed', conclusion: conclusion, output: { title: 'Octo RuboCop', summary: summary, text: text, annotations: annotations }, actions: [{ label: 'Fix this', description: 'Automatically fix all linter notices.', identifier: 'fix_rubocop_notices' }], accept: 'application/vnd.github+json' ) end # Clones the repository to the current working directory, updates the # contents using Git pull, and checks out the ref. # # full_repo_name - The owner and repo. Ex: octocat/hello-world # repository - The repository name # ref - The branch, commit SHA, or tag to check out def clone_repository(full_repo_name, repository, ref) @git = Git.clone("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", repository) pwd = Dir.getwd() Dir.chdir(repository) @git.pull @git.checkout(ref) Dir.chdir(pwd) end # Handles the check run `requested_action` event # See /webhooks/event-payloads/#check_run def take_requested_action full_repo_name = @payload['repository']['full_name'] repository = @payload['repository']['name'] head_branch = @payload['check_run']['check_suite']['head_branch'] if (@payload['requested_action']['identifier'] == 'fix_rubocop_notices') clone_repository(full_repo_name, repository, head_branch) # Sets your commit username and email address @git.config('user.name', ENV['GITHUB_APP_USER_NAME']) @git.config('user.email', ENV['GITHUB_APP_USER_EMAIL']) # Automatically correct RuboCop style errors @report = `rubocop '#{repository}/*' --format json --auto-correct` pwd = Dir.getwd() Dir.chdir(repository) begin @git.commit_all('Automatically fix Octo RuboCop notices.') @git.push("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", head_branch) rescue # Nothing to commit! puts 'Nothing to commit' end Dir.chdir(pwd) `rm -rf '#{repository}'` end end # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) # request.body is an IO or StringIO object # Rewind in case someone already read it request.body.rewind # The raw text of the body is required for webhook signature verification @payload_raw = request.body.read begin @payload = JSON.parse @payload_raw rescue => e fail 'Invalid JSON (#{e}): #{@payload_raw}' end end # Instantiate an Octokit client authenticated as a GitHub App. # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, # so GitHub can be sure that it came from the app and not altered by # a malicious third party. def authenticate_app payload = { # The time that this JWT was issued, _i.e._ now. iat: Time.now.to_i, # JWT expiration time (10 minute maximum) exp: Time.now.to_i + (10 * 60), # Your GitHub App's identifier number iss: APP_IDENTIFIER } # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end # Instantiate an Octokit client, authenticated as an installation of a # GitHub App, to run API operations. def authenticate_installation(payload) @installation_id = payload['installation']['id'] @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to # create the hash signature sent in the `X-HUB-Signature` header of each # webhook. This code computes the expected hash signature and compares it to # the signature sent in the `X-HUB-Signature` header. If they don't match, # this request is an attack, and you should reject it. GitHub uses the HMAC # hexdigest to compute the signature. The `X-HUB-Signature` looks something # like this: 'sha1=123456'. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw) halt 401 unless their_digest == our_digest # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end end # Finally some logic to let us run this server directly from the command line, # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file # If they are the same—that is, we are running this file directly, call the # Sinatra run method run! if __FILE__ == $0 end
require 'sinatra/base' # Use the Sinatra web framework
require 'octokit' # Use the Octokit Ruby library to interact with GitHub's REST API
require 'dotenv/load' # Manages environment variables
require 'json' # Allows your app to manipulate JSON data
require 'openssl' # Verifies the webhook signature
require 'jwt' # Authenticates a GitHub App
require 'time' # Gets ISO 8601 representation of a Time object
require 'logger' # Logs debug statements
# This code is a Sinatra app, for two reasons:
# 1. Because the app will require a landing page for installation.
# 2. To easily handle webhook events.
class GHAapp < Sinatra::Application
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
# Expects the private key in PEM format. Converts the newlines.
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n"))
# Your registered app must have a webhook secret.
# The secret is used to verify that webhooks are sent by GitHub.
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
# The GitHub App's identifier (type integer).
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
# Executed before each request to the `/event_handler` route
before '/event_handler' do
get_payload_request(request)
verify_webhook_signature
# If a repository name is provided in the webhook, validate that
# it consists only of latin alphabetic characters, `-`, and `_`.
unless @payload['repository'].nil?
halt 400 if (@payload['repository']['name'] =~ /[0-9A-Za-z\-\_]+/).nil?
end
authenticate_app
# Authenticate the app installation in order to run API operations
authenticate_installation(@payload)
end
post '/event_handler' do
# Get the event type from the HTTP_X_GITHUB_EVENT header
case request.env['HTTP_X_GITHUB_EVENT']
when 'check_suite'
# A new check_suite has been created. Create a new check run with status queued
if @payload['action'] == 'requested' || @payload['action'] == 'rerequested'
create_check_run
end
when 'check_run'
# Check that the event is being sent to this app
if @payload['check_run']['app']['id'].to_s === APP_IDENTIFIER
case @payload['action']
when 'created'
initiate_check_run
when 'rerequested'
create_check_run
when 'requested_action'
take_requested_action
end
end
end
200 # success status
end
helpers do
# Create a new check run with status "queued"
def create_check_run
@installation_client.create_check_run(
# [String, Integer, Hash, Octokit Repository object] A GitHub repository.
@payload['repository']['full_name'],
# [String] The name of your check run.
'Octo RuboCop',
# [String] The SHA of the commit to check
# The payload structure differs depending on whether a check run or a check suite event occurred.
@payload['check_run'].nil? ? @payload['check_suite']['head_sha'] : @payload['check_run']['head_sha'],
# [Hash] 'Accept' header option, to avoid a warning about the API not being ready for production use.
accept: 'application/vnd.github+json'
)
end
# Start the CI process
def initiate_check_run
# Once the check run is created, you'll update the status of the check run
# to 'in_progress' and run the CI process. When the CI finishes, you'll
# update the check run status to 'completed' and add the CI results.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'in_progress',
accept: 'application/vnd.github+json'
)
full_repo_name = @payload['repository']['full_name']
repository = @payload['repository']['name']
head_sha = @payload['check_run']['head_sha']
clone_repository(full_repo_name, repository, head_sha)
# Run RuboCop on all files in the repository
@report = `rubocop '#{repository}' --format json`
logger.debug @report
`rm -rf #{repository}`
@output = JSON.parse @report
annotations = []
# You can create a maximum of 50 annotations per request to the Checks
# API. To add more than 50 annotations, use the "Update a check run" API
# endpoint. This example code limits the number of annotations to 50.
# See /rest/reference/checks#update-a-check-run
# for details.
max_annotations = 50
# RuboCop reports the number of errors found in "offense_count"
if @output['summary']['offense_count'] == 0
conclusion = 'success'
else
conclusion = 'neutral'
@output['files'].each do |file|
# Only parse offenses for files in this app's repository
file_path = file['path'].gsub(/#{repository}\//,'')
annotation_level = 'notice'
# Parse each offense to get details and location
file['offenses'].each do |offense|
# Limit the number of annotations to 50
next if max_annotations == 0
max_annotations -= 1
start_line = offense['location']['start_line']
end_line = offense['location']['last_line']
start_column = offense['location']['start_column']
end_column = offense['location']['last_column']
message = offense['message']
# Create a new annotation for each error
annotation = {
path: file_path,
start_line: start_line,
end_line: end_line,
start_column: start_column,
end_column: end_column,
annotation_level: annotation_level,
message: message
}
# Annotations only support start and end columns on the same line
if start_line == end_line
annotation.merge({start_column: start_column, end_column: end_column})
end
annotations.push(annotation)
end
end
end
# Updated check run summary and text parameters
summary = "Octo RuboCop summary\n-Offense count: #{@output['summary']['offense_count']}\n-File count: #{@output['summary']['target_file_count']}\n-Target file count: #{@output['summary']['inspected_file_count']}"
text = "Octo RuboCop version: #{@output['metadata']['rubocop_version']}"
# Mark the check run as complete! And if there are warnings, share them.
@installation_client.update_check_run(
@payload['repository']['full_name'],
@payload['check_run']['id'],
status: 'completed',
conclusion: conclusion,
output: {
title: 'Octo RuboCop',
summary: summary,
text: text,
annotations: annotations
},
actions: [{
label: 'Fix this',
description: 'Automatically fix all linter notices.',
identifier: 'fix_rubocop_notices'
}],
accept: 'application/vnd.github+json'
)
end
# Clones the repository to the current working directory, updates the
# contents using Git pull, and checks out the ref.
#
# full_repo_name - The owner and repo. Ex: octocat/hello-world
# repository - The repository name
# ref - The branch, commit SHA, or tag to check out
def clone_repository(full_repo_name, repository, ref)
@git = Git.clone("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", repository)
pwd = Dir.getwd()
Dir.chdir(repository)
@git.pull
@git.checkout(ref)
Dir.chdir(pwd)
end
# Handles the check run `requested_action` event
# See /webhooks/event-payloads/#check_run
def take_requested_action
full_repo_name = @payload['repository']['full_name']
repository = @payload['repository']['name']
head_branch = @payload['check_run']['check_suite']['head_branch']
if (@payload['requested_action']['identifier'] == 'fix_rubocop_notices')
clone_repository(full_repo_name, repository, head_branch)
# Sets your commit username and email address
@git.config('user.name', ENV['GITHUB_APP_USER_NAME'])
@git.config('user.email', ENV['GITHUB_APP_USER_EMAIL'])
# Automatically correct RuboCop style errors
@report = `rubocop '#{repository}/*' --format json --auto-correct`
pwd = Dir.getwd()
Dir.chdir(repository)
begin
@git.commit_all('Automatically fix Octo RuboCop notices.')
@git.push("https://x-access-token:#{@installation_token.to_s}@github.com/#{full_repo_name}.git", head_branch)
rescue
# Nothing to commit!
puts 'Nothing to commit'
end
Dir.chdir(pwd)
`rm -rf '#{repository}'`
end
end
# Saves the raw payload and converts the payload to JSON format
def get_payload_request(request)
# request.body is an IO or StringIO object
# Rewind in case someone already read it
request.body.rewind
# The raw text of the body is required for webhook signature verification
@payload_raw = request.body.read
begin
@payload = JSON.parse @payload_raw
rescue => e
fail 'Invalid JSON (#{e}): #{@payload_raw}'
end
end
# Instantiate an Octokit client authenticated as a GitHub App.
# GitHub App authentication requires that you construct a
# JWT (https://jwt.io/introduction/) signed with the app's private key,
# so GitHub can be sure that it came from the app and not altered by
# a malicious third party.
def authenticate_app
payload = {
# The time that this JWT was issued, _i.e._ now.
iat: Time.now.to_i,
# JWT expiration time (10 minute maximum)
exp: Time.now.to_i + (10 * 60),
# Your GitHub App's identifier number
iss: APP_IDENTIFIER
}
# Cryptographically sign the JWT.
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
# Create the Octokit client, using the JWT as the auth token.
@app_client ||= Octokit::Client.new(bearer_token: jwt)
end
# Instantiate an Octokit client, authenticated as an installation of a
# GitHub App, to run API operations.
def authenticate_installation(payload)
@installation_id = payload['installation']['id']
@installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token]
@installation_client = Octokit::Client.new(bearer_token: @installation_token)
end
# Check X-Hub-Signature to confirm that this webhook was generated by
# GitHub, and not a malicious third party.
#
# GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to
# create the hash signature sent in the `X-HUB-Signature` header of each
# webhook. This code computes the expected hash signature and compares it to
# the signature sent in the `X-HUB-Signature` header. If they don't match,
# this request is an attack, and you should reject it. GitHub uses the HMAC
# hexdigest to compute the signature. The `X-HUB-Signature` looks something
# like this: 'sha1=123456'.
def verify_webhook_signature
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
method, their_digest = their_signature_header.split('=')
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, @payload_raw)
halt 401 unless their_digest == our_digest
# The X-GITHUB-EVENT header provides the name of the event.
# The action value indicates the which action triggered the event.
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil?
end
end
# Finally some logic to let us run this server directly from the command line,
# or with Rack. Don't worry too much about this code. But, for the curious:
# $0 is the executed file
# __FILE__ is the current file
# If they are the same—that is, we are running this file directly, call the
# Sinatra run method
run! if __FILE__ == $0
end
Pasos siguientes
Ahora deberías tener una aplicación que recibe eventos de API, crea ejecuciones de comprobación, usa RuboCop para buscar errores de Ruby, crea anotaciones en una solicitud de incorporación de cambios y corrige automáticamente errores de linter. A continuación, es posible que quieras expandir el código de la aplicación, implementarla y hacer que sea pública.
Si tiene alguna pregunta, inicie una discusión de GitHub Community en la categoría API y Webhooks.
Modificación de la aplicación
En este tutorial se ha mostrado cómo crear un botón "Corregir esto" que siempre se muestra en las solicitudes de incorporación de cambios del repositorio. Prueba a actualizar el código para que muestre el botón "Corregir esto" únicamente cuando RuboCop encuentre errores.
Si prefieres que RuboCop no confirme archivos directamente en la rama principal, actualiza el código para que, en su lugar, cree una solicitud de incorporación de cambios con una nueva rama basada en la rama principal.
Implementación de la aplicación
En este tutorial se muestra cómo desarrollar la aplicación localmente. Cuando estés listo para implementar la aplicación, debes realizar cambios para atenderla y mantener la credencial de la aplicación segura. Los pasos que realices dependerán del servidor que uses, pero las secciones siguientes ofrecen instrucciones generales.
Hospedaje de la aplicación en un servidor
En este tutorial se usó el equipo o codespace como servidor. Una vez que la aplicación esté lista para su uso en producción, debes implementarla en un servidor dedicado. Por ejemplo, puedes usar Azure App Service.
Actualización de la dirección URL del webhook
Una vez que tengas un servidor configurado para recibir tráfico de webhook de GitHub, actualiza la dirección URL del webhook en la configuración de la aplicación. No debes usar Smee.io para reenviar los webhooks en producción.
Actualización del valor :port
Cuando implementes la aplicación, querrás cambiar el puerto donde escucha el servidor. El código ya indica al servidor que escuche todas las interfaces de red disponibles estableciendo :bind
en 0.0.0.0
.
Por ejemplo, puedes establecer una variable PORT
en el archivo .env
en el servidor para indicar el puerto donde debe escuchar el servidor. A continuación, puedes actualizar el lugar donde el código establece :port
para que el servidor escuche en el puerto de implementación:
set :port, ENV['PORT']
set :port, ENV['PORT']
Protección de las credenciales de la aplicación
Nunca debes publicar la clave privada ni el secreto de webhook de la aplicación. En este tutorial se almacenan las credenciales de la aplicación en un archivo gitignored .env
. Cuando implementes la aplicación, debes elegir una manera segura de almacenar las credenciales y actualizar el código para obtener el valor en consecuencia. Por ejemplo, puedes almacenar las credenciales con un servicio de administración de secretos, como Azure Key Vault. Cuando se ejecuta la aplicación, puedes recuperar las credenciales y almacenarlas en variables de entorno en el servidor donde se implementa la aplicación.
Para obtener más información, vea «Procedimientos recomendados para crear una aplicación de GitHub».
Compartición de la aplicación
Si quieres compartir la aplicación con otros usuarios y organizaciones, haz que la aplicación sea pública. Para obtener más información, vea «Hacer pública o privada a una GitHub App».
Seguimiento de los procedimientos recomendados
Debes intentar seguir los procedimientos recomendados con tu instancia de GitHub App. Para obtener más información, vea «Procedimientos recomendados para crear una aplicación de GitHub».