Introduction
Ce tutoriel montre comment générer un serveur d’intégration continue (CI) qui exécute des tests sur un nouveau code poussé (push) vers un dépôt. Le tutoriel montre comment générer et configurer une GitHub App afin qu’elle fasse office de serveur qui reçoit les événements de webhook check_run
et check_suite
et y répond avec l’API REST de GitHub.
Dans ce tutoriel, vous allez utiliser votre ordinateur ou codespace comme serveur pendant que vous développez votre application. Une fois que l’application est prête pour être utilisée en production, vous devez déployer votre application sur un serveur dédié.
Ce tutoriel utilise Ruby, mais vous pouvez utiliser n’importe quel langage de programmation que vous pouvez exécuter sur votre serveur.
Ce tutoriel est divisé en deux parties :
- Dans la première partie, vous allez apprendre à configurer le framework d’un serveur CI en utilisant l’API REST de GitHub, à créer des exécutions de vérification pour les tests de CI quand un dépôt reçoit des commits nouvellement poussés et à réexécuter les exécutions de vérification quand un utilisateur demande cette action sur GitHub.
- Dans la deuxième partie, vous allez ajouter des fonctionnalités à votre test de CI, en ajoutant un test de linter à votre serveur CI. Vous allez également créer des annotations qui s’affichent sous l’onglet Vérifications et Fichiers modifiés d’une demande de tirage (pull request) et corriger automatiquement les recommandations de linter en exposant un bouton « Corriger cela » sous l’onglet Vérifications de la demande de tirage.
À propos de l’intégration continue (CI)
L’intégration continue (CI) est une pratique logicielle qui nécessite un validation fréquente de code dans un dépôt partagé. Le fait de valider le code plus souvent permet de détecter les erreurs plus tôt, et réduit la quantité de code dont un développeur a besoin pour le débogage quand il trouve la source d’une erreur. Les mises à jour fréquentes du code facilitent également la fusion des modifications apportées par différents membres d’une équipe de développement logiciel. Ceci est idéal pour les développeurs, qui peuvent alors passer plus de temps à écrire du code et moins de temps à déboguer des erreurs ou à résoudre les conflits de fusion.
Un serveur de CI héberge du code qui exécute des tests de CI, comme des linters de code (qui vérifient la mise en forme du style), des vérifications de sécurité, la couverture du code et d’autres vérifications sur de nouvelles validations de code dans un dépôt. Des serveurs de CI peuvent même générer et déployer du code sur des serveurs intermédiaires ou de production. Pour obtenir des exemples des types de tests de CI que vous pouvez créer avec une GitHub App, consultez les applications d’intégration continue disponibles dans GitHub Marketplace.
À propos des vérifications
L’API REST de GitHub vous permet de configurer des tests de CI (vérifications) qui s’exécutent automatiquement sur chaque validation de code dans un dépôt. L’API fournit des informations détaillées sur chaque vérification sous l’onglet Vérifications de la demande de tirage sur GitHub. Vous pouvez utiliser des vérifications dans un dépôt pour déterminer quand un commit de code introduit des erreurs.
Les vérifications incluent les exécutions de vérification, les suites de vérifications et les états de commit.
- Une exécution de vérification est un test de CI individuel qui s’exécute sur un commit.
- Une suite de vérifications est un groupe d’exécutions de vérification.
- Un état de commit marque l’état d’un commit, par exemple
error
,failure
,pending
ousuccess
, et est visible dans une demande de tirage sur GitHub. Les suites de vérifications et les exécutions de vérification contiennent des états de commit.
GitHub crée automatiquement des événements check_suite
pour les nouveaux commits de code dans un dépôt en utilisant le flux par défaut, même si vous pouvez changer les paramètres par défaut. Pour plus d’informations, consultez « Points de terminaison d’API REST pour les suites de vérifications ». Voici comment fonctionne le flux par défaut :
- Quand quelqu’un pousse du code vers le dépôt, GitHub envoie automatiquement l’événement
check_suite
avec une action derequested
à toutes les GitHub Apps installées sur le dépôt qui disposent de l’autorisationchecks:write
. Cet événement permet aux applications de savoir que du code a été poussé vers le dépôt et que GitHub a créé automatiquement une suite de vérifications. - Lorsque votre application reçoit cet événement, elle peut ajouter des exécutions de vérification à cette suite.
- Vos exécutions de vérification peuvent inclure des annotations qui sont affichées sur des lignes de code spécifiques. Des annotations sont visibles sous l’onglet Vérifications. Quand vous créez une annotation pour un fichier qui fait partie de la demande de tirage, les annotations sont également affichées sous l’onglet Fichiers modifiés. Pour plus d’informations, consultez l’objet
annotations
dans « Points de terminaison d’API REST pour les exécutions de vérifications. »
Pour plus d’informations sur les vérifications, consultez « Points de terminaison d’API REST pour les vérifications » et « Utilisation de l’API REST pour interagir avec des vérifications ».
Prérequis
Ce tutoriel part du principe que vous avez une compréhension de base du langage de programmation Ruby.
Avant de commencer, vous pouvez vous familiariser avec les concepts suivants :
Les vérifications peuvent également être utilisées avec l’API GraphQL, mais ce tutoriel se concentre sur l’API REST. Pour plus d’informations sur les objets GraphQL, consultez Suite de vérifications et Exécution de vérification dans la documentation GraphQL.
Programme d’installation
Les sections suivantes vous guident tout au long de la configuration des composants suivants :
- Un dépôt pour stocker le code de votre application.
- Un moyen de recevoir des webhooks localement.
- Une GitHub App qui est abonnée aux événements de webhook « Suite de vérifications » et « Exécution de vérification », a l’autorisation d’écriture pour les vérifications et utilise une URL de webhook que vous pouvez recevoir localement.
Créer un dépôt pour stocker le code de votre GitHub App
-
Créez un référentiel pour stocker le code de votre application. Pour plus d’informations, consultez « Création d’un dépôt ».
-
Clonez votre référentiel à partir de l’étape précédente. Pour plus d’informations, consultez « Clonage d’un dépôt ». Vous pouvez utiliser un clone local ou GitHub Codespaces.
-
Dans un terminal, accédez au répertoire dans lequel votre clone est stocké.
-
Créez un fichier Ruby nommé
server.rb
. Ce fichier contient tout le code de votre application. Vous ajouterez du contenu à ce fichier ultérieurement. -
Si le répertoire n’inclut pas encore de fichier
.gitignore
, ajoutez un fichier.gitignore
. Vous ajouterez du contenu à ce fichier ultérieurement. Pour plus d’informations sur les fichiers.gitignore
, consultez « Ignorer des fichiers ». -
Créez un fichier appelé
Gemfile
. Ce fichier décrit les dépendances de gemme dont votre code Ruby a besoin. Ajoutez le contenu suivant à votreGemfile
: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'
-
Créez un fichier appelé
config.ru
. Ce fichier configure l’exécution de votre serveur Sinatra. Ajoutez le contenu suivant à votre fichierconfig.ru
:Ruby require './server' run GHAapp
require './server' run GHAapp
Obtenir une URL de proxy webhook
Pour développer votre application localement, vous pouvez utiliser une URL de proxy webhook pour transférer des événements de webhook à partir de GitHub vers votre ordinateur ou codespace. Ce tutoriel utilise Smee.io pour fournir une URL de proxy webhook et transférer des événements.
-
Dans un terminal, exécutez la commande suivante pour installer le client Smee :
Shell npm install --global smee-client
npm install --global smee-client
-
Dans votre navigateur, accédez à https://smee.io/.
-
Cliquez sur Démarrer un nouveau canal.
-
Copiez l’URL complète sous « URL du proxy webhook ».
-
Dans le terminal, exécutez la commande suivante pour démarrer le client Smee. Remplacez
YOUR_DOMAIN
par l’URL du proxy webhook que vous avez copiée à l’étape précédente.Shell smee --url YOUR_DOMAIN --path /event_handler --port 3000
smee --url YOUR_DOMAIN --path /event_handler --port 3000
Un résultat similaire à ce qui suit s’affiche normalement :
Forwarding https://smee.io/YOUR_DOMAIN to http://127.0.0.1:3000/event_handler Connected https://smee.io/YOUR_DOMAIN
La commande smee --url https://smee.io/YOUR_DOMAIN
indique à Smee de transférer tous les événements de webhook reçus par le canal Smee vers le client Smee s’exécutant sur votre ordinateur. L’option --path /event_handler
transfère les événements à la route /event_handler
. L’option --port 3000
spécifie le port 3000, qui est le port que vous indiquerez à votre serveur d’écouter, quand vous ajouterez du code plus loin dans le tutoriel. Si vous utilisez Smee, votre machine n’a pas besoin d’être ouverte à l’Internet public pour recevoir des webhooks provenant de GitHub. Vous pouvez également ouvrir cette URL Smee dans votre navigateur pour inspecter les charges utiles de webhook à mesure qu’elles arrivent.
Nous vous recommandons de laisser cette fenêtre de terminal ouverte et Smee connecté pour le reste des étapes décrites dans ce guide. Bien que vous puissiez déconnecter et reconnecter le client Smee sans perdre votre domaine unique, il est plus facile de rester connecté et d’effectuer les autres tâches de ligne de commande dans une autre fenêtre de terminal.
Inscrire une GitHub App
Pour ce tutoriel, vous devez inscrire une GitHub App qui :
- a des webhooks actifs
- utilise une URL de webhook que vous pouvez recevoir localement
- dispose de l’autorisation de dépôt « Vérifications »
- s’abonne aux événements webhook « Suite de vérifications » et « Exécution de vérification »
Les étapes suivantes vous guident tout au long de la configuration d’une GitHub App avec ces paramètres. Pour plus d’informations sur les GitHub App, consultez « Inscription d’une application GitHub ».
- Dans le coin supérieur droit de n’importe quelle page sur GitHub, cliquez sur votre photo de profil.
- Accédez aux paramètres de votre compte.
- Pour une application appartenant à un compte personnel, cliquez sur Paramètres.
- Pour une application appartenant à une organisation :
- Cliquez sur Vos organisations.
- À droite de l’organisation, cliquez sur Paramètres.
- Dans la barre latérale gauche, cliquez sur Paramètres de développeur.
- Dans la barre latérale à gauche, cliquez sur GitHub Apps .
- Cliquez sur Nouvelle application GitHub.
- Sous « Nom de l’application GitHub », entrez un nom pour votre application. Par exemple,
USERNAME-ci-test-app
oùUSERNAME
est votre nom d'utilisateur GitHub. - Sous « URL de la page d’accueil », entrez une URL pour votre application. Par exemple, vous pouvez utiliser l’URL du référentiel que vous avez créé pour stocker le code de votre application.
- Ignorez les sections « Identification et autorisation des utilisateurs » et « Après installation » pour ce tutoriel.
- Assurez-vous que Actif est sélectionné sous « Webhooks ».
- Sous « URL de webhook », entrez l’URL de votre proxy webhook à partir d’une version antérieure. Pour plus d’informations, consultez « Obtenir une URL de proxy webhook ».
- Sous « Webhook secret », entrez une chaîne aléatoire. Ce secret est utilisé pour vérifier que les webhooks sont envoyés par GitHub. Enregistrez cette chaîne ; vous l’utiliserez plus tard.
- Sous « Autorisations du dépôt », en regard de « Vérifications », sélectionnez Lire et écrire.
- Sous « S’abonner aux événements », sélectionnez Suite de vérifications et Exécution de vérification.
- Sous « Où cette application GitHub peut-elle être installée ? », sélectionnez Uniquement sur ce compte. Vous pouvez le modifier cela ultérieurement si vous souhaitez publier votre application.
- Cliquez sur Créer une application GitHub.
Stocker les informations d’identification et les informations de connexion de votre application
Ce tutoriel vous montre comment stocker les informations d’identification de votre application et identifier les informations en tant que variables d’environnement dans un fichier .env
. Quand vous déployez votre application, vous devez changer la façon dont vous stockez les informations d’identification. Pour plus d’informations, consultez « Déployer votre application ».
Assurez-vous que vous êtes sur un ordinateur sécurisé avant d’effectuer ces étapes, car vous allez stocker vos informations d’identification localement.
-
Dans votre terminal, accédez au répertoire dans lequel votre clone est stocké.
-
Créez un fichier appelé
.env
au niveau supérieur de ce répertoire. -
Ajoutez
.env
à votre fichier.gitignore
. Cette action vous empêche de valider accidentellement les informations d'identification de votre application. -
Ajoutez le contenu suivant à votre fichier
.env
. RemplacezYOUR_HOSTNAME
par le nom de votre instance GitHub Enterprise Server. Vous mettrez à jour les autres valeurs ultérieurement.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"
-
Accédez à la page Paramètres de votre application :
-
Dans le coin supérieur droit de n’importe quelle page sur GitHub, cliquez sur votre photo de profil.
-
Accédez aux paramètres de votre compte.
- Pour une application appartenant à un compte personnel, cliquez sur Paramètres.
- Pour une application appartenant à une organisation :
- Cliquez sur Vos organisations.
- À droite de l’organisation, cliquez sur Paramètres.
-
Dans la barre latérale gauche, cliquez sur Paramètres de développeur.
-
Dans la barre latérale à gauche, cliquez sur GitHub Apps .
-
En regard du nom de votre application, cliquez sur Modifier.
-
-
Dans la page des paramètres de votre application, en regard de « ID d’application », recherchez l’ID d’application de votre application.
-
Dans votre fichier,
.env
remplacezYOUR_APP_ID
par l’ID d’application de votre application. -
Dans votre fichier
.env
, remplacezYOUR_WEBHOOK_SECRET
par le secret webhook de votre application. Si vous avez oublié votre secret webhook, sous « Secret webhook (facultatif) », cliquez sur Modifier le secret. Entrez un nouveau secret, puis cliquez sur Enregistrer les modifications. -
Dans la page des paramètres de votre application, sous « Clés privées », cliquez sur Générer une clé privée. Un fichier
.pem
de clé privée est téléchargée sur votre ordinateur. -
Ouvrez le fichier
.pem
avec un éditeur de texte ou utilisez la commande suivante sur la ligne de commande pour afficher le contenu du fichier :cat PATH/TO/YOUR/private-key.pem
. -
Copiez et collez l’intégralité du contenu du fichier dans votre fichier
.env
en tant que valeur deGITHUB_PRIVATE_KEY
, puis ajoutez des guillemets doubles autour de la valeur entière.Voici un exemple de fichier .env :
GITHUB_APP_IDENTIFIER=12345 GITHUB_WEBHOOK_SECRET=your webhook secret GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- ... HkVN9... ... -----END RSA PRIVATE KEY-----"
Ajouter du code pour votre GitHub App
Cette section vous montre comment ajouter du code de modèle de base pour votre GitHub App, et explique ce que fait le code. Plus loin dans le tutoriel, vous allez apprendre à modifier et à ajouter à ce code, pour générer les fonctionnalités de votre application.
Ajoutez le code du modèle suivant à votre fichier 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
Le reste de cette section explique ce que fait le code du modèle. Vous ne devez effectuer aucune étape dans cette section. Si vous êtes déjà familiarisé avec le code du modèle, vous pouvez passer à la section « Démarrer le serveur ».
Comprendre le code du modèle
Ouvrez le fichier server.rb
dans un éditeur de texte. Des commentaires dans le fichier fournissent un contexte supplémentaire pour le code du modèle. Nous vous recommandons de lire ces commentaires attentivement et même d’ajouter vos propres commentaires pour accompagner le nouveau code que vous écrivez.
Sous la liste des fichiers obligatoires, le premier code que vous voyez est la déclaration class GHApp < Sinatra::Application
. Vous allez écrire tout le code de votre GitHub App dans cette classe. Les sections suivantes expliquent en détail ce que fait le code à l’intérieur de cette classe.
- Définir le port
- Lire les variables d’environnement
- Activation de la journalisation
- Définir un filtre
before
- Définir le gestionnaire de routage
- Définir les méthodes d’assistance
Définir le port
La première chose que vous voyez à l’intérieur de la déclaration class GHApp < Sinatra::Application
est set :port 3000
. Cela définit le port utilisé lors du démarrage du serveur web pour qu’il corresponde au port vers lequel vous avez redirigé les charges utiles de votre webhook dans « Obtenir une URL de proxy webhook ».
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
Lire les variables d’environnement
Ensuite, cette classe lit les trois variables d’environnement que vous avez définies dans « Stocker les informations d’identification et les informations de connexion de votre application » et les stocke dans des variables à utiliser ultérieurement.
# 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']
Activation de la journalisation
Voici un bloc de code qui active la journalisation pendant le développement. Il s’agit de l’environnement par défaut dans Sinatra. Ce code active la journalisation au niveau DEBUG
pour afficher une sortie utile dans le terminal pendant le développement de l’application.
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
Définir un filtre before
Sinatra utilise des filtres before
qui vous permettent d’exécuter du code avant le gestionnaire de routage. Le bloc before
du modèle appelle quatre méthodes d’assistance : get_payload_request
, verify_webhook_signature
, authenticate_app
et authenticate_installation
. Pour plus d’informations, consultez « Filtres » et « Assistances » dans la documentation 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
Chacune de ces méthodes d’assistance est définie plus loin dans le code, dans le bloc de code qui commence par helpers do
. Pour plus d’informations, consultez « Définir les méthodes d’assistance ».
Sous verify_webhook_signature
, le code qui commence par unless @payload
est une mesure de sécurité. Si un nom de dépôt est fourni avec une charge utile de webhook, ce code vérifie que le nom du dépôt contient uniquement des caractères alphabétiques latins, des traits d’union et des traits de soulignement. Cela permet de s’assurer qu’un acteur malveillant ne tente pas d’exécuter des commandes arbitraires ou d’injecter de faux noms de dépôt. Plus tard, dans le bloc de code qui commence par helpers do
, la méthode d’assistance verify_webhook_signature
valide également les charges utiles de webhook entrantes en tant que mesure de sécurité supplémentaire.
Définir un gestionnaire de routage
Un itinéraire vide est inclus dans le code du modèle. Ce code gère toutes les demandes POST
vers l’itinéraire /event_handler
. Vous y ajouterez plus de code ultérieurement.
post '/event_handler' do
end
Définir les méthodes d’assistance
Quatre méthodes d’assistance sont appelées dans le bloc before
du code du modèle. Le bloc de code helpers do
définit chacune de ces méthodes d’assistance.
Gestion de la charge utile de webhook
La première méthode d’assistance get_payload_request
capture la charge utile de webhook et la convertit au format JSON, ce qui facilite beaucoup l’accès aux données de la charge utile.
Vérification de la signature du webhook
La deuxième méthode d’assistance verify_webhook_signature
effectue la vérification de la signature webhook pour s’assurer que GitHub a généré l’événement. Pour en savoir plus sur le code de la méthode d’assistance verify_webhook_signature
, consultez « Validation des livraisons de webhook ». Si les webhooks sont sécurisés, cette méthode journalise toutes les charges utiles entrantes sur votre terminal. Le code d’enregistreur d’événements est utile pour vérifier que votre serveur web fonctionne.
Authentification en tant qu’GitHub App
La troisième méthode d’assistance authenticate_app
permet à votre GitHub App de s’authentifier, afin qu’elle puisse demander un jeton d’installation.
Pour effectuer des appels d’API, vous allez utiliser la bibliothèque Octokit. Pour faire quelque chose d’intéressant avec cette bibliothèque, votre GitHub App doit s’authentifier. Pour plus d’informations sur la bibliothèque Octokit, consultez la documentation Octokit.
Les GitHub Apps ont trois méthodes d’authentification :
- Authentification en tant qu’GitHub App avec un jeton JWT (JSON Web Token).
- Authentification en tant qu’installation spécifique d’une GitHub App avec un jeton d’accès d’installation.
- Authentification pour le compte d’un utilisateur. Ce tutoriel n’utilise pas cette méthode d’authentification.
Vous découvrirez l’authentification en tant qu’installation dans la section suivante, « Authentification en tant qu’installation ».
L’authentification en tant qu’GitHub App vous permet de faire plusieurs choses :
- Vous pouvez récupérer des informations de gestion générales concernant votre GitHub App.
- Vous pouvez demander des jetons d’accès pour une installation de l’application.
Par exemple, vous vous authentifiez en tant qu’GitHub App pour récupérer une liste des comptes (organisation et personnel) qui ont installé votre application. Mais cette méthode d’authentification offre peu de possibilités avec l’API. Pour accéder aux données d’un référentiel et effectuer des opérations pour le compte de l’installation, vous devez vous authentifier en tant qu’installation. Pour ce faire, vous devez d’abord vous authentifier en tant qu’GitHub App pour demander un jeton d’accès d’installation. Pour plus d’informations, consultez « À propos de l’authentification avec une application GitHub ».
Avant de pouvoir utiliser la bibliothèque Octokit.rb pour effectuer des appels d’API, vous devez initialiser un client Octokit authentifié en tant qu’GitHub App, en utilisant la méthode d’assistance 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
Le code ci-dessus génère un jeton JSON Web Token (JWT) et l’utilise (ainsi que la clé privée de votre application) pour initialiser le client Octokit. GitHub vérifie l’authentification d’une demande en vérifiant le jeton avec la clé publique stockée de l’application. Pour en savoir plus sur le fonctionnement de ce code, consultez « Génération d’un jeton web JSON (JWT) pour une application GitHub ».
Authentification en tant qu’installation
La quatrième et dernière méthode d’assistance, authenticate_installation
, initialise un client Octokit authentifié en tant qu’installation, que vous pouvez utiliser pour effectuer des appels authentifiés à l’API.
Une installation fait référence à tout compte d’utilisateur ou d’organisation ayant installé l’application. Même si un utilisateur accorde à l’application l’accès à plusieurs dépôts sur ce compte, cela compte comme une seule installation, car il s’agit du même compte.
# 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
La méthode Octokit create_app_installation_access_token
crée un jeton d’installation. Pour plus d’informations, consultez « create_installation_access_token » dans la documentation Octokit.
Cette méthode accepte deux arguments :
- Installation (entier) : ID de l’installation d’une GitHub App
- Options (hachage, valeur par défaut :
{}
) : ensemble personnalisable d’options
Chaque fois qu’une GitHub App reçoit un webhook, elle inclut un objet installation
avec un id
. En utilisant le client authentifié en tant qu’GitHub App, vous transmettez cet ID à la méthode create_app_installation_access_token
afin de générer un jeton d’accès pour chaque installation. Étant donné que vous ne transmettez aucune option à la méthode, les options sont par défaut un hachage vide. La réponse pour create_app_installation_access_token
comprend deux champs : token
et expired_at
. Le code de modèle sélectionne le jeton dans la réponse et initialise un client d’installation.
Avec cette méthode en place, chaque fois que votre application reçoit une nouvelle charge utile webhook, elle crée un client pour l’installation qui a déclenché l’événement. Ce processus d’authentification permet à votre GitHub App de fonctionner sur toutes les installations de n’importe quel compte.
Démarrer le serveur
Votre application n’effectue aucune opération pour le moment. Toutefois, vous pouvez l’exécuter sur le serveur à ce stade.
-
Dans votre terminal, vérifiez que Smee est toujours en cours d’exécution. Pour plus d’informations, consultez « Obtenir une URL de proxy webhook ».
-
Ouvrez un nouvel onglet dans votre terminal, puis accédez avec
cd
au répertoire dans lequel vous avez cloné le dépôt que vous avez créé dans le tutoriel. Pour plus d’informations, consultez « Créer un dépôt pour stocker le code de votre application GitHub ». Le code Ruby dans ce référentiel démarre un serveur web Sinatra. -
Installez les dépendances en exécutant les deux commandes suivantes l’une après l’autre :
Shell gem install bundler
gem install bundler
Shell bundle install
bundle install
-
Après avoir installé les dépendances, démarrez le serveur en exécutant cette commande :
Shell bundle exec ruby server.rb
bundle exec ruby server.rb
La réponse doit ressembler à ceci :
> == 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
En cas d’erreur, vérifiez que vous avez créé le fichier
.env
dans le répertoire qui contientserver.rb
. -
Pour tester le serveur, accédez dans votre navigateur à
http://localhost:3000
.Si vous voyez une page d’erreur indiquant « Sinatra doesn't know this ditty », l’application fonctionne comme prévu. Il s’agit d’une page d’erreur, mais c’est une page d’erreur Sinatra. Cela signifie que votre application est connectée au serveur comme prévu. Ce message s’affiche parce que vous n’avez fourni à l’application aucun élément à afficher.
Tester que le serveur écoute votre application
Vous pouvez tester que le serveur écoute votre application en déclenchant un événement qu’il doit recevoir. Pour ce faire, installez l’application sur un dépôt de test, qui envoie l’événement installation
à votre application. Si l’application le reçoit, une sortie doit s’afficher sous l’onglet de terminal où vous exécutez server.rb
.
-
Créez un dépôt à utiliser pour tester le code de votre tutoriel. Pour plus d’informations, consultez « Création d’un dépôt ».
-
Installez l’GitHub App sur le dépôt que vous venez de créer. Pour plus d’informations, consultez « Installation de votre propre application GitHub ». Pendant le processus d’installation, choisissez Sélectionner uniquement les dépôts, puis sélectionnez le dépôt que vous avez créé à l’étape précédente.
-
Après avoir cliqué sur Installer, examinez la sortie sous l’onglet de terminal où vous exécutez
server.rb
. Un résultat semblable à celui-ci doit s’afficher :> 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 vous voyez une sortie comme celle-ci, cela signifie que votre application a reçu une notification indiquant qu’elle a été installée sur votre compte GitHub. L’application s’exécute sur le serveur comme prévu.
Si cette sortie ne s’affiche pas, assurez-vous que Smee s’exécute correctement dans un autre onglet de terminal. Si vous devez redémarrer Smee, notez que vous devez également désinstaller puis réinstaller l’application pour envoyer à nouveau l’événement
installation
à votre application et voir la sortie dans le terminal.
Si vous vous demandez d’où provient la sortie de terminal ci-dessus, elle est écrite dans le code de modèle d’application que vous avez ajouté à server.rb
dans la section « Ajouter du code pour votre GitHub App ».
Partie 1. Création de l’interface de l’API Vérifications
Dans cette partie, vous allez ajouter le code nécessaire pour recevoir des événements de webhook check_suite
, puis créer et mettre à jour des exécutions de vérification. Vous allez également apprendre à créer des exécutions de vérification quand une vérification a été redemandée sur GitHub. À la fin de cette section, vous serez en mesure d’afficher l’exécution de vérification que vous avez créée dans une demande de tirage GitHub.
Votre exécution de vérification n’effectue aucune vérification du code dans cette section. Vous allez ajouter cette fonctionnalité dans la « Partie 2 : Création du test de CI ».
Vous devriez déjà disposer d’un canal Smee configuré qui transfère des charges utiles de webhook à votre serveur local. Votre serveur devrait être en cours d’exécution et connecté à l’GitHub App que vous avez inscrite et installée sur un dépôt de test.
Voici les étapes que vous allez suivre dans la Partie 1 :
- Ajouter la gestion des événements
- Créer une exécution de vérifications
- Mettre à jour une exécution de vérifications
Étape 1.1. Ajouter la gestion des événements
Votre application étant abonnée aux événements Suite de vérifications et Exécution de vérification, elle va recevoir les webhooks check_suite
et check_run
. GitHub envoie les charges utiles de webhook sous forme de demandes POST
. Étant donné que vous avez transféré vos charges utiles de webhook Smee à http://localhost:3000/event_handler
, votre serveur recevra les charges utiles de demande POST
dans l’itinéraire post '/event_handler'
.
Ouvrez le fichier server.rb
que vous avez créé dans « Ajouter du code pour votre GitHub App », puis recherchez le code suivant. Une route post '/event_handler'
vide est déjà incluse dans le code du modèle. Voici comment se présente l’itinéraire vide :
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
Dans le bloc de code qui commence par post '/event_handler' do
, où il est indiqué # ADD EVENT HANDLING HERE #
, ajoutez le code suivant. Cette route gère l’événement 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
Chaque événement que GitHub envoie inclut un en-tête de demande appelé HTTP_X_GITHUB_EVENT
, qui indique le type d’événement dans la demande POST
. En ce moment précis, vous ne vous intéressez qu’aux événements de type check_suite
, qui sont émis lors de la création d’une suite de vérifications. Chaque événement a un champ action
supplémentaire qui indique le type d’action ayant déclenché les événements. Pour check_suite
, le champ action
peut être requested
, rerequested
ou completed
.
L’action requested
demande une exécution de vérification chaque fois que du code est envoyé (push) au dépôt, tandis que l’action rerequested
demande que vous réexécutiez une vérification du code existant déjà dans le dépôt. Étant donné que les actions requested
et rerequested
nécessitent toutes deux la création d’une exécution de vérification, vous allez appeler une assistance appelée create_check_run
. Écrivons cette méthode maintenant.
Étape 1.2. Créer une exécution de vérifications
Vous allez ajouter cette nouvelle méthode en tant qu’assistance Sinatra au cas où vous voudriez que d’autres itinéraires l’utilisent également.
Dans le bloc de code qui commence par helpers do
, où il est indiqué # ADD CREATE_CHECK_RUN HELPER METHOD HERE #
, ajoutez le code suivant :
# 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
Ce code appelle le point de terminaison POST /repos/{owner}/{repo}/check-runs
à l’aide de la méthode create_check_run d’Octokit. Pour plus d’informations sur le point de terminaison, consultez « Points de terminaison d’API REST pour les exécutions de vérifications. »
Pour créer une exécution de vérification, seuls deux paramètres d’entrée sont requis : name
et head_sha
. Dans ce code, nous nommons l’exécution de vérification « Octo RuboCop », car nous utiliserons RuboCop pour implémenter le test de CI plus loin dans le tutoriel. Toutefois, vous pouvez choisir n’importe quel nom pour l’exécution de vérification. Pour plus d’informations sur RuboCop, consultez la documentation RuboCop.
Pour l’instant, vous ne fournissez que les paramètres requis pour que la fonctionnalité de base opère, mais vous allez mettre à jour l’exécution de vérification plus tard, à mesure que vous recueillerez plus d’informations sur celle-ci. Par défaut, GitHub définit status
sur queued
.
GitHub créant une exécution de vérification pour un SHA de validation spécifique, le paramètre head_sha
est requis. Vous pouvez trouver le SHA de validation dans la charge utile du webhook. Même si vous ne créez pour l’instant qu’une exécution de vérification pour l’événement check_suite
, il est bon de savoir que le head_sha
est inclus dans les objets check_suite
et check_run
dans les charges utiles de l’événement.
Le code ci-dessus utilise un opérateur ternaire, qui fonctionne comme une instruction if/else
, pour vérifier si la charge utile contient un objet check_run
. Si c’est le cas, vous lisez le head_sha
à partir de l’objet check_run
. Sinon, vous l’avez lu à partir de l’objet check_suite
.
Tester le code
Les étapes suivantes vous montrent comment tester que le code fonctionne et qu’il crée correctement une exécution de vérification.
-
Exécutez la commande suivante pour redémarrer le serveur à partir de votre terminal. Si le serveur est déjà en cours d’exécution, entrez
Ctrl-C
dans votre terminal pour arrêter le serveur, puis exécutez la commande suivante pour redémarrer le serveur.Shell ruby server.rb
ruby server.rb
-
Créez une demande de tirage dans le dépôt de test que vous avez créé dans « Tester que le serveur écoute votre application ». Il s’agit du dépôt auquel vous avez accordé l’accès à l’application.
-
Dans la demande de tirage que vous avez créée, accédez à l’onglet Vérifications. Vous devriez voir une exécution de vérification avec le nom « Octo RuboCop » ou le nom que vous avez éventuellement choisi pour la vérification.
Si vous voyez d’autres applications sous l’onglet Vérifications, cela signifie que vous disposez d’autres applications installées sur votre dépôt, qui disposent d’un accès en lecture et écriture aux vérifications, et sont abonnées aux événements Suite de vérifications et Exécution de vérification. Cela peut également signifier que vous avez des workflows GitHub Actions sur le dépôt qui sont déclenchés par l’événement pull_request
ou pull_request_target
.
Jusqu’à présent, vous avez demandé à GitHub de créer une exécution de vérification. L’état de l’exécution de vérification dans la demande de tirage est défini sur mise en file d’attente avec une icône jaune. À l’étape suivante, vous attendez que GitHub crée l’exécution de vérification et mette à jour son état.
Étape 1.3. Mettre à jour une exécution de vérifications
Quand votre méthode create_check_run
s’exécute, elle demande à GitHub de créer une exécution de vérification. Quand GitHub termine la création de l’exécution de vérification, vous recevez l’événement de webhook check_run
avec l’action created
. Cet événement est votre signal pour commencer à exécuter la vérification.
Vous allez mettre à jour votre gestionnaire d’événements pour rechercher l’action created
. Pendant que vous mettez à jour le gestionnaire d’événements, vous pouvez ajouter une condition pour l’action rerequested
. Quand quelqu’un réexécute un seul test sur GitHub en cliquant sur le bouton « Réexécuter », GitHub envoie l’événement d’exécution de vérification rerequested
à votre application. Quand une exécution de vérification est rerequested
, vous recommencez le processus et créez une exécution de vérification. Pour ce faire, vous incluez une condition pour l’événement check_run
dans la route post '/event_handler'
.
Dans le bloc de code qui commence par post '/event_handler' do
, où il est indiqué # ADD CHECK_RUN METHOD HERE #
, ajoutez le code suivant :
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 envoie tous les événements pour les exécutions de vérification created
à chaque application installée sur un dépôt disposant des autorisations de vérification nécessaires. Cela signifie que votre application recevra des exécutions de vérification créées par d’autres applications. Une exécution de vérification created
diffère légèrement d’une suite de vérifications requested
ou rerequested
, que GitHub envoie uniquement aux applications qui doivent exécuter une vérification. Le code ci-dessus recherche l’ID d’application de l’exécution de vérification. Cela filtre toutes les exécutions de vérification pour d’autres applications sur le dépôt.
Ensuite, vous allez écrire la méthode initiate_check_run
, qui est l’endroit où vous allez mettre à jour l’état de la vérification d’exécution et préparer le lancement de votre test de CI.
Dans cette section, vous n’allez pas encore lancer le test de CI, mais découvrir comment mettre à jour l’état de l’exécution de vérification de queued
à pending
, puis de pending
à completed
voir le flux global d’une exécution de vérification. Dans la « Partie 2 : Création du test de CI », vous allez ajouter le code qui effectue réellement le test de CI.
Nous allons créer la méthode initiate_check_run
et mettre à jour l’état de l’exécution de vérification.
Dans le bloc de code qui commence par helpers do
, où il est indiqué # ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
, ajoutez le code suivant :
# 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
Le code ci-dessus appelle le point de terminaison PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
à l’aide de la méthode Octokit update_check_run
et met à jour l’exécution de vérification que vous avez déjà créée. Pour plus d’informations sur le point de terminaison, consultez « Points de terminaison d’API REST pour les exécutions de vérifications. »
Examinons ce que fait ce code. Tout d’abord, il met à jour l’état de l’exécution de vérification en in_progress
, et définit implicitement l’heure started_at
sur l’heure actuelle. Dans la Partie 2 de ce tutoriel, vous allez ajouter du code qui lance un test de CI réel sous ***** RUN A CI TEST *****
. Pour l’instant, vous allez laisser cette section comme un espace réservé, de sorte que le code qui le suit simulera simplement le fait que le processus de CI réussit, ainsi que tous les tests. Enfin, le code remet à jour l’état de l’exécution de vérification sur completed
.
Quand vous utilisez l’API REST pour fournir l’état d’exécution de vérification completed
, les paramètres conclusion
et completed_at
sont requis. conclusion
récapitule le résultat d’une exécution de vérification et peut être success
, failure
, neutral
, cancelled
, timed_out
, skipped
ou action_required
. Vous allez définir la conclusion sur success
, l’heure completed_at
sur l’heure actuelle, et l’état sur completed
.
Vous pouvez également fournir des informations supplémentaires l’action de votre vérification, mais vous y reviendrez dans la section suivante.
Tester le code
Les étapes suivantes vous montrent comment tester le fonctionnement du code et du bouton « Réexécuter tout » que vous avez créé.
-
Exécutez la commande suivante pour redémarrer le serveur à partir de votre terminal. Si le serveur est déjà en cours d’exécution, entrez
Ctrl-C
dans votre terminal pour arrêter le serveur, puis exécutez la commande suivante pour redémarrer le serveur.Shell ruby server.rb
ruby server.rb
-
Créez une demande de tirage dans le dépôt de test que vous avez créé dans « Tester que le serveur écoute votre application ». Il s’agit du dépôt auquel vous avez accordé l’accès à l’application.
-
Dans la demande de tirage que vous venez de créer, accédez à l’onglet Vérifications. Vous devriez voir un bouton « Réexécuter tout ».
-
Cliquez sur le bouton « Réexécuter tout » en haut à droite. Le test doit se réexécuter et se terminer par
success
.
Partie 2. Création d’un test de CI
Maintenant que vous avez l’interface créée pour recevoir des événements d’API et créer des exécutions de vérification, vous pouvez créer une exécution de vérification qui implémente un test de CI.
RuboCop est un linter et formateur de code Ruby. Il vérifie le code Ruby pour s’assurer qu’il est conforme au Guide de style Ruby. Pour plus d’informations, consultez la documentation RuboCop.
RuboCop à trois fonctions principales :
- Linting pour vérifier le style de code
- Mise en forme du code
- Remplace les fonctionnalités de linting Ruby natives en utilisant
ruby -w
Votre application exécute RuboCop sur le serveur de CI et crée des exécutions de vérification (tests de CI dans ce cas) qui rapportent les résultats que RuboCop rapporte à GitHub.
L’API REST vous permet de rapporter des détails riches sur chaque exécution de vérification, dont des états, des images, des résumés, des annotations et des actions demandées.
Les annotations sont des informations sur des lignes de code spécifiques dans un dépôt. Une annotation vous permet d’identifier et de visualiser les parties exactes du code pour lesquelles vous souhaitez afficher des informations supplémentaires. Par exemple, vous pouvez afficher ces informations sous la forme d’un commentaire, d’une erreur ou d’un avertissement sur une ligne de code spécifique. Ce tutoriel utilise des annotations pour visualiser des erreurs de RuboCop.
Pour tirer parti des actions demandées, les développeurs d’applications peuvent créer des boutons sous l’onglet Vérifications des demandes d’extraction. Quand quelqu’un clique sur l’un de ces boutons, le clic envoie un événement requested_action
check_run
à l’GitHub App. La mesure de l’application prend est entièrement configurable par son développeur. Ce tutoriel vous guidera pour ajouter un bouton permettant aux utilisateurs de demander que RuboCop corrige les erreurs qu’il trouve. RuboCop prend en charge la correction automatique des erreurs à l’aide d’une option de ligne de commande, et vous allez configurer la requested_action
pour tirer parti de cette option.
Voici les étapes que vous allez suivre dans cette section :
- Ajouter un fichier Ruby
- Autoriser RuboCop à cloner le référentiel de test
- Exécuter RuboCop
- Collecter les erreurs de RuboCop
- Mettre à jour l’exécution de vérification avec les résultats de test de CI
- Corriger automatiquement les erreurs de RuboCop
Étape 2.1. Ajouter un fichier Ruby
Vous pouvez transmettre des fichiers spécifiques ou des répertoires entiers pour vérification par RuboCop. Dans ce tutoriel, vous allez exécuter RuboCop sur un répertoire entier. RuboCop vérifie uniquement le code Ruby. Pour tester votre GitHub App, vous devez ajouter un fichier Ruby dans votre dépôt qui contient des erreurs que RuboCop doit rechercher. Après avoir ajouté le fichier Ruby suivant à votre dépôt, vous allez mettre à jour votre vérification de CI pour exécuter RuboCop sur le code.
-
Accédez au dépôt de test que vous avez créé dans « Tester que le serveur écoute votre application ». Il s’agit du dépôt auquel vous avez accordé l’accès à l’application.
-
Créez un nouveau fichier appelé
myfile.rb
. Pour plus d’informations, consultez « Création de fichiers ». -
Ajoutez le contenu suivant à
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 vous avez créé le fichier localement, veillez à le commiter et à le pousser vers votre dépôt sur GitHub.
Étape 2.2. Autoriser RuboCop à cloner le dépôt de test
RuboCop est disponible en tant qu’utilitaire de ligne de commande. Cela signifie que si vous souhaitez exécuter RuboCop sur un dépôt, votre GitHub App doit cloner une copie locale du dépôt sur le serveur de CI afin que RuboCop puisse analyser les fichiers. Pour ce faire, votre code doit être en mesure d’exécuter des opérations Git, et votre GitHub App doit disposer des autorisations appropriées pour cloner un dépôt.
Autoriser les opérations Git
Pour exécuter des opérations Git dans votre application Ruby, vous pouvez utiliser la gemme ruby-git. Le Gemfile
que vous avez créé dans « Configuration » inclut déjà la gemme ruby-git, et vous l’avez installé lors de l’exécution de bundle install
dans « Démarrer le serveur ».
Maintenant, en haut de votre fichier server.rb
, sous les autres éléments require
, ajoutez le code suivant :
require 'git'
require 'git'
Mettre à jour les autorisations de votre application
Ensuite, vous devez mettre à jour les autorisations de votre GitHub App. Pour cloner un dépôt, votre application a besoin d’une autorisation de lecture pour « Contenu ». Plus loin dans ce tutoriel, elle aura besoin d’une autorisation d’écriture pour pousser du contenu vers GitHub. Pour mettre à jour les autorisations de votre application :
- Sélectionnez votre application dans la page des paramètres de l’application, puis cliquez sur Autorisations et événements dans la barre latérale.
- Sous « Autorisations du dépôt », en regard de « Contenu », sélectionnez Lire et écrire.
- Cliquez sur Enregistrer les changements au bas de la page.
- Si vous avez installé l’application sur votre compte, vérifiez votre adresse e-mail et suivez le lien pour accepter les nouvelles autorisations. Chaque fois que vous modifiez les autorisations ou les webhooks de votre application, les utilisateurs qui ont installé l’application doivent accepter les nouvelles autorisations avant que les changements ne prennent effet (y compris vous-même). Vous pouvez aussi accepter les nouvelles autorisations en accédant à la page de vos installations. Un lien apparaît sous le nom de l’application vous informant que celle-ci demande des autorisations différentes. Cliquez sur « Examiner la demande », puis sur « Accepter les nouvelles autorisations ».
Ajouter du code pour cloner un dépôt
Pour cloner un dépôt, le code utilise les autorisations de votre GitHub App et le Kit de développement logiciel (SDK) Octokit pour créer un jeton d’installation pour votre application (x-access-token:TOKEN
) et l’utiliser dans la commande clone suivante :
git clone https://x-access-token:TOKEN@github.com/OWNER/REPO.git
La commande ci-dessus clone un dépôt via HTTP. Il requiert le nom complet du dépôt, qui inclut le propriétaire du dépôt (utilisateur ou organisation) et le nom du dépôt. Par exemple, le dépôt octocat Hello-World porte le nom complet octocat/hello-world
.
Ouvrez votre fichier server.rb
. Dans le bloc de code qui commence par helpers do
, où il est indiqué # ADD CLONE_REPOSITORY HELPER METHOD HERE #
, ajoutez le code suivant :
# 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
Le code ci-dessus utilise la gemme ruby-git
pour cloner le dépôt à l’aide du jeton d’installation de l’application. Il clone le code dans le même répertoire que server.rb
. Pour exécuter des commandes Git dans le dépôt, le code doit changer dans le répertoire du dépôt. Avant de modifier des répertoires, le code stocke le répertoire de travail actif dans une variable (pwd
) afin de se rappeler où retourner avant de quitter la méthode clone_repository
.
Dans le répertoire du dépôt, ce code extrait et fusionne les dernières modifications (@git.pull
) et extrait la référence Git spécifique (@git.checkout(ref)
). Le code permettant d’effectuer tout cela s’intègre parfaitement dans sa propre méthode. Pour effectuer ces opérations, la méthode a besoin du nom et du nom complet du dépôt, ainsi que de la référence pour la validation. La référence peut être un SHA, une branche ou une étiquette de validation. Quand l’opération est terminée, le code bascule vers le répertoire de travail d’origine (pwd
).
Vous disposez maintenant d’une méthode qui clone un dépôt et valide une référence. Ensuite, vous devez ajouter du code pour obtenir les paramètres d’entrée requis et appeler la nouvelle méthode clone_repository
.
Dans le bloc de code qui commence par helpers do
, dans la méthode d’assistance initiate_check_run
où il est indiqué # ***** RUN A CI TEST *****
, ajoutez le code suivant :
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 #
Le code ci-dessus obtient le nom complet du dépôt et le SHA principal de la validation à partir de la charge utile du webhook check_run
.
Étape 2.3. Exécuter RuboCop
Jusqu’à présent, votre code clone le dépôt et crée des exécutions de vérification avec votre serveur de CI. Vous allez maintenant entrer dans les détails du linter RuboCop et des annotations de vérifications.
Tout d’abord, vous allez ajouter du code pour exécuter RuboCop et enregistrer les erreurs de code de style au format JSON.
Dans le bloc de code qui commence par helpers do
, recherchez la méthode d’assistance initiate_check_run
. À l’intérieur de cette méthode d’assistance, sous clone_repository(full_repo_name, repository, head_sha)
, où il est indiqué # ADD CODE HERE TO RUN RUBOCOP #
, ajoutez le code suivant :
# 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 #
Le code au-dessus exécute RuboCop sur tous les fichiers du répertoire du dépôt. L’option --format json
enregistre une copie des résultats de linting dans un format analysable par une machine. Pour plus d’informations et un exemple de format JSON, consultez « Formateur JSON » dans la documentation RuboCop. Ce code analyse également le code JSON afin que vous puissiez facilement accéder aux clés et aux valeurs de votre GitHub App en utilisant la variable @output
.
Après avoir exécuté RuboCop et enregistré les résultats de linting, ce code exécute la commande rm -rf
pour supprimer l’extraction du dépôt. Comme le code stocke les résultats de RuboCop dans une variable @report
, il peut supprimer la validation du dépôt de façon sécurisée.
La commande rm -rf
ne peut pas être annulée. Pour assurer la sécurité de votre application, le code de ce tutoriel vérifie les webhooks entrants à la recherche de commandes malveillantes injectées qui pourraient être utilisées pour supprimer un répertoire différent de celui prévu par votre application. Par exemple, si un acteur mal intentionné envoyait un webhook avec le nom de dépôt ./
, votre application supprimerait le répertoire racine. La méthode verify_webhook_signature
valide l’expéditeur du webhook. Le gestionnaire d’événements verify_webhook_signature
vérifie également que le nom du dépôt est valide. Pour plus d’informations, consultez « Définir un filtre before
».
Tester le code
Les étapes suivantes vous montrent comment tester le fonctionnement du code et afficher les erreurs signalées par RuboCop.
-
Exécutez la commande suivante pour redémarrer le serveur à partir de votre terminal. Si le serveur est déjà en cours d’exécution, entrez
Ctrl-C
dans votre terminal pour arrêter le serveur, puis exécutez la commande suivante pour redémarrer le serveur.Shell ruby server.rb
ruby server.rb
-
Dans le dépôt dans lequel vous avez ajouté le fichier
myfile.rb
, créez une demande de tirage. -
Dans l’onglet de votre terminal où le serveur est en cours d’exécution, vous devez voir la sortie de débogage qui contient des erreurs de linting. Les erreurs de linting sont affichées sans aucune mise en forme. Vous pouvez copier et coller votre sortie de débogage dans un outil web tel que le formateur JSON, pour mettre en forme votre sortie JSON comme dans l’exemple suivant :
{ "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 } }
Étape 2.4. Collecter les erreurs de RuboCop
La variable @output
contient les résultats JSON analysés du rapport de RuboCop. Comme indiqué dans l’exemple de sortie à l’étape précédente, les résultats contiennent une section summary
que votre code peut utiliser pour déterminer rapidement s’il y a des erreurs. Le code suivant définira la conclusion de l’exécution de vérification sur success
si aucune erreur n’est signalée. RuboCop signale des erreurs pour chaque fichier figurant dans le tableau files
. Par conséquent, s’il y a des erreurs, vous devez extraire des données de l’objet fichier.
Les points de terminaison de l’API REST disponibles pour gérer les exécutions de vérification vous permettent de créer des annotations pour des lignes de code spécifiques. Lorsque vous créez ou mettez à jour une exécution de vérification, vous pouvez ajouter des annotations. Dans ce tutoriel, vous allez mettre à jour l’exécution de vérification avec des annotations, en utilisant le point de terminaison PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
. Pour plus d’informations sur le point de terminaison, consultez « Points de terminaison d’API REST pour les exécutions de vérifications. »
L’API limite le nombre d’annotations à un maximum de 50 par demande. Pour créer plus de 50 annotations, vous devez adresser plusieurs demandes au point de terminaison « Mettre à jour une exécution de vérification ». Par exemple, pour créer 105 annotations, vous devez adresser trois demandes distinctes à l’API. Les deux premières demandes auraient chacune 50 annotations, et la troisième inclurait les cinq annotations restantes. Chaque fois que vous mettez à jour l’exécution de vérification, des annotations sont ajoutées à la liste des annotations existantes pour l’exécution de vérification.
Une exécution de vérification attend des annotations sous la forme d’un tableau d’objets. Chaque objet d’annotation doit inclure les éléments path
, start_line
, end_line
, annotation_level
et message
. RuboCop fournissant également les paramètres start_column
et end_column
, vous pouvez inclure ces paramètres facultatifs dans l’annotation. Les annotations ne prennent en charge les paramètres start_column
et end_column
que sur la même ligne. Pour plus d’informations, consultez l’objet annotations
dans « Points de terminaison d’API REST pour les exécutions de vérifications ».
Vous allez maintenant ajouter du code pour extraire les informations requises de RuboCop qui sont nécessaires pour créer chaque annotation.
Sous le code que vous avez ajouté à l’étape précédente, où il est indiqué # ADD ANNOTATIONS CODE HERE #
, ajoutez le code suivant :
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 #
Ce code limite le nombre total d’annotations à 50. Toutefois, vous pouvez le modifier pour mettre à jour l’exécution de vérification pour chaque lot de 50 annotations. Le code ci-dessus inclut la variable max_annotations
qui définit la limite de 50 utilisée dans la boucle qui itère dans les attaques.
Lorsque la valeur offense_count
est zéro, le résultat du test de CI est success
. S’il y a des erreurs, ce code définit la conclusion sur neutral
afin d’empêcher l’application stricte d’erreurs à partir de linters de code. Toutefois, vous pouvez modifier la conclusion en failure
si vous souhaitez vous assurer que la suite de vérifications échoue en cas d’erreurs de linting.
Lorsque des erreurs sont signalées, le code ci-dessus itère sur le tableau files
dans le rapport de RuboCop. Pour chaque fichier, il extrait le chemin d’accès du fichier et définit le niveau d’annotation sur notice
. Vous pourriez aller encore plus loin et définir des niveaux d’avertissement spécifiques pour chaque type de RuboCop Cop mais, pour simplifier les choses dans ce tutoriel, toutes les erreurs sont définies à un niveau de notice
.
Ce code itère également sur chaque erreur dans le tableau offenses
et collecte l’emplacement de l’attaque et du message d’erreur. Après avoir extrait les informations nécessaires, le code crée une annotation pour chaque erreur et la stocke dans le tableau annotations
. En effet, les annotations ne prennent en charge les colonnes de début et de fin que sur la même ligne, et les paramètres start_column
et end_column
ne sont ajoutés à l’objet annotation
que si les valeurs de ligne de début et de fin sont identiques.
Ce code ne crée pas encore d’annotation pour l’exécution de vérification. Vous allez ajouter ce code dans la section suivante.
Étape 2.5. Mettre à jour l’exécution de vérification avec les résultats de test de CI
Chaque exécution de vérification de GitHub contient un objet output
qui inclut des éléments title
, summary
, text
, annotations
et images
. Étant donné que les paramètres summary
et title
sont les seuls requis pour le output
, mais qu’ils ne fournissent pas beaucoup de détails, ce tutoriel ajoute également text
et annotations
.
Pour le summary
, cet exemple utilise les informations résumées de RuboCop, et ajoute de nouvelles lignes (\n
) pour mettre en forme la sortie. Vous pouvez personnaliser ce que vous ajoutez au text
paramètre, mais cet exemple définit le text
paramètre sur la version RuboCop. Le code suivant définit les summary
et text
.
Sous le code que vous avez ajouté à l’étape précédente, où il est indiqué # ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
, ajoutez le code suivant :
# 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']}"
Votre code doit maintenant contenir toutes les informations nécessaires pour mettre à jour votre exécution de vérification. Dans « Étape 1.3. Mettre à jour une exécution de vérification », vous avez ajouté du code pour définir l’état de l’exécution de vérification sur success
. Vous devez mettre à jour ce code pour utiliser la variable conclusion
que vous définissez en fonction des résultats de RuboCop (sur success
ou neutral
). Voici le code que vous avez ajouté à votre fichier 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'
)
Remplacez ce code par le code suivant :
# 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'
)
Maintenant que votre code définit une conclusion basée sur l’état du test de CI et ajoute la sortie des résultats de RuboCop, vous avez créé un test de CI.
Le code ci-dessus ajoute également une fonctionnalité appelée « actions demandées » à votre serveur de CI, via l’objet actions
. Pour plus d’informations, consultez « Demander d’autres actions à partir d’une exécution de vérification ». Les actions demandées ajoutent un bouton sous l’onglet Vérifications de GitHub, qui permet à une personne de demander l’exécution de vérification pour prendre une mesure supplémentaire. La mesure supplémentaire est entièrement configurable par votre application. Par exemple, RuboCop disposant d’une fonctionnalité permettant de corriger automatiquement les erreurs détectées dans le code Ruby, votre serveur de CI peut utiliser un bouton Actions demandées pour permettre aux à des personnes de demander des corrections automatiques d’erreurs. Lorsque quelqu’un clique sur le bouton, l’application reçoit l’événement check_run
avec une action requested_action
. Chaque action demandée a un identifier
que l’application utilise pour déterminer le bouton qui a été cliqué.
Dans le code ci-dessus, RuboCop ne corrige pas encore automatiquement les erreurs. Vous l’ajouterez plus loin dans le tutoriel.
Tester le code
Les étapes suivantes vous montrent comment tester le fonctionnement du code et afficher le test de CI que vous venez de créer.
-
Exécutez la commande suivante pour redémarrer le serveur à partir de votre terminal. Si le serveur est déjà en cours d’exécution, entrez
Ctrl-C
dans votre terminal pour arrêter le serveur, puis exécutez la commande suivante pour redémarrer le serveur.Shell ruby server.rb
ruby server.rb
-
Dans le dépôt dans lequel vous avez ajouté le fichier
myfile.rb
, créez une demande de tirage. -
Dans la demande de tirage que vous venez de créer, accédez à l’onglet Vérifications. Vous devriez voir des annotations pour chacune des erreurs détectées par RuboCop. Notez également le bouton « Corriger cela » que vous avez créé en ajoutant une action demandée.
Étape 2.6. Corriger automatiquement les erreurs de RuboCop
Jusqu’à présent, vous avez créé un test de CI. Dans cette section, vous allez ajouter une fonctionnalité qui utilise RuboCop pour corriger automatiquement les erreurs détectées. Vous avez déjà ajouté le bouton « Corriger cela » à l’Étape 2.5. Mettre à jour l’exécution de vérification avec les résultats de test de CI ». Vous allez maintenant ajouter le code pour gérer l’événement d’exécution de vérification requested_action
qui est déclenché quand quelqu’un clique sur le bouton « Corriger cela ».
L’outil RuboCop offre l’option de ligne de commande --auto-correct
pour corriger automatiquement les erreurs détectées. Pour plus d’informations, consultez « Correction automatique des infractions » dans la documentation RuboCop. Lorsque vous utilisez la fonctionnalité --auto-correct
, les mises à jour sont appliquées aux fichiers locaux sur le serveur. Vous devez pousser les modifications vers GitHub après que RuboCop a apporté les correctifs.
Pour effectuer une poussée vers à un dépôt, votre application doit disposer d’autorisations d’écriture pour « Contenu » dans un dépôt. Vous avez déjà défini cette autorisation sur En lecture et en écriture à « l’Étape 2.2. Autoriser RuboCop à cloner le référentiel de test ».
Pour commiter des fichiers, Git doit connaître le nom d’utilisateur et l’adresse e-mail à associer au commit. Ensuite, vous allez ajouter des variables d’environnement pour stocker le nom et l’adresse e-mail que votre application utilisera quand elle effectuera des commits Git.
-
Ouvrez le fichier
.env
que vous avez créé dans ce tutoriel. -
Ajoutez les variables d’environnement suivantes à votre fichier
.env
. RemplacezAPP_NAME
par le nom de votre application etEMAIL_ADDRESS
par un e-mail que vous souhaitez utiliser pour cet exemple.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"
Ensuite, vous devez ajouter du code pour lire les variables d’environnement et définir la configuration Git. Vous allez bientôt ajouter ce code.
Lorsque quelqu’un clique sur le bouton « Corriger cela », votre application reçoit le webhook d’exécution de vérification avec le type d’action requested_action
.
Dans « l’Étape 1.3. Mise à jour d’une exécution de vérification », vous avez mis à jour le event_handler
dans votre fichier server.rb
pour rechercher les actions dans l’événement check_run
. Vous disposez déjà d’une instruction case pour gérer les types d’actions created
et 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
Après le cas rerequested
, où il est indiqué # ADD REQUESTED_ACTION METHOD HERE #
, ajoutez le code suivant :
when 'requested_action' take_requested_action
when 'requested_action'
take_requested_action
Ce code appelle une nouvelle méthode qui gère tous les événements requested_action
pour votre application.
Dans le bloc de code qui commence par helpers do
, où il est indiqué # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
, ajoutez la méthode d’assistance suivante :
# 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
Le code ci-dessus clone un référentiel comme le code que vous avez ajouté dans « Étape 2.2. Autoriser RuboCop à cloner le référentiel de test ». Une instruction if
vérifie que l’identificateur de l’action demandée correspond à l’identificateur du bouton RuboCop (fix_rubocop_notices
). Quand ils correspondent, le code clone le dépôt, définit le nom d’utilisateur et l’e-mail Git, puis exécute RuboCop avec l’option --auto-correct
. L’option --auto-correct
applique automatiquement les modifications aux fichiers du serveur de CI local.
Les fichiers sont changés localement, mais vous devez toujours les pousser vers GitHub. Vous allez utiliser la gemme ruby-git
pour commiter tous les fichiers. Git dispose d’une commande qui effectue une copie intermédiaire de tous les fichiers modifiés ou supprimés et les valide : git commit -a
. Pour faire la même chose à l’aide de ruby-git
, le code ci-dessus utilise la méthode commit_all
. Ensuite, le code pousse les fichiers commités vers GitHub avec le jeton d’installation, en utilisant la même méthode d’authentification que la commande Git clone
. Enfin, il supprime le répertoire du dépôt pour s’assurer que le répertoire de travail est prêt pour l’événement suivant.
Le code que vous avez écrit termine maintenant votre serveur d’intégration continue que vous avez généré avec une GitHub App et des vérifications. Pour afficher le code final complet de votre application, consultez « Exemple de code complet ».
Tester le code
Les étapes suivantes vous montrent comment vérifier que le code fonctionne et que RuboCop peut corriger automatiquement les erreurs qu’il trouve.
-
Exécutez la commande suivante pour redémarrer le serveur à partir de votre terminal. Si le serveur est déjà en cours d’exécution, entrez
Ctrl-C
dans votre terminal pour arrêter le serveur, puis exécutez la commande suivante pour redémarrer le serveur.Shell ruby server.rb
ruby server.rb
-
Dans le dépôt dans lequel vous avez ajouté le fichier
myfile.rb
, créez une demande de tirage. -
Dans la nouvelle demande de tirage que vous avez créée, accédez à l’onglet Vérifications, puis cliquez sur le bouton « Corriger cela » pour corriger automatiquement les erreurs détectées par RuboCop.
-
Accédez à l’onglet Commits. Vous devriez voir un nouveau commit par le nom d’utilisateur que vous avez défini dans votre configuration Git. Il se peut que vous deviez actualiser votre navigateur pour voir la mise à jour.
-
Accédez à l’onglet Vérifications. Vous devriez voir une nouvelle suite de vérifications pour Octo RuboCop. Mais cette fois, il ne devrait y avoir aucune erreur, car RuboCop les a toutes corrigées.
Exemple de code complet
Voici à quoi doit ressembler le code final dans server.rb
, une fois que vous avez suivi toutes les étapes de ce tutoriel. Le code comporte également des commentaires qui fournissent un contexte supplémentaire.
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
Étapes suivantes
Vous devez maintenant disposer d’une application qui reçoit des événements d’API, crée des exécutions de vérification, utilise RuboCop pour rechercher les erreurs Ruby, crée des annotations dans une demande de tirage et corrige automatiquement les erreurs de linter. Vous pouvez à présent développer le code de votre application, déployer celle-ci et la rendre publique.
Si vous avez des questions, démarrez une discussion GitHub Community dans la catégorie API et Webhooks.
Modifier le code de l’application
Ce tutoriel a montré comment créer un bouton « Corriger cela » qui est toujours affiché dans les demandes de tirage dans le dépôt. Essayez de mettre à jour le code pour afficher le bouton « Corriger cela » uniquement quand RuboCop détecte des erreurs.
Si vous préférez que RuboCop ne commite pas les fichiers directement dans la branche principale, mettez à jour le code pour, à la place, créer une demande de tirage avec une nouvelle branche qui est basée sur la branche principale.
Déployer votre application
Ce tutoriel a montré comment développer votre application localement. Lorsque vous êtes prêt à déployer votre application, vous devez apporter des modifications pour servir votre application et sécuriser les informations d’identification de votre application. Les étapes que vous effectuez dépendent du serveur que vous utilisez, mais les sections suivantes offrent des conseils généraux.
Héberger votre application sur un serveur
Ce tutoriel a utilisé votre ordinateur ou codespace comme serveur. Une fois que l’application est prête pour être utilisée en production, vous devez déployer votre application sur un serveur dédié. Par exemple, vous pouvez utiliser Azure App Service.
Mettre à jour l’URL du webhook
Une fois que vous disposez d’un serveur configuré pour recevoir le trafic webhook à partir de GitHub, mettez à jour l’URL du webhook dans les paramètres de votre application. Vous ne devez pas utiliser Smee.io pour transférer vos webhooks en production.
Mettre à jour le paramètre :port
Quand vous déployez votre application, vous souhaitez changer le port d’écoute de votre serveur. Le code indique déjà à votre serveur d’écouter toutes les interfaces réseau disponibles en définissant :bind
sur 0.0.0.0
.
Par exemple, vous pouvez définir une variable PORT
dans votre fichier .env
sur votre serveur pour indiquer le port d’écoute probable de votre serveur. Ensuite, vous pouvez mettre à jour l’emplacement où votre code définit :port
afin que votre serveur écoute sur votre port de déploiement :
set :port, ENV['PORT']
set :port, ENV['PORT']
Sécuriser les informations d’identification de votre application
Vous ne devez jamais rendre public la clé privée ou le secret du webhook de votre application. Ce tutoriel a stocké les informations d’identification de votre application dans un fichier gitignored .env
. Quand vous déployez votre application, choisissez un moyen sécurisé de stocker les informations d'identification et de mettre à jour votre code pour obtenir la valeur en conséquence. Par exemple, vous pouvez stocker les informations d’identification avec un service de gestion des secrets comme Azure Key Vault. Lorsque votre application s’exécute, elle peut récupérer les informations d’identification et les stocker dans des variables d’environnement sur le serveur sur lequel votre application est déployée.
Pour plus d’informations, consultez « Meilleures pratiques pour la création d’une application GitHub ».
Partager vos données
Si vous souhaitez partager votre application avec d’autres utilisateurs et organisations, rendez-la publique. Pour plus d’informations, consultez « Rendre une application GitHub publique ou privée ».
Suivre les bonnes pratiques
Vous devez vous efforcer de suivre les bonnes pratiques avec votre GitHub App. Pour plus d’informations, consultez « Meilleures pratiques pour la création d’une application GitHub ».