Einführung
In diesem Tutorial wird veranschaulicht, wie du einen CI-Server (Continuous Integration) erstellst, der Tests für neuen Code ausführt, der in ein Repository gepusht wird. In diesem Tutorial wird gezeigt, wie du eine GitHub App erstellst und konfigurierst, damit sie als Server fungiert, der check_run
- und check_suite
-Webhookereignisse mit der REST-API von GitHub empfängt und darauf reagiert.
In diesem Tutorial verwendest du deinen Computer oder Codespace als Server, während du deine App entwickelst. Sobald die App für die Verwendung in der Produktion bereit ist, solltest du deine App auf einem dedizierten Server bereitstellen.
In diesem Tutorial wird Ruby verwendet, du kannst jedoch jede Programmiersprache verwenden, die du auf deinem Server ausführen kannst.
Dieses Tutorial ist in zwei Teile unterteilt:
- Im ersten Teil erfährst du, wie du das Framework für einen CI-Server mit der REST-API von GitHub einrichtest, neue Überprüfungsausführungen für CI-Tests erstellst, wenn ein Repository neu gepushte Commits empfängt, und Überprüfungsausführungen erneut ausführst, wenn ein Benutzer diese Aktion für GitHub anfordert.
- Im zweiten Teil fügst du deinem CI-Test eine Funktion hinzu, indem du deinem CI-Server einen Lintertest hinzufügst. Außerdem erstellst du Anmerkungen, die auf den Registerkarten Überprüfungen und Geänderte Dateien eines Pull Requests angezeigt werden, und korrigierst automatisch Linterempfehlungen, indem du auf der Registerkarte Überprüfungen des Pull Request die Schaltfläche „Beheben“ verfügbar machst.
Informationen zu Continuous Integration (CI)
Bei der Softwarepraktik der CI erfolgen häufige Codecommits an ein gemeinsames Repository. Codecommits in kurzen Abständen tragen dazu bei, Fehler frühzeitiger aufzudecken, und verringern die Codemenge, die ein Entwickler auf der Suche nach der Fehlerursache debuggen muss. Durch häufige Code-Aktualisierungen lassen sich zudem Änderungen von verschiedenen Mitgliedern eines Software-Entwicklungsteams leichter zusammenführen. Dies bedeutet einen erheblichen Vorteil für die Entwickler, die sich damit stärker auf das Schreiben des Codes konzentrieren können, statt Fehler debuggen oder Mergekonflikte beheben zu müssen.
Auf einem CI-Server wird Code zum Ausführen von CI-Tests wie Code-Linter (zum Überprüfen der Formatvorlagenformatierung), Sicherheitsüberprüfungen, Code Coverage und andere Überprüfungen bei neuen Codecommits in einem Repository gehostet. CI-Server können auch Code für Staging- oder Produktionsserver erstellen und bereitstellen. Beispiele für die Arten von CI-Tests, die du mit einer GitHub App erstellen kannst, findest du in den Continuous Integration-Apps, die im GitHub Marketplace verfügbar sind.
Informationen zu Überprüfungen
Mit der REST-API von GitHub kannst du CI-Tests (Überprüfungen) einrichten, die automatisch für jeden Codecommit in einem Repository ausgeführt werden. Die API meldet detaillierte Informationen zu jeder Überprüfung auf der Registerkarte Überprüfungen des Pull Requests auf GitHub. Du kannst Überprüfungen in einem Repository verwenden, um zu bestimmen, wann mit einem Codecommit Fehler verursacht werden.
Zu den Überprüfungen gehören Überprüfungsausführungen, Überprüfungssammlungen und Commitstatus.
- Eine Überprüfungsausführung ist ein einzelner CI-Test, der mit einem Commit ausgeführt wird.
- Eine Überprüfungssammlung ist eine Gruppe von Überprüfungsausführungen.
- Ein Commitstatus gibt den Status eines Commits an, zum Beispiel
error
,failure
,pending
odersuccess
, und ist in einem Pull Request auf GitHub sichtbar. Sowohl Überprüfungssammlungen und Überprüfungsausführungen enthalten Commitstatus.
GitHub erstellt mithilfe des Standardflows automatisch check_suite
-Ereignisse für neue Codecommits in einem Repository. Du kannst die Standardeinstellungen jedoch ändern. Weitere Informationen findest du unter REST-API-Endpunkte für Prüfsuiten. Und so sieht der Standardablauf aus:
- Wenn jemand Code in das Repository pusht, sendet GitHub das
check_suite
-Ereignis automatisch mit einer Aktion vonrequested
an alle GitHub Apps, die im Repository installiert sind, die über diechecks:write
-Berechtigung verfügen. Dieses Ereignis teilt den Apps mit, dass Code in das Repository gepusht wurde und dass GitHub automatisch eine neue Überprüfungssammlung erstellt hat. - Wenn deine App dieses Ereignis empfängt, kann sie dieser Sammlung Überprüfungsausführungen hinzufügen.
- Deine Überprüfungsausführungen können Anmerkungen enthalten, die in bestimmten Codezeilen angezeigt werden. Anmerkungen werden auf der Registerkarte Überprüfungen angezeigt. Wenn Sie eine Anmerkung für eine Datei erstellen, die Teil des Pull Requests ist, werden die Anmerkungen auch auf der Registerkarte Geänderte Dateien angezeigt. Weitere Informationen finden Sie im
annotations
-Objekt unter „REST-API-Endpunkte für Überprüfungsausführungen“.
Weitere Informationen zu Überprüfungen findest du unter REST-API-Endpunkte für Prüfungen und Verwenden der REST-API zur Interaktion mit Überprüfungen.
Voraussetzungen
In diesem Tutorial wird vorausgesetzt, dass du über grundlegende Kenntnisse der Ruby-Programmiersprache verfügst.
Bevor du anfängst, solltest du dich mit den folgenden Konzepten vertraut machen:
Überprüfungen können auch mit der GraphQL-API verwendet werden, der Fokus dieses Tutorials liegt jedoch auf der REST-API. Weitere Informationen zu den GraphQL-Objekten findest du unter Überprüfungssammlung und Überprüfungsausführung in der GraphQL-Dokumentation.
Einrichten
Die folgenden Abschnitte führen dich durch die Einrichtung der folgenden Komponenten:
- ein Repository zum Speichern des Codes für deine App
- eine Möglichkeit zum lokalen Empfang von Webhooks
- eine GitHub App, die die Webhookereignisse „Überprüfungssammlung“ und „Überprüfungsausführung“ abonniert hat, über die Schreibberechtigung für Überprüfungen verfügt und eine Webhook-URL verwendet, die du lokal empfangen kannst
Erstellen eines Repositorys zum Speichern von Code für deine GitHub App
-
Erstelle ein Repository zum Speichern des Codes für deine App. Weitere Informationen findest du unter Ein neues Repository erstellen.
-
Klone dein Repository aus dem vorherigen Schritt. Weitere Informationen findest du unter Ein Repository klonen. Du kannst einen lokalen Klon oder GitHub Codespaces verwenden.
-
Navigiere in einem Terminal zu dem Verzeichnis, in dem dein Klon gespeichert ist.
-
Erstelle eine Ruby-Datei mit dem Namen
server.rb
. Diese Datei enthält den gesamten Code für deine App. Du fügst dieser Datei später Inhalte hinzu. -
Wenn das Verzeichnis noch keine
.gitignore
-Datei enthält, füge eine.gitignore
-Datei hinzu. Du fügst dieser Datei später Inhalte hinzu. Weitere Informationen zu.gitignore
-Dateien findest du unter Ignorieren von Dateien. -
Erstelle eine Datei mit dem Namen
Gemfile
. In dieser Datei werden die Gem-Abhängigkeiten beschrieben, die dein Ruby-Code benötigt. Füge derGemfile
den folgenden Inhalt hinzu: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'
-
Erstelle eine Datei mit dem Namen
config.ru
. Diese Datei konfiguriert deinen Sinatra-Server für die Ausführung. Füge derconfig.ru
-Datei den folgenden Inhalt hinzu:Ruby require './server' run GHAapp
require './server' run GHAapp
Abrufen einer Webhook-Proxy-URL
Um deine App lokal zu entwickeln, kannst du eine Webhook-Proxy-URL verwenden, um Webhookereignisse von GitHub an deinen Computer oder Codespace weiterzuleiten. In diesem Tutorial wird „Smee.io“ verwendet, um eine Webhook-Proxy-URL bereitzustellen und Ereignisse weiterzuleiten.
-
Führe in einem Terminal den folgenden Befehl aus, um den Smee-Client zu installieren:
Shell npm install --global smee-client
npm install --global smee-client
-
Navigiere im Browser zu https://smee.io/.
-
Klicke auf Neuen Kanal starten.
-
Kopiere die vollständige URL unter „Webhook-Proxy-URL“.
-
Führe im Terminal den folgenden Befehl aus, um den Smee-Client zu starten: Ersetze
YOUR_DOMAIN
durch die Webhook-Proxy-URL, die du im vorherigen Schritt kopiert hast.Shell smee --url YOUR_DOMAIN --path /event_handler --port 3000
smee --url YOUR_DOMAIN --path /event_handler --port 3000
Es sollte eine Ausgabe wie die folgende angezeigt werden:
Forwarding https://smee.io/YOUR_DOMAIN to http://127.0.0.1:3000/event_handler Connected https://smee.io/YOUR_DOMAIN
Durch den Befehl smee --url https://smee.io/YOUR_DOMAIN
wird Smee veranlasst, alle vom Smee-Kanal empfangenen Webhookereignisse an den Smee-Client weiterzuleiten, der auf deinem Computer ausgeführt wird. Die --path /event_handler
-Option leitet Ereignisse an die /event_handler
-Route weiter. Die --port 3000
-Option gibt Port 3000 an. Dabei handelt es sich um den Port, auf den dein Server lauschen soll, wenn du im weiteren Verlauf des Tutorials mehr Code hinzufügst. Mithilfe von Smee muss der Computer nicht für das öffentliche Internet verfügbar sein, damit er Webhooks von GitHub empfangen kann. Du kannst auch die Smee-URL im Browser öffnen, um Webhooknutzlasten zu überprüfen, wenn sie eingehen.
Es empfiehlt sich, dieses Terminalfenster geöffnet zu lassen und die Verbindung mit Smee aufrechtzuerhalten, während du die restlichen Schritte in diesem Leitfaden ausführst. Obwohl du die Verbindung mit dem Smee-Client trennen und erneut herstellen kannst, ohne deine eindeutige Domäne zu verlieren, ist es einfacher, die Verbindung aufrechtzuerhalten und andere Befehlszeilenaufgaben in einem anderen Terminalfenster auszuführen.
Registrieren einer GitHub App
Für dieses Tutorial musst du eine GitHub App mit folgenden Eigenschaften registrieren:
- aktive Webhooks
- Verwendung einer Webhook-URL, die du lokal empfangen kannst
- hat die Repositoryberechtigung „Überprüfungen“
- hat die Webhookereignisse „Überprüfungssammlung“ und „Überprüfungsausführung“ abonniert
Die folgenden Schritte führen dich durch das Konfigurieren einer GitHub App mit diesen Einstellungen. Weitere Informationen zu GitHub App-Einstellungen findest du unter Registrieren einer GitHub-App.
- Klicke auf GitHub in der oberen rechten Ecke einer beliebigen Seite auf dein Profilfoto.
- Navigiere zu den Einstellungen für dein Konto.
- Klicken Sie bei einer App, die zu einem persönlichen Konto gehört, auf Einstellungen.
- Für eine App im Besitz einer Organisation:
- Klicke auf Deine Organisationen.
- Klicke dann rechts neben der Organisation auf Einstellungen.
- Klicke auf der linken Seitenleiste auf Entwicklereinstellungen.
- Klicke auf der linken Randleiste auf GitHub Apps .
- Klicke auf Neue GitHub-App.
- Gib unter „Name der GitHub-App“ einen Namen für deine App ein. Beispielsweise
USERNAME-ci-test-app
, wobeiUSERNAME
dein GitHub-Benutzername ist. - Gib unter „Homepage-URL“ eine URL für deine App ein. Du kannst beispielsweise die URL des Repositorys verwenden, das du erstellt hast, um den Code für deine App zu speichern.
- Überspringe die Abschnitte „Identifizieren und Autorisieren von Benutzern“ und „Nach der Installation“ dieses Tutorials.
- Stelle sicher, dass unter „Webhooks“ die Option Aktiv ausgewählt ist.
- Gib unter „Webhook-URL“ deine Webhook-Proxy-URL von früher ein. Weitere Informationen findest du unter Abrufen einer Webhook-Proxy-URL.
- Gib unter „Webhookgeheimnis“ eine zufällige Zeichenfolge ein. Dieses Geheimnis wird verwendet, um zu überprüfen, ob Webhooks von GitHub gesendet werden. Speichere diese Zeichenfolge. Du wirst sie später verwenden.
- Wähle unter „Repositoryberechtigungen“ neben „Überprüfungen“ die Option Lesen & Schreiben aus.
- Wähle unter „Ereignisse abonnieren“ die Optionen Überprüfungssammlung und Überprüfungsausführung aus.
- Wähle unter „Wo kann diese GitHub-App installiert werden?“ die Option Nur in diesem Konto aus. Du kannst dies später ändern, wenn du deine App veröffentlichen möchtest.
- Klicke auf GitHub-App erstellen.
Speichern der identifizierenden Informationen und Anmeldeinformationen deiner App
In diesem Tutorial erfährst du, wie du die Anmeldeinformationen deiner App und identifizierende Informationen als Umgebungsvariablen in einer .env
-Datei speichern kannst. Wenn du deine App bereitstellst, musst du die Art ändern, in der die Anmeldeinformationen gespeichert werden. Weitere Informationen findest du unter Bereitstellen deiner App.
Stelle sicher, dass du dich auf einem sicheren Computer befindest, bevor du diese Schritte ausführst, da du deine Anmeldeinformationen lokal speicherst.
-
Navigiere in deinem Terminal zu dem Verzeichnis, in dem dein Klon gespeichert ist.
-
Erstelle eine Datei namens
.env
auf der obersten Ebene dieses Verzeichnisses. -
Füge der
.gitignore
-Datei.env
hinzu. Dadurch verhinderst du, dass du versehentlich die Anmeldeinformationen deiner App committest. -
Füge der
.env
-Datei den folgenden Inhalt hinzu. ErsetzeYOUR_HOSTNAME
durch den Namen von Ihre GitHub Enterprise Server-Instance. Du aktualisierst die anderen Werte in einem späteren Schritt.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"
-
Navigiere zur Seite „Einstellungen“ für deine App:
-
Klicke auf GitHub in der oberen rechten Ecke einer beliebigen Seite auf dein Profilfoto.
-
Navigiere zu den Einstellungen für dein Konto.
- Klicken Sie bei einer App, die zu einem persönlichen Konto gehört, auf Einstellungen.
- Für eine App im Besitz einer Organisation:
- Klicke auf Deine Organisationen.
- Klicke dann rechts neben der Organisation auf Einstellungen.
-
Klicke auf der linken Seitenleiste auf Entwicklereinstellungen.
-
Klicke auf der linken Randleiste auf GitHub Apps .
-
Klicke neben dem Namen deiner App auf Bearbeiten.
-
-
Suche auf der Einstellungenseite deiner App neben „App-ID“ die App-ID für deine App.
-
Ersetze in deiner
.env
-DateiYOUR_APP_ID
durch die App-ID deiner App. -
Ersetze in deiner
.env
-DateiYOUR_WEBHOOK_SECRET
durch das Webhookgeheimnis für deine App. Wenn du dein Webhookgeheimnis vergessen hast, klicke unter „Webhookgeheimnis (optional)“ auf Geheimnis ändern. Gib ein neues Geheimnis ein, und klicke dann auf Änderungen speichern. -
Klicke auf der Einstellungenseite deiner App unter „Private Schlüssel“ auf Privaten Schlüssel generieren. Es wird eine private
.pem
-Schlüsseldatei auf deinen Computer heruntergeladen. -
Öffne die
.pem
-Datei mit einem Text-Editor, oder verwende den folgenden Befehl auf der Befehlszeile, um den Inhalt der Datei anzuzeigen:cat PATH/TO/YOUR/private-key.pem
. -
Kopiere den gesamten Inhalt der Datei, füge ihn als Wert von
GITHUB_PRIVATE_KEY
in deine.env
-Datei ein, und setze den gesamten Wert in Anführungszeichen.Hier siehst du eine env-Beispieldatei:
GITHUB_APP_IDENTIFIER=12345 GITHUB_WEBHOOK_SECRET=your webhook secret GITHUB_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- ... HkVN9... ... -----END RSA PRIVATE KEY-----"
Hinzufügen von Code für deine GitHub App
In diesem Abschnitt erfährst, wie du grundlegenden Vorlagencode für deine GitHub App hinzufügen und erklären kannst, was der Code tut. Später im Tutorial erfährst du, wie du diesen Code änderst und hinzufügst, um die Funktionalität deiner App zu erweitern.
Füge deiner server.rb
-Datei den folgenden Vorlagencode hinzu:
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
Im Rest dieses Abschnitts wird erläutert, was der Vorlagencode tut. Es gibt keine Schritte, die du in diesem Abschnitt ausführen musst. Wenn du bereits mit dem Vorlagencode vertraut bist, kannst du mit Starten des Servers fortfahren.
Verstehen des Vorlagencodes
Öffne die server.rb
-Datei in einem Text-Editor. In dieser Datei werden Kommentare angezeigt, die zusätzlichen Kontext für den Vorlagencode bereitstellen. Es empfiehlt sich, diese Kommentare sorgfältig zu lesen und sogar eigene Kommentare zu neuem Code hinzuzufügen, den du schreibst.
Unterhalb der Liste der erforderlichen Dateien ist der erste Code, der angezeigt wird, die class GHApp < Sinatra::Application
-Deklaration. Du schreibst den gesamten Code für die GitHub App innerhalb dieser Klasse. In den folgenden Abschnitten wird ausführlich erläutert, was der Code innerhalb dieser Klasse tut.
- Festlegen des Ports
- Lesen der Umgebungsvariablen
- Aktiviere die Protokollierung.
- Definieren eines
before
-Filters - Definieren des Routenhandlers
- Definieren der Hilfsmethoden
Festlegen des Ports
Das erste, was in der class GHApp < Sinatra::Application
-Deklaration angezeigt wird, ist set :port 3000
. Dadurch wird der Port, der beim Starten des Webservers verwendet wird, so festgelegt, dass er mit dem Port übereinstimmt, an den du deine Webhooknutzlasten in Abrufen einer Webhook-Proxy-URL umgeleitet hast.
# Sets the port that's used when starting the web server.
set :port, 3000
set :bind, '0.0.0.0'
Lesen der Umgebungsvariablen
Als Nächstes liest diese Klasse die drei Umgebungsvariablen, die du in Speichern der identifizierenden Informationen und Anmeldeinformationen deiner App festgelegt hast, und speichert sie in Variablen, die später verwendet werden können.
# 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']
Aktiviere die Protokollierung.
Als Nächstes wird ein Codeblock angezeigt, der die Protokollierung während der Entwicklung ermöglicht. Dies ist die Standardumgebung in Sinatra. Mit diesem Code wird die Protokollierung auf DEBUG
-Ebene aktiviert, sodass eine nützliche Ausgabe im Terminal angezeigt wird, während du die App entwickelst.
# Turn on Sinatra's verbose logging during development
configure :development do
set :logging, Logger::DEBUG
end
Definieren eines before
-Filters
In Sinatra werden before
-Filter verwendet, mit denen du Code vor dem Routenhandler ausführen kannst. Vom before
-Block in der Vorlage werden vier Hilfsmethoden aufgerufen: get_payload_request
, verify_webhook_signature
, authenticate_app
und authenticate_installation
. Weitere Informationen findest du in der Sinatra-Dokumentation unter Filter und Hilfsprogramme.
# 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
Jede dieser Hilfsmethoden wird später im Codeblock definiert, der mit helpers do
beginnt. Weitere Informationen findest du unter Definieren der Hilfsmethoden.
Unter verify_webhook_signature
ist der Code, der mit unless @payload
beginnt, eine Sicherheitsmaßnahme. Wenn ein Repositoryname mit einer Webhooknutzlast angegeben wird, wird mit diesem Code überprüft, ob der Repositoryname nur lateinische Buchstaben, Bindestriche und Unterstriche enthält. Dadurch wird sichergestellt, dass ein fehlerhafter Akteur nicht versucht, beliebige Befehle auszuführen oder falsche Repositorynamen einzuschleusen. Später überprüft die verify_webhook_signature
-Hilfsmethode im Codeblock, der mit helpers do
beginnt, auch eingehende Webhooknutzlasten als zusätzliche Sicherheitsmaßnahme.
Definieren eines Routenhandlers
Im Vorlagencode ist eine leere Route enthalten. Über diesen Code werden alle POST
-Anforderungen an die /event_handler
-Route verarbeitet. Hier fügst du später mehr Code hinzu.
post '/event_handler' do
end
Definieren der Hilfsmethoden
Vier Hilfsmethoden werden im before
-Block des Vorlagencodes aufgerufen. Der helpers do
-Codeblock definiert jede dieser Hilfsmethoden.
Verarbeiten der Webhooknutzlast
Mit der ersten Hilfsmethode, get_payload_request
, wird die Webhooknutzlast erfasst und in das JSON-Format konvertiert. Dadurch wird der Zugriff auf die Daten der Nutzlast deutlich erleichtert.
Überprüfen der Webhooksignatur
Mithilfe der zweiten Hilfsmethode, verify_webhook_signature
, wird die Überprüfung der Webhooksignatur durchgeführt, damit sichergestellt ist, dass das Ereignis von GitHub generiert wurde. Weitere Informationen zum Code in der verify_webhook_signature
-Hilfsmethode findest du unter Validierung von Webhook-Zustellung. Wenn die Webhooks sicher sind, werden mit dieser Methode alle am Terminal eingehenden Nutzlasten protokolliert. Der Protokollierungscode ist hilfreich, damit du dich vergewissern kannst, dass der Webserver funktioniert.
Authentifizieren als GitHub App
Mit der dritten Hilfsmethode (authenticate_app
) kann sich deine GitHub App authentifizieren, sodass sie ein Installationstoken anfordern kann.
Zum Tätigen von API-Aufrufen verwendest du die Octokit-Bibliothek. Wenn du etwas Interessantes mit dieser Bibliothek tust, musst du deine GitHub App authentifizieren. Weitere Informationen zur Octokit-Bibliothek findest du in der Octokit-Dokumentation.
GitHub Apps verfügt über drei Authentifizierungsmethoden:
- Authentifizieren als GitHub App mithilfe eines JSON-Webtokens (JWT)
- Authentifizieren als spezifische Installation einer GitHub App mithilfe eines Installationszugriffstokens
- Authentifizieren im Namen eines Benutzers In diesem Tutorial wird diese Authentifizierungsmethode nicht verwendet.
Im nächsten Abschnitt Authentifizieren als Installation erfährst du mehr über die Authentifizierung als Installation.
Die Authentifizierung als GitHub App ermöglicht dir verschiedene Aktionen:
- Du kannst Verwaltungsinformationen auf hoher Ebene über deine GitHub App abrufen.
- Du kannst Zugriffstoken für eine Installation der App anfordern.
Du würdest beispielsweise die Authentifizierung als GitHub App durchführen, um eine Liste der Konten (Organisationskonten und persönliche Konten) abzurufen, von denen die App installiert wurde. Mithilfe dieser Authentifizierungsmethode kannst du jedoch nicht besonders viel mit der API anfangen. Du musst eine Authentifizierung als Installation durchführen, um auf die Daten eines Repositorys zugreifen und Vorgänge im Auftrag der Installation ausführen zu können. Dazu musst du zuerst eine Authentifizierung als GitHub App durchführen, um ein Installationszugriffstoken anzufordern. Weitere Informationen findest du unter Informationen zur Authentifizierung mit einer GitHub-App.
Bevor du die Octokit.rb-Bibliothek zum Tätigen von API-Aufrufen verwenden kannst, musst du einen Octokit-Client initialisieren, der mithilfe der authenticate_app
-Hilfsmethode als GitHub App authentifiziert wurde.
# 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
Vom obigen Code wird ein JSON Web Token (JWT) generiert und (zusammen mit dem privaten Schlüssel der App) zum Initialisieren des Octokit-Clients verwendet. Von GitHub wird die Authentifizierung einer Anforderung durch Verifizierung des Tokens mit dem gespeicherten öffentlichen Schlüssel der App überprüft. Weitere Informationen zur Funktionsweise dieses Codes findest du unter Generieren eines JSON Web Token (JWT) für eine GitHub-App.
Authentifizieren als Installation
Die vierte und letzte Hilfsmethode, authenticate_installation
, initialisiert einen Octokit-Client, der als Installation authentifiziert ist, mit dem du authentifizierte Aufrufe an die API tätigen kannst.
Eine Installation bezieht sich auf ein beliebiges Benutzer- oder Organisationskonto, für das die App installiert wurde. Selbst wenn jemand der Anwendung Zugriff auf mehr als ein Repository in diesem Konto gewährt, zählt dies nur als eine Installation, da sie innerhalb desselben Kontos erfolgt.
# 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
Mit der Octokit-Methode create_app_installation_access_token
wird ein Installationstoken erstellt. Weitere Informationen findest du in der Octokit-Dokumentation unter create_installation_access_token.
Diese Methode akzeptiert zwei Argumente:
- Installation (ganze Zahl): Die ID einer GitHub App-Installation
- Optionen (Hash, Standardwert ist
{}
): anpassbare Optionen
Jedes Mal, wenn eine GitHub App einen Webhook empfängt, ist ein installation
-Objekt mit einer id
darin enthalten. Mit dem Client, der als GitHub App authentifiziert wurde, übergibst du diese ID an die Methode create_app_installation_access_token
, um ein Zugriffstoken für jede einzelne Installation zu generieren. Da du keine Optionen an die Methode übergibst, werden die Optionen standardmäßig einem leeren Hash zugewiesen. Die Antwort für create_app_installation_access_token
enthält zwei Felder: token
und expired_at
. Vom Vorlagencode wird das Token in der Antwort ausgewählt und ein Installationsclient initialisiert.
Bei dieser Methode wird jedes Mal, wenn die App eine neue Webhooknutzlast empfängt, ein Client für die Installation erstellt, die das Ereignis ausgelöst hat. Mit diesem Authentifizierungsprozess kann die GitHub App für alle Installationen eines beliebigen Kontos genutzt werden.
Starten des Servers
Die App hat noch keine eigentliche Funktion, aber an diesem Punkt kannst du die App auf dem Server ausführen.
-
Stelle in deinem Terminal sicher, dass Smee weiterhin ausgeführt wird. Weitere Informationen findest du unter Abrufen einer Webhook-Proxy-URL.
-
Öffne eine neue Registerkarte in deinem Terminal und
cd
in das Verzeichnis, in dem du das Repository geklont hast, das du zuvor im Tutorial erstellt hast. Weitere Informationen findest du unter Erstellen eines Repositorys zum Speichern von Code für deine GitHub-App. Vom Ruby-Code in diesem Repository wird ein Sinatra-Webserver gestartet. -
Installiere die Abhängigkeiten, indem du die folgenden beiden Befehle nacheinander ausführst:
Shell gem install bundler
gem install bundler
Shell bundle install
bundle install
-
Starte nach der Installation der Abhängigkeiten den Server, indem du den folgenden Befehl ausführst:
Shell bundle exec ruby server.rb
bundle exec ruby server.rb
Du solltest eine Rückgabe wie diese erhalten:
> == 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
Wenn ein Fehler angezeigt wird, vergewissere dich, dass du die
.env
-Datei in dem Verzeichnis erstellt hast, das die Dateiserver.rb
enthält. -
Navigiere zum Testen des Servers in deinem Browser zu
http://localhost:3000
.Wenn eine Fehlerseite mit der Aufschrift „Sinatra kennt diesen Ditty nicht“ angezeigt wird, funktioniert die App wie erwartet. Zwar wird eine Fehlerseite angezeigt, aber eben eine Sinatra-Fehlerseite. Das bedeutet, dass die App wie erwartet mit dem Server verbunden ist. Diese Meldung wird angezeigt, weil die App nichts anderes anzeigen kann.
Testen, dass der Server deine App lauscht
Du kannst testen, ob der Server die App überwacht, indem du ein zu empfangendes Ereignis auslöst. Dazu installierst du die App in einem Testrepository, das das installation
-Ereignis an deine App sendet. Wenn die App das Ereignis empfängt, solltest du eine Ausgabe auf der Terminalregisterkarte sehen, auf der du server.rb
ausführst.
-
Erstelle ein neues Repository zum Testen deines Tutorialcodes. Weitere Informationen findest du unter Ein neues Repository erstellen.
-
Installiere GitHub App für das soeben erstellte Repository. Weitere Informationen findest du unter Installieren einer eigenen GitHub-App. Wähle während des Installationsvorgangs Nur Repositorys auswählen aus, und wähle das Repository aus, das du im vorherigen Schritt erstellt hast.
-
Nachdem du auf Installieren geklickt hast, sieh dir die Ausgabe auf der Terminalregisterkarte an, auf der du
server.rb
ausführst. Die Ausgabe sollte in etwa wie folgt aussehen:> 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
Wenn die Ausgabe wie folgt angezeigt wird, bedeutet dies, dass deine App eine Benachrichtigung erhalten hat, dass sie in deinem GitHub-Konto installiert wurde. Die App wird wie erwartet auf dem Server ausgeführt.
Wenn diese Ausgabe nicht angezeigt wird, vergewissere dich, dass Smee ordnungsgemäß auf einer anderen Terminalregisterkarte ausgeführt wird. Wenn du Smee neu starten musst, musst du auch die App deinstallieren und erneut installieren, um das
installation
-Ereignis noch mal an die App zu senden und die Ausgabe im Terminal anzuzeigen.
Wenn du dich fragst, woher die obige Terminalausgabe stammt: Sie wird im App-Vorlagencode geschrieben, dem du in Hinzufügen von Code für deine GitHub App zu server.rb
hinzugefügt hast.
Teil 1: Erstellen der API für Überprüfungen
In diesem Teil wird der für den Empfang von check_suite
-Webhookereignissen erforderliche Code hinzugefügt. Zudem werden Überprüfungsausführungen erstellt und aktualisiert. Darüber hinaus erfährst du auch, wie Überprüfungsausführungen erstellt werden, wenn eine Überprüfung auf GitHub erneut angefordert wurde. Am Ende dieses Abschnitts kannst du die Überprüfungsausführung anzeigen, die du in einem Pull Request auf GitHub erstellt hast.
In diesem Abschnitt werden mit der Überprüfungsausführung noch keine Überprüfungen für den Code durchgeführt. Diese Funktionalität wird erst in Teil 2: Erstellen eines CI-Tests hinzugefügt.
Du solltest bereits einen Smee-Kanal konfiguriert haben, über den Webhooknutzdaten an deinen lokalen Server weitergeleitet werden. Dein Server sollte ausgeführt werden und mit der GitHub App verbunden sein, die du in einem Testrepository registriert und installiert hast.
Folgende Schritte werden in Teil 1 durchgeführt:
- Hinzufügen von Ereignisbehandlung
- Erstellen einer Überprüfungsausführung
- Aktualisieren einer Überprüfungsausführung
Schritt 1.1. Hinzufügen von Ereignisbehandlung
Da du für deine App die Ereignisse Überprüfungssammlung und Überprüfungsausführung abonniert hast, werden nun die Webhooks check_suite
und check_run
empfangen. GitHub sendet Webhooknutzlasten als POST
-Anforderungen. Da du die Smee-Webhooknutzdaten an http://localhost:3000/event_handler
weitergeleitet hast, erhält der Server die POST
-Anforderungsnutzdaten auf der post '/event_handler'
-Route.
Öffne die server.rb
-Datei, die du in Code für deine GitHub App erstellt hast, und suche nach dem folgenden Code. Im Vorlagencode ist eine leere post '/event_handler'
-Route bereits enthalten. Die leere Route sieht wie folgt aus:
post '/event_handler' do
# ADD EVENT HANDLING HERE #
200 # success status
end
Füge in den Codeblock, der mit post '/event_handler' do
beginnt, an der Stelle, an der # ADD EVENT HANDLING HERE #
steht, den folgenden Code ein. Diese Route verarbeitet das check_suite
-Ereignis.
# 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
Alle Ereignisse, die von GitHub gesendet werden, enthalten den Anforderungsheader HTTP_X_GITHUB_EVENT
, der den Ereignistyp in der POST
-Anforderung angibt. Im Moment sind nur Ereignisse vom Typ check_suite
von Interesse, die immer dann gesendet werden, wenn eine neue Überprüfungssammlung erstellt wird. Alle Ereignisse enthalten ein zusätzliches action
-Feld, das die Aktion angibt, durch die die Ereignisse ausgelöst wurden. Bei check_suite
kann das action
-Feld requested
, rerequested
oder completed
angeben.
Mit der Aktion requested
wird jedes Mal, wenn Code in das Repository gepusht wird, eine Überprüfungsausführung angefordert, während mit der Aktion rerequested
angefordert wird, dass du für Code, der im Repository bereits vorhanden ist, erneut eine Überprüfung ausführst. Da für die beiden Aktionen requested
und rerequested
eine Überprüfungsausführung erstellt werden muss, rufe das Hilfsprogramm create_check_run
auf. Im nächsten Schritt wird diese Methode geschrieben.
Schritt 1.2. Erstellen einer Überprüfungsausführung
Füge diese neue Methode als Sinatra-Hilfsprogramm hinzu, wenn sie auch von anderen Routen verwendet werden soll.
Füge in den Codeblock, der mit helpers do
beginnt, an der Stelle, an der # ADD CREATE_CHECK_RUN HELPER METHOD HERE #
steht, den folgenden Code ein:
# 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
Mit diesem Code wird der Endpunkt POST /repos/{owner}/{repo}/check-runs
mithilfe der Octokit-Methode create_check_run aufgerufen. Weitere Informationen zum Endpunkt finden Sie unter „REST-API-Endpunkte für Überprüfungsausführungen“.
Zum Erstellen einer Überprüfungsausführung sind nur zwei Parameter erforderlich: name
und head_sha
. In diesem Code nennen wir die Überprüfungsausführung „Octo RuboCop“, da wir RuboCop verwenden, um den CI-Test später im Tutorial zu implementieren. Du kannst jedoch einen beliebigen Namen für die Überprüfung auswählen. Weitere Informationen zu RuboCop findest du in der RuboCop-Dokumentation.
Als Nächstes gibst du die für die grundlegende Funktionalität erforderlichen Parameter an. Du wirst die Überprüfungsausführung jedoch später aktualisieren, wenn du weitere Informationen zur Überprüfungsausführung sammelst. Standardmäßig legt GitHub den status
auf queued
fest.
Da in GitHub für eine bestimmte Commit-SHA-Komponente eine Überprüfungsausführung erstellt wird, ist head_sha
ein erforderlicher Parameter. Die Commit-SHA-Komponente befindet sich in den Webhooknutzdaten. Auch wenn du im Moment nur eine Überprüfungsausführung für das Ereignis check_suite
erstellst, ist es dennoch gut zu wissen, dass der Parameter head_sha
sowohl im Objekt check_suite
als auch im Objekt check_run
in den Ereignisnutzdaten enthalten ist.
Der obige Code verwendet zum Überprüfen, ob die Nutzdaten ein check_run
-Objekt enthalten, den ternären Operator, der wie eine if/else
-Anweisung funktioniert. Wenn das Objekt enthalten ist, wird der Parameter head_sha
über das Objekt check_run
gelesen, wenn nicht, über das Objekt check_suite
.
Testen des Codes
In den folgenden Schritten wird gezeigt, wie du testen kannst, ob der Code funktioniert und ob er erfolgreich eine neue Überprüfungsausführung erstellt.
-
Führe den folgenden Befehl aus, um den Server über dein Terminal neu zu starten. Wenn der Server bereits ausgeführt wird, gib zuerst
Ctrl-C
in dein Terminal ein, um den Server zu beenden, und führe dann den folgenden Befehl aus, um den Server erneut zu starten.Shell ruby server.rb
ruby server.rb
-
Erstelle einen Pull Request im Testrepository, das du unter Testen, dass der Server deine App lauscht erstellt hast. Dies ist das Repository, auf das du der App Zugriff gewährt hast.
-
Navigiere im gerade erstellten Pull Request zur Registerkarte Überprüfungen. Dann sollte eine Überprüfungsausführung mit dem Namen „Octo RuboCop“ oder dem Namen angezeigt werden, den du zuvor für die Überprüfung ausgewählt hast.
Wenn auf der Registerkarte Überprüfungen weitere Apps angezeigt werden, bedeutet das, dass im Repository weitere Apps mit der Berechtigung zum Lesen und Schreiben für Überprüfungen installiert sind, für die die Ereignisse Überprüfungssammlung und Überprüfungsausführung abonniert wurden. Dies kann auch bedeuten, dass du GitHub Actions-Workflows im Repository hast, die durch das pull_request
- oder pull_request_target
-Ereignis ausgelöst werden.
Bisher hast du GitHub angewiesen, eine Überprüfungsausführung zu erstellen. Der Status der Überprüfungsausführung im Pull Request wird in die Warteschlange gesetzt und mit einem gelben Symbol versehen. Im nächsten Schritt wartest du, bis GitHub die Überprüfungsausführung erstellt und den Status aktualisiert hat.
Schritt 1.3. Aktualisieren einer Überprüfungsausführung
Wenn deine create_check_run
-Methode ausgeführt wird, wird GitHub aufgefordert, eine neue Überprüfungsausführung zu erstellen. Nachdem die Überprüfungsausführung in GitHub erstellt wurde, erhältst du das check_run
-Webhookereignis mit der Aktion created
. Dieses Ereignis ist dein Signal, mit der Ausführung der Überprüfung zu beginnen.
Als Nächstes aktualisierst du den Ereignishandler so, dass damit nach der Aktion created
gesucht wird. Beim Aktualisieren des Ereignishandlers kannst du für die Aktion rerequested
eine Bedingung hinzufügen. Wenn ein einzelner Test in GitHub durch einen Klick auf die Schaltfläche „Erneut ausführen“ erneut ausgeführt wird, wird in GitHub das Ereignis rerequested
der Überprüfungsausführung an deine App gesendet. Wenn eine Überprüfungsausführung den Status rerequested
aufweist, beginne von vorn, und erstelle eine neue Überprüfungsausführung. Dazu fügst du eine Bedingung für das check_run
-Ereignis in die post '/event_handler'
-Route ein.
Füge in den Codeblock, der mit post '/event_handler' do
beginnt, an der Stelle, an der # ADD CHECK_RUN METHOD HERE #
steht, den folgenden Code ein:
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
In GitHub werden alle Ereignisse für Überprüfungsausführungen mit dem Status created
an alle Apps gesendet, die in einem Repository mit den erforderlichen Überprüfungsberechtigungen installiert sind. Das bedeutet, dass deine App Überprüfungsausführungen empfängt, die von anderen Apps erstellt wurden. Eine Überprüfungsausführung mit dem Status created
unterscheidet sich ein wenig von einer Überprüfungssammlung mit dem Status requested
oder rerequested
, die in GitHub nur an Apps gesendet werden, die aufgefordert sind, eine Überprüfung auszuführen. Mit dem obigen Code wird nach der Anwendungs-ID der Überprüfungsausführung gesucht. Dadurch werden alle Überprüfungsausführungen für andere Apps im Repository herausgefiltert.
Als Nächstes schreibst du die initiate_check_run
-Methode, mit der du den Ausführungsstatus aktualisierst und den Start deines CI-Tests vorbereitest.
In diesem Abschnitt wird der CI-Test noch nicht gestartet. Vielmehr wird hier der Status der Überprüfungsausführung von queued
in pending
und anschließend von pending
in completed
geändert, um den gesamten Ablauf einer Überprüfungsausführung anzuzeigen. In Teil 2: Erstellen des CI-Tests füge den Code hinzu, mit dem der CI-Test tatsächlich durchgeführt wird.
Zunächst erstellst du die Methode initiate_check_run
und aktualisierst den Status der Überprüfungsausführung.
Füge in den Codeblock, der mit helpers do
beginnt, an der Stelle, an der # ADD INITIATE_CHECK_RUN HELPER METHOD HERE #
steht, den folgenden Code ein:
# 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
Mit dem obigen Code wird der Endpunkt PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
mithilfe der Octokit-Methode update_check_run
aufgerufen und aktualisiert die bereits erstellte Überprüfungsausführung. Weitere Informationen zum Endpunkt finden Sie unter „REST-API-Endpunkte für Überprüfungsausführungen“.
Und so funktioniert dieser Code. Zunächst wird der Status der Überprüfungsausführung in in_progress
geändert und die started_at
-Zeit auf die aktuelle Uhrzeit festgelegt. In Teil 2 dieses Tutorials fügst du Code hinzu, mit dem unter ***** RUN A CI TEST *****
ein echter CI-Test gestartet wird. Im Moment lässt du diesen Abschnitt als Platzhalter stehen, sodass mit dem nachfolgenden Code nur der fehlerfreie Ablauf des CI-Prozesses und aller Tests simuliert wird. Abschließend wird mit dem Code der Status der Überprüfungsausführung in completed
geändert.
Wenn du die REST-API verwendest, um den Status der Überprüfungsausführung von completed
bereitzustellen, sind die Parameter conclusion
und completed_at
erforderlich. Unter conclusion
wird das Ergebnis einer Überprüfungsausführung zusammengefasst, die den Status success
, failure
, neutral
, cancelled
, timed_out
, skipped
oder action_required
aufweisen kann. Lege das Ergebnis („conclusion“) auf success
, die completed_at
-Zeit auf die aktuelle Uhrzeit und den Status auf completed
fest.
Du kannst darüber hinaus noch weitere Informationen zur Funktionsweise der Überprüfung bereitstellen. Das wird jedoch erst im nächsten Abschnitt behandelt.
Testen des Codes
In den folgenden Schritten wird gezeigt, wie du testen kannst, ob der Code und die neu erstellte Schaltfläche „Alle erneut ausführen“ funktionieren.
-
Führe den folgenden Befehl aus, um den Server über dein Terminal neu zu starten. Wenn der Server bereits ausgeführt wird, gib zuerst
Ctrl-C
in dein Terminal ein, um den Server zu beenden, und führe dann den folgenden Befehl aus, um den Server erneut zu starten.Shell ruby server.rb
ruby server.rb
-
Erstelle einen Pull Request im Testrepository, das du unter Testen, dass der Server deine App lauscht erstellt hast. Dies ist das Repository, auf das du der App Zugriff gewährt hast.
-
Navigiere im gerade erstellten Pull Request zur Registerkarte Überprüfungen. Die Schaltfläche „Alle erneut ausführen“ sollte angezeigt werden.
-
Klicke in der oberen rechten Ecke auf die Schaltfläche „Alle erneut ausführen“. Der Test sollte erneut ausgeführt werden und mit
success
enden.
Teil 2: Erstellen eines CI-Tests
Nachdem du die Schnittstelle zum Empfangen von API-Ereignissen und Überprüfungsausführungen erstellt hast, kannst du eine Überprüfungsausführung erstellen, mit der ein CI-Test implementiert wird.
RuboCop ist ein Ruby-Code-Linter und -Formatierer. Damit wird Ruby-Code überprüft, um sicherzustellen, dass er dem Ruby-Styleguide entspricht. Weitere Informationen findest du in der RuboCop-Dokumentation.
RuboCop erfüllt im Wesentlichen drei Funktionen:
- Linten zum Überprüfen des Codeformats
- Codeformatierung
- Ersetzen der nativen Ruby-Lintfunktionen mithilfe von
ruby -w
Mit deiner App wird RuboCop auf dem CI-Server ausgeführt, und es werden Überprüfungsausführungen (in diesem Fall CI-Tests) erstellt, mit denen die Ergebnisse angezeigt werden, die von RuboCop an GitHub gesendet werden.
Mit der REST-API kannst du umfangreiche Informationen zu den einzelnen Überprüfungsausführungen wie Status, Bilder, Zusammenfassungen, Anmerkungen und angeforderte Aktionen anzeigen.
Anmerkungen sind Informationen zu bestimmten Codezeilen in einem Repository. Mit einer Anmerkung kannst du die Teile des Codes genau festlegen und visualisieren, für die zusätzliche Informationen angezeigt werden sollen. Du kannst diese Informationen beispielsweise als Kommentar, Fehler oder Warnung in einer bestimmten Codezeile anzeigen. In diesem Tutorial werden Anmerkungen zum Visualisieren von RuboCop-Fehlern verwendet.
App-Entwicklerinnen können zum Erstellen von Schaltflächen auf der Registerkarte Überprüfungen von Pull Requests angeforderte Aktionen nutzen. Bei einem Klick auf eine dieser Schaltflächen wird für die Aktion requested_action
ein check_run
-Ereignis an die GitHub App gesendet. Die von der App verwendete Aktion kann vom App-Entwickler beliebig konfiguriert werden. In diesem Tutorial erfährst du, wie eine Schaltfläche hinzugefügt wird, mit der Benutzerinnen anfordern können, dass mit RuboCop alle gefundenen Fehler behoben werden. In RuboCop wird das automatische Beheben von Fehlern mithilfe einer Befehlszeilenoption unterstützt, und du konfigurierst die Aktion requested_action
so, dass diese Option verwendet werden kann.
Folgende Schritte werden in diesem Abschnitt durchgeführt:
- Hinzufügen einer Ruby-Datei
- Zulassen, dass RuboCop das Testrepository klonen kann
- Ausführen von RuboCop
- Sammeln von RuboCop-Fehlern
- Aktualisieren der Überprüfungsausführung mit CI-Testergebnissen
- Automatisches Beheben von RuboCop-Fehlern
Schritt 2.1. Hinzufügen einer Ruby-Datei
Du kannst einzelne Dateien oder ganze Verzeichnisse zum Überprüfen durch RuboCop übergeben. In diesem Tutorial führst du RuboCop in einem ganzen Verzeichnis aus. RuboCop überprüft nur Ruby-Code. Um deine GitHub App zu testen, musst du in deinem Repository eine Ruby-Datei hinzufügen, die Fehler enthält, damit RuboCop sie finden kann. Nachdem du deinem Repository die folgende Ruby-Datei hinzugefügt hast, aktualisiere deine CI-Überprüfung, um RuboCop für den Code auszuführen.
-
Navigiere zum Testrepository, das du unter Testen, dass der Server deine App lauscht erstellt hast. Dies ist das Repository, auf das du der App Zugriff gewährt hast.
-
Erstelle eine neue Datei mit dem Namen
myfile.rb
. Weitere Informationen findest du unter Neue Dateien erstellen. -
Füge folgenden Inhalt zu
myfile.rb
hinzu: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
-
Wenn du die Datei lokal erstellt hast, stelle sicher, dass du einen Commit ausführst und die Datei auf GitHub in dein Repository pushst.
Schritt 2.2. Zulassen, dass RuboCop das Testrepository klonen kann
RuboCop ist als Befehlszeilen-Hilfsprogramm verfügbar. Wenn du also RuboCop in einem Repository ausführen möchtest, musst deine GitHub App eine lokale Kopie des Repositorys auf den CI-Server klonen, damit RuboCop die Dateien analysieren kann. Dazu muss dein Code Git-Vorgänge ausführen können, und deine GitHub App muss über die richtigen Berechtigungen zum Klonen eines Repositorys verfügen.
Zulassen von Git-Vorgängen
Zum Ausführen von Git-Vorgängen in deiner Ruby-App kannst du das Gem ruby-git verwenden. Die in Einrichten erstellte Gemfile
enthält bereits das Gem „ruby-git“. Du hast es beim Ausführen von bundle install
in Starten des Servers installiert.
Füge nun oben in deiner server.rb
-Datei unter den anderen require
-Elementen den folgenden Code hinzu:
require 'git'
require 'git'
Aktualisieren deiner App-Berechtigungen
Als Nächstes musst du deine GitHub App-Berechtigungen aktualisieren. Zum Klonen eines Repositorys benötigt die App Leseberechtigungen für „Inhalte“. Später in diesem Tutorial ist die Schreibberechtigung erforderlich, um Inhalte per Push an GitHub zu übertragen. So aktualisierst du die Berechtigungen deiner App:
- Wähle auf der Seite der App-Einstellungen die App aus, und klicke in der Seitenleiste auf Berechtigungen und Ereignisse.
- Wähle unter „Repositoryberechtigungen“ neben „Inhalte“ die Option Lesen & Schreiben aus.
- Klicke unten auf der Seite auf Änderungen speichern.
- Wenn du die App in deinem Konto installiert hast, überprüfe deine E-Mail, und folge dem Link, um die neuen Berechtigungen zu akzeptieren. Wenn du die Berechtigungen oder Webhooks deiner App änderst, müssen Benutzer, die die App installiert haben (auch du selbst), die neuen Berechtigungen akzeptieren, bevor die Änderungen wirksam werden. Du kannst die neuen Berechtigungen auch akzeptieren, indem du zur Installationsseite navigierst. Unter dem App-Namen wird ein Link angezeigt, der dich darüber informiert, dass die App andere Berechtigungen erfordert. Klicken Sie zunächst auf Anforderung überprüfen und dann auf Neue Berechtigungen akzeptieren.
Hinzufügen von Code zum Klonen eines Repositorys
Wenn ein Repository geklont werden soll, verwendet der Code die Berechtigungen deiner GitHub App und das Octokit-SDK, um ein Installationstoken für deine App (x-access-token:TOKEN
) zu erstellen und es im folgenden Klonbefehl zu verwenden:
git clone https://x-access-token:TOKEN@github.com/OWNER/REPO.git
Mit dem obigen Befehl wird ein Repository über HTTP geklont. Dazu muss der vollständige Repositoryname angegeben werden, der den Repositorybesitzer (Benutzer oder Organisation) und den Repositorynamen umfasst. Der vollständige Name des Repositorys octocat Hello-World lautet beispielsweise octocat/hello-world
.
Öffne deine server.rb
-Datei. Füge in den Codeblock, der mit helpers do
beginnt, an der Stelle, an der # ADD CLONE_REPOSITORY HELPER METHOD HERE #
steht, den folgenden Code ein:
# 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
Im obigen Code wird das ruby-git
-Gem zum Klonen des Repositorys mithilfe des Installationstokens der App verwendet. Dadurch wird der Code im selben Verzeichnis wie server.rb
geklont. Zum Ausführen von Git-Befehlen im Repository muss der Code in das Repositoryverzeichnis wechseln. Vor dem Wechsel in ein anderes Verzeichnis wird mit dem Code das aktuelle Arbeitsverzeichnis in einer Variablen (pwd
) und damit die Stelle gespeichert, an die vor dem Beenden der Methode clone_repository
zurückgekehrt werden muss.
Mit dem Code werden die aktuellen Änderungen aus dem Repositoryverzeichnis abgerufen (@git.pull
) und zusammengeführt und die spezifische Git-Referenz ausgecheckt (@git.checkout(ref)
). Der Code für all diese Aufgaben lässt sich gut in einer eigenen Methode unterbringen. Zum Ausführen dieser Vorgänge muss für die Methode der Name und der vollständige Name des Repositorys und der Referenz angegeben werden, die ausgecheckt werden soll. Bei der Referenz kann es sich um eine Commit-SHA-Komponente, einen Branch oder ein Tag handeln. Wenn dies abgeschlossen ist, ändert der Code das Verzeichnis wieder in das ursprüngliche Arbeitsverzeichnis (pwd
).
Nun verfügst du über eine Methode, mit der ein Repository geklont und eine Referenz ausgecheckt wird. Als Nächstes musst du Code hinzufügen, um die erforderlichen Eingabeparameter abzurufen und die neue clone_repository
-Methode aufzurufen.
Füge in den Codeblock, der mit helpers do
beginnt, in der initiate_check_run
-Hilfsmethode an der Stelle, an der # ***** RUN A CI TEST *****
steht, den folgenden Code ein:
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 #
Mit dem obigen Code werden der vollständige Repositoryname und der Parameter „head_SHA“ des Commits aus den check_run
-Webhooknutzdaten abgerufen.
Schritt 2.3. Ausführen von RuboCop
Bisher klont dein Code das Repository und erstellt Überprüfungsausführungen mit deinem CI-Server. Jetzt geht es an die Feinheiten des RuboCop-Linters und der Überprüfungsanmerkungen.
Zunächst fügst du Code zum Ausführen von RuboCop hinzu und speicherst die Formatcodefehler im JSON-Format.
Suche im Codeblock, der mit helpers do
beginnt, die initiate_check_run
-Hilfsmethode. Füge in dieser Hilfsmethode unter clone_repository(full_repo_name, repository, head_sha)
, an der Stelle, an der # ADD CODE HERE TO RUN RUBOCOP #
steht, den folgenden Code hinzu:
# 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 #
Mit dem obigen Code wird RuboCop für alle Dateien im Verzeichnis des Repositorys ausgeführt. Mit der Option --format json
kann eine Kopie der Lintingergebnisse in einem Format gespeichert werden, das vom Computer analysiert werden kann. Weitere Informationen und ein Beispiel für das JSON-Format findest du unter JSON-Formatierer in der RuboCop-Dokumentation. Diese Code parst auch den JSON-Code, sodass du mithilfe der @output
-Variablen problemlos auf die Schlüssel und Werte in deiner GitHub App zugreifen kannst.
Nachdem du RuboCop ausgeführt und die Lintingergebnisse gespeichert hast, führt dieser Code den Befehl rm -rf
aus, um den Checkout des Repositorys zu entfernen. Da mit dem Code die RuboCop-Ergebnisse in einer @report
-Variablen gespeichert werden, kann das Auschecken des Repositorys problemlos übersprungen werden.
Der rm -rf
-Befehl kann nicht rückgängig gemacht werden. Um die Sicherheit deiner App zu gewährleisten, überprüft der Code in diesem Tutorial eingehende Webhooks auf eingeschleuste bösartige Befehle, die dazu verwendet werden könnten, ein anderes Verzeichnis zu entfernen, als von deiner App beabsichtigt. Wenn beispielsweise eine böswilliger Akteur*in einen Webhook mit dem Repositorynamen ./
sendet, entfernt deine App das Stammverzeichnis. Die verify_webhook_signature
-Methode überprüft den Absender des Webhooks. Der verify_webhook_signature
-Ereignishandler überprüft auch, ob der Repositoryname gültig ist. Weitere Informationen findest du unter Definieren eines before
-Filters.
Testen des Codes
Die folgenden Schritte zeigen dir, wie du testen kannst, ob der Code funktioniert und wie du die von RuboCop gemeldeten Fehler anzeigen kannst.
-
Führe den folgenden Befehl aus, um den Server über dein Terminal neu zu starten. Wenn der Server bereits ausgeführt wird, gib zuerst
Ctrl-C
in dein Terminal ein, um den Server zu beenden, und führe dann den folgenden Befehl aus, um den Server erneut zu starten.Shell ruby server.rb
ruby server.rb
-
Erstelle im Repository, in dem du die
myfile.rb
-Datei hinzugefügt hast, einen neuen Pull Request. -
Auf der Terminalregisterkarte, auf der der Server ausgeführt wird, sollte die Debugausgabe mit Lintingfehlern angezeigt werden. Die Lintingfehler werden ohne Formatierung ausgegeben. Du kannst deine Debugausgabe kopieren und in ein Webtool wie den JSON-Formatierer einfügen, um deine JSON-Ausgabe wie im folgenden Beispiel zu formatieren:
{ "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 } }
Schritt 2.4. Sammeln von RuboCop-Fehlern
Die @output
-Variable enthält die analysierten JSON-Ergebnisse des RuboCop-Berichts. Wie in der Beispielausgabe im vorigen Schritt gezeigt, enthalten die Ergebnisse den Abschnitt summary
, mit dem dein Code schnell ermitteln kann, ob Fehler vorhanden sind. Mit dem folgenden Code wird das Ergebnis der Überprüfungsausführung auf success
festgelegt, wenn keine Fehler gemeldet werden. RuboCop meldet Fehler für jede Datei im files
-Array. Wenn Fehler vorhanden sind, musst du einige Daten aus dem Dateiobjekt extrahieren.
Mit den REST-API-Endpunkten zum Verwalten von Überprüfungsausführungen kannst du Anmerkungen für bestimmte Codezeilen erstellen. Wenn du eine Überprüfungsausführung erstellst oder aktualisierst, kannst du Anmerkungen hinzufügen. In diesem Tutorial aktualisieren Sie die Überprüfungsausführung mit Anmerkungen, indem sie den Endpunkt PATCH /repos/{owner}/{repo}/check-runs/{check_run_id}
verwenden. Weitere Informationen zum Endpunkt finden Sie unter „REST-API-Endpunkte für Überprüfungsausführungen“.
Bei der API ist die Anzahl der Anmerkungen auf maximal 50 pro Anforderung begrenzt. Wenn du mehr als 50 Anmerkungen erstellen möchtest, musst du mehrere Anforderungen für den Endpunkt „Update a check run“ erstellen. Um beispielsweise 105 Anmerkungen zu erstellen, musst du drei separate Anforderungen an die API stellen. Dabei umfassen die ersten beiden Anforderungen jeweils 50 Anmerkungen, während die dritte Anforderung die fünf verbleibenden Anmerkungen enthält. Jedes Mal, wenn du die Überprüfungsausführung aktualisierst, werden an die für die Überprüfungsausführung bereits vorhandene Liste mit Anmerkungen weitere Anmerkungen angefügt.
Für eine Überprüfungsausführung müssen Anmerkungen in Form von Objektarrays vorliegen. Jedes Anmerkungsobjekt muss path
, start_line
, end_line
, annotation_level
und message
enthalten. RuboCop stellt auch start_column
und end_column
bereit, sodass du diese optionalen Parameter in die Anmerkung einschließen kannst. In Anmerkungen dürfen die Parameter start_column
und end_column
nur in einer Zeile verwendet werden. Weitere Informationen findest du im annotations
-Objekt unter REST-API-Endpunkte für Überprüfungsausführungen.
Nun fügst du Code hinzu, um die erforderlichen Informationen aus RuboCop zu extrahieren, die zum Erstellen jeder Anmerkung erforderlich sind.
Füge unter dem Code, den du im vorherigen Schritt hinzugefügt hast, an der Stelle, wo # ADD ANNOTATIONS CODE HERE #
steht, den folgenden Code hinzu:
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 #
Mit diesem Code wird die Gesamtzahl der Anmerkungen auf 50 beschränkt. Du kannst diesen Code jedoch ändern, sodass die Überprüfungsausführung für alle Batches mit 50 Anmerkungen aktualisiert wird. Der obige Code enthält die Variable max_annotations
, mit der Grenzwert auf 50 festgelegt wird. Dieser Wert wird in der Schleife verwendet, die für die Verletzungen durchlaufen wird.
Wenn offense_count
null ist, ist der CI-Test ein success
. Wenn Fehler vorhanden sind, wird das Ergebnis vom Code auf neutral
festgelegt, um die strikte Erzwingung von Fehlern über Code-Linter zu verhindern. Du kannst das Ergebnis jedoch in failure
ändern, wenn du sicherstellen möchtest, dass bei der Überprüfungssammlung ein Fehler auftritt, wenn Lintingfehler vorhanden sind.
Wenn Fehler gemeldet werden, durchläuft der obige Code das files
-Array im RuboCop-Bericht. Für jede Datei wird der Dateipfad extrahiert und die Anmerkungsebene auf notice
festgelegt. Du kannst noch einen Schritt weitergehen und für jeden RuboCop-Cop-Typ eine Warnstufe festlegen. Damit dieses Tutorial jedoch nicht zu kompliziert wird, werden alle Fehler auf die Ebene notice
festgelegt.
Dieser Code durchläuft darüber hinaus alle Fehler im offenses
-Array und erfasst den Ort der Verletzung und die Fehlermeldung. Nach dem Extrahieren der erforderlichen Informationen erstellt der Code eine Anmerkung für jeden Fehler und speichert sie im annotations
-Array. Da in Anmerkungen Start- und Endspalten nur in einer Zeile verwendet werden dürfen, werden die Parameter start_column
und end_column
dem annotation
-Objekt nur dann hinzugefügt, wenn die Werte der Anfangs- und Endzeile identisch sind.
Mit dem Code wird noch keine Anmerkung für die Überprüfungsausführung erstellt. Der entsprechende Code wird im nächsten Abschnitt erstellt.
Schritt 2.5. Aktualisieren der Überprüfungsausführung mit CI-Testergebnissen
Alle Überprüfungsausführungen in GitHub enthalten ein output
-Objekt, das die Parameter title
, summary
, text
, annotations
und images
enthält. summary
und title
sind die einzigen Parameter, die für das output
-Objekt erforderlich sind. Diese allein bieten jedoch nicht viele Details. Daher werden in diesem Tutorial auch die Parameter text
und annotations
hinzugefügt.
Für den Parameter summary
werden in diesem Beispiel Zusammenfassungsinformationen von RuboCop verwendet. Zudem wird die Ausgabe durch Hinzufügen einiger neuer Zeilen (\n
) formatiert. Alles, was du dem Parameter text
hinzufügst, kannst du anpassen. In diesem Beispiel wird der Parameter text
jedoch auf die RuboCop-Version festgelegt. Der folgende Code legt summary
und text
fest.
Füge unter dem Code, den du im vorherigen Schritt hinzugefügt hast, an der Stelle, wo # ADD CODE HERE TO UPDATE CHECK RUN SUMMARY #
steht, den folgenden Code hinzu:
# 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']}"
Nun sollte dein Code alle Informationen enthalten, die er zum Aktualisieren der Überprüfungsausführung benötigt. In Schritt 1.3.: Aktualisieren einer Überprüfungsausführung hast du Code hinzugefügt, um den Status der Überprüfungsausführung auf success
festzulegen. Du musst diesen Code aktualisieren, damit die conclusion
-Variable verwendet wird, die du basierend auf den RuboCop-Ergebnissen (auf neutral
oder success
) festgelegt hast. Hier siehst du den Code, den du deiner server.rb
-Datei zuvor hinzugefügt hast:
# 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'
)
Ersetze den Code durch den folgenden Code:
# 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'
)
Nachdem dein Code basierend auf dem Status des CI-Tests ein Ergebnis festgelegt und die Ausgabe aus den RuboCop-Ergebnissen hinzugefügt hat, hast du einen CI-Test erstellt.
Mit dem obigen Code wird deinem CI-Server das Feature „Angeforderte Aktionen“ über das actions
-Objekt hinzugefügt. Weitere Informationen findest du unter Anfordern weiterer Aktionen aus einer Überprüfungsausführung. Mit angeforderten Aktionen wird auf der Registerkarte Überprüfungen auf GitHub eine Schaltfläche hinzugefügt, über die angefordert werden kann, dass mit der Überprüfungsausführung eine weitere Aktion durchgeführt wird. Diese zusätzliche Aktion kann von deiner App umfassend konfiguriert werden. Da RuboCop beispielsweise über ein Feature verfügt, um die im Ruby-Code gefundenen Fehler automatisch zu beheben, kann dein CI-Server eine Schaltfläche für angeforderte Aktionen enthalten, damit Benutzer*innen automatische Fehlerkorrekturen anfordern können. Wenn ein Benutzer auf die Schaltfläche klickt, empfängt die App das check_run
-Ereignis mit einer requested_action
-Aktion. Jede angeforderte Aktion weist den Parameter identifier
auf, der von der App verwendet wird, um zu ermitteln, auf welche Schaltfläche geklickt wurde.
Mit dem obigen Code werden noch keine RuboCop-Fehler automatisch behoben. Du fügst dies später im Tutorial hinzu.
Testen des Codes
Die folgenden Schritte zeigen dir, wie du testen kannst, ob der Code funktioniert und wie du den gerade erstellten CI-Test anzeigen kannst.
-
Führe den folgenden Befehl aus, um den Server über dein Terminal neu zu starten. Wenn der Server bereits ausgeführt wird, gib zuerst
Ctrl-C
in dein Terminal ein, um den Server zu beenden, und führe dann den folgenden Befehl aus, um den Server erneut zu starten.Shell ruby server.rb
ruby server.rb
-
Erstelle im Repository, in dem du die
myfile.rb
-Datei hinzugefügt hast, einen neuen Pull Request. -
Navigiere im soeben erstellten Pull Request zur Registerkarte Überprüfungen. Für jeden Fehler, den RuboCop gefunden hat, sollten Anmerkungen angezeigt werden. Beachte auch die Schaltfläche „Problem beheben“, die du erstellt hast, indem du eine angeforderte Aktion hinzugefügt hast.
Schritt 2.6. Automatisches Beheben von RuboCop-Fehlern
Bisher hast du einen CI-Test erstellt. In diesem Abschnitt füge ein weiteres Feature hinzu, das RuboCop verwendet, um die gefundenen Fehler automatisch zu beheben. Du hast bereits die Schaltfläche „Problem beheben“ in Schritt 2.5.: Aktualisieren der Überprüfungsausführung mit CI-Testergebnissen hinzugefügt. Im Folgenden füge den Code hinzu, mit dem das Ereignis requested_action
der Überprüfungsausführung behandelt wird, das beim Klicken auf die Schaltfläche „Problem beheben“ ausgelöst wird.
Das RuboCop-Tool enthält die Befehlszeilenoption --auto-correct
zum automatischen Beheben von gefundenen Fehlern. Weitere Informationen findest du in der RuboCop-Dokumentation unter Autokorrektur von Verstößen. Wenn du das Feature --auto-correct
verwendest, werden die Updates auf die lokalen Dateien auf dem Server angewendet. Du musst die Änderungen an GitHub pushen, nachdem RuboCop die Korrekturen vorgenommen hat.
Zum Pushen an ein Repository muss deine App über Schreibberechtigungen für „Inhalte“ in einem Repository verfügen. Du hast diese Berechtigung bereits im Schritt 2.2.: RuboCop das Klonen des Test-Repositorys erlauben auf Lesen & schreiben festgelegt.
Damit Dateien committet werden können, muss Git wissen, welcher Benutzername und welche E-Mail-Adresse mit dem Commit verknüpft werden muss. Als Nächstes fügst du Umgebungsvariablen hinzu, um den Namen und die E-Mail-Adresse zu speichern, die deine App beim Erstellen von Git-Commits verwendet.
-
Öffne die zuvor in diesem Tutorial erstellte
.env
-Datei. -
Füge deiner
.env
-Datei die folgenden Umgebungsvariablen hinzu. ErsetzeAPP_NAME
durch den Namen deiner App undEMAIL_ADDRESS
durch alle E-Mails, die du für dieses Beispiel verwenden möchtest.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"
Als Nächstes musst du Code hinzufügen, um die Umgebungsvariablen zu lesen und die Git-Konfiguration festzulegen. Diesen Code füge in Kürze hinzu.
Wenn eine Benutzerin auf die Schaltfläche „Problem beheben“ klickt, empfängt deine App den Webhook der Überprüfungsausführung mit dem Aktionstyp requested_action
.
In Schritt 1.3. Aktualisierung einer Überprüfungsausführung hast du den event_handler
in deiner server.rb
-Datei aktualisiert, um nach Aktionen im check_run
-Ereignis zu suchen. Du verfügst bereits über eine CASE-Anweisung zur Behandlung der Aktionstypen created
und 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
Füge nach dem Fall rerequested
, wo # ADD REQUESTED_ACTION METHOD HERE #
steht, den folgenden Code ein:
when 'requested_action' take_requested_action
when 'requested_action'
take_requested_action
Mit diesem Code wird eine neue Methode aufgerufen, mit der alle requested_action
-Ereignisse für deine App verarbeitet werden.
Füge in den Codeblock, der mit helpers do
beginnt, an der Stelle, an der # ADD TAKE_REQUESTED_ACTION HELPER METHOD HERE #
steht, die folgende Hilfsmethode ein:
# 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
Mit dem obigen Code wird wie mit dem Code, den du in Schritt 2.2.: RuboCop das Klonen des Test-Repositorys erlauben hinzugefügt hast, ein Repository geklont. Mit einer if
-Anweisung wird überprüft, ob der Bezeichner der angeforderten Aktion dem Bezeichner der RuboCop-Schaltfläche (fix_rubocop_notices
) entspricht. Wenn die Bezeichner übereinstimmen, klont der Code das Repository, legt den Git-Benutzernamen und die E-Mail-Adresse fest und führt RuboCop mit der Option --auto-correct
aus. Die Änderungen werden mit der Option --auto-correct
automatisch auf die lokalen CI-Serverdateien angewendet.
Die Dateien werden lokal geändert. Du musst sie jedoch noch an GitHub pushen. Zum Committen aller Dateien verwendest du das ruby-git
-Gem. Git verfügt über einen Befehl, mit dem alle geänderten oder gelöschten Dateien gestagt und committet werden: git commit -a
. Zum Erledigen derselben Aufgabe mit ruby-git
verwendet der obige Code die Methode commit_all
. Danach pusht der Code die committeten Dateien mithilfe des Installationstokens in GitHub und verwendet dabei dieselbe Authentifizierungsmethode wie der Git-Befehl clone
. Schließlich entfernt er das Repositoryverzeichnis, um sicherzustellen, dass das Arbeitsverzeichnis für das nächste Ereignis vorbereitet ist.
Der von dir geschriebene Code vervollständigt nun deinen Continuous Integration-Server, den du mithilfe einer GitHub App erstellt hast, und überprüft diesen. Den vollständigen endgültigen Code für deine App findest du unter Vollständiges Codebeispiel.
Testen des Codes
Die folgenden Schritte zeigen dir, wie du testen kannst, ob der Code funktioniert und wie RuboCop die gefundenen Fehler automatisch beheben kann.
-
Führe den folgenden Befehl aus, um den Server über dein Terminal neu zu starten. Wenn der Server bereits ausgeführt wird, gib zuerst
Ctrl-C
in dein Terminal ein, um den Server zu beenden, und führe dann den folgenden Befehl aus, um den Server erneut zu starten.Shell ruby server.rb
ruby server.rb
-
Erstelle im Repository, in dem du die
myfile.rb
-Datei hinzugefügt hast, einen neuen Pull Request. -
Navigiere im neu erstellten Pull Request zur Registerkarte Überprüfungen, und klicke auf die Schaltfläche „Problem beheben“, um von RuboCop gefundene Fehler automatisch zu beheben.
-
Navigiere zur Registerkarte Commits. Dir wird ein neuer Commit mit dem Benutzernamen angezeigt, den du in deiner Git-Konfiguration festgelegt hast. Möglicherweise musst du den Browser aktualisieren, damit das Update angezeigt wird.
-
Navigiere zur Registerkarte Überprüfungen. Es sollte eine neue Überprüfungssammlung für Octo RuboCop angezeigt werden. Dieses Mal sollte es jedoch keine Fehler geben, da RuboCop sie alle behoben hat.
Vollständiges Codebeispiel
So sollte der endgültige Code in server.rb
aussehen, nachdem du alle Schritte in diesem Tutorial ausgeführt hast. Es gibt auch Kommentare im gesamten Code, die zusätzlichen Kontext liefern.
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
Nächste Schritte
Du solltest jetzt eine App haben, die API-Ereignisse empfängt, Überprüfungsausführungen erstellt, RuboCop verwendet, um Ruby-Fehler zu finden, Anmerkungen in einem Pull Request erstellt und automatisch Linterfehler behebt. Als Nächstes kannst du den Code deiner App erweitern, die App bereitstellen und sie öffentlich machen.
Wenn Sie Fragen haben, beginnen Sie eine GitHub Community-Diskussion in der API- und Webhooks-Kategorie.
Ändern des App-Codes
In diesem Tutorial wurde gezeigt, wie du die Schaltfläche „Problem beheben“ erstellst, die immer in Pull Requests im Repository angezeigt wird. Versuche, den Code so zu aktualisieren, dass die Schaltfläche „Problem beheben“ nur angezeigt wird, wenn RuboCop Fehler findet.
Wenn Dateien von RuboCop nicht direkt in den Hauptbranch committet werden sollen, kannst du den Code so aktualisieren, dass stattdessen ein Pull Request mit einem neuen Branch erstellt wird, der auf dem Hauptbranch basiert.
Bereitstellen deiner App
In diesem Tutorial wurde gezeigt, wie du deine App lokal entwickelst. Wenn du bereit bist, deine App bereitzustellen, musst du Änderungen vornehmen, um deine App zu bedienen und die Anmeldeinformationen deiner App zu schützen. Welche Schritte du ausführst, hängt vom verwendeten Server ab, aber die folgenden Abschnitte enthalten allgemeine Anleitungen.
Hosten deiner App auf einem Server
In diesem Tutorial wurde dein Computer oder Codespace als Server verwendet. Sobald die App für die Verwendung in der Produktion bereit ist, solltest du deine App auf einem dedizierten Server bereitstellen. Du kannst beispielsweise Azure App Service nutzen.
Aktualisieren der Webhook-URL
Sobald du über einen Server verfügst, der für den Empfang von Webhookdatenverkehr von GitHub eingerichtet ist, aktualisiere die Webhook-URL in deinen App-Einstellungen. Du solltest „Smee.io“ nicht verwenden, um deine Webhooks in der Produktion weiterzuleiten.
Aktualisieren der :port
-Einstellung
Wenn du deine App bereitstellst, solltest du den Port ändern, auf den dein Server lauscht. Der Code weist deinen Server bereits an, auf alle verfügbaren Netzwerkschnittstellen zu lauschen, indem :bind
auf 0.0.0.0
festgelegt wird.
Beispielsweise kannst du eine PORT
-Umgebungsvariable in deiner .env
-Datei auf deinem Server festlegen, um den Port anzugeben, auf den dein Server lauschen soll. Anschließend kannst du die Stelle aktualisieren, wo dein Code die Konstanten :port
festlegt, sodass dein Server auf deinen Bereitstellungsport lauscht:
set :port, ENV['PORT']
set :port, ENV['PORT']
Schützen der Anmeldeinformationen deiner App
Du solltest niemals den privaten Schlüssel deiner App oder das Webhookgeheimnis veröffentlichen. In diesem Tutorial wurden die Anmeldeinformationen deiner App in einer von Git ignorierten .env
-Datei gespeichert. Wenn du deine App bereitstellst, solltest du eine sichere Methode zum Speichern der Anmeldeinformationen auswählen und deinen Code aktualisieren, um den Wert entsprechend abzurufen. Du kannst die Anmeldeinformationen beispielsweise mit einem Geheimnisverwaltungsdienst wie Azure Key Vault speichern. Wenn deine App ausgeführt wird, kann sie die Anmeldeinformationen abrufen und in Umgebungsvariablen auf dem Server speichern, auf dem deine App bereitgestellt wird.
Weitere Informationen findest du unter Best Practices beim Erstellen einer GitHub-App.
Freigeben der App
Wenn du deine App für andere Benutzer und Organisationen freigeben möchtest, mache deine App öffentlich. Weitere Informationen findest du unter Öffentlich- oder Privatmachen einer GitHub-App.
Bewährte Methode befolgen
Du solltest Best Practices für deine GitHub App befolgen. Weitere Informationen findest du unter Best Practices beim Erstellen einer GitHub-App.